Hi all,
I’ve gone ahead and rewritten the solvers within solver.py
so that each solver is now a subclass of the base Solver()
class. This is in the feature_solverclasses
branch at the moment, but I will merge into develop
shortly.
Previously the Solver()
class looked like the below, with the actual solver stored as a lambda
function in Solver().solver
. This was then stored in Poly().solver
, and polynomial coefficients were obtained with x=Poly().solver(A,b)
.
class Solver(object):
"""
Returns solver functions for solving Ax=b
:param string method: The method used for solving the linear system. Options include: ``compressed-sensing``, ``least-squares``, ``minimum-norm``, ``numerical-integration``, ``least-squares-with-gradients``, ``least-absolute-residual``, ``huber`` and ``elastic-net``.
:param dict solver_args: Optional arguments centered around the specific solver.
:param numpy.ndarray noise-level: The noise-level to be used in the basis pursuit de-noising solver.
:param bool verbose: Default value of this input is set to ``False``; when ``True`` a string is printed to the screen detailing the solver convergence and condition number of the matrix.
"""
2 def __init__(self, method, solver_args):
self.method = method
self.solver_args = solver_args
self.param1 = None
self.verbose = False
if self.solver_args is not None:
if 'param1' in self.solver_args: self.param1 = solver_args.get('param1')
if 'verbose' in self.solver_args: self.verbose = solver_args.get('verbose')
if self.method.lower() == 'least-squares':
self.solver = lambda A, b: least_squares(A, b, self.verbose)
In the new approach, parameters and methods shared across all the solvers are defined in the base Solver()
class, and each solver is then a subclass of this class i.e.
# Least squares solver subclass.
################################
class least_squares(Solver):
"""
Ordinary least squares solver.
:param dict solver_args: Optional arguments for the solver.
:param bool verbose: Default value of this input is set to ``False``; when ``True``, the solver prints information to screen.
"""
def __init__(self,solver_args={}):
# Init base class
super().__init__('least-squares',solver_args)
def solve(self, A, b):
if np.__version__ < '1.14':
alpha = np.linalg.lstsq(A, b)
else:
alpha = np.linalg.lstsq(A, b, rcond=None)
if self.verbose is True:
print('The condition number of the matrix is '+str(np.linalg.cond(A))+'.')
self.coefficients = alpha[0]
Now the entire solver subcass is stored within Poly().solver
, and the coefficients are obtained with x=Poly().solver.get_coefficients(A, b)
. This approach has a number of advantages:
- Cleaner code i.e. avoids polluting namespace when multiple solvers have similar parameters or methods.
- Each solver can have its own methods i.e. I’m working on a plotting method to plot the regularisation path of the
elastic-net
solver. - For more complex iterative solvers, we could do things like warm-starting with solver solution from previously fitted
Poly()
. - Can play around with the solvers themselves (see below!).
Creating Polynomials
In terms of the operation of Poly()
, nothing has changed on the outside e.g. to define a polynomial with the elastic-net
solver:
mypoly = eq.Poly(parameters=myparam, basis=mybasis, method='elastic-net',
sampling_args={'mesh':'user-defined',
'sample-points':X, 'sample-outputs':y},
solver_args={'verbose':False,'path':True})
Creating Solver objects
If you want to experiment with the solvers with your own \mathbf{A}\mathbf{x}=\mathbf{b} system, you can now define and use the solver objects independently. For example:
import equadratures.solver as solver
# select_solver will return the requested solver subclass
mysolver = solver.Solver.select_solver('least-squares',solver_args={'verbose':True})
# Or you can select the solversubclass directly
#mysolver = solver.least_squares(solver_args={'verbose':True})
# You can manually run solve and then get_coefficients
#mysolver.solve(A,b)
#x = mysolver.get_coefficients()
# Or providing get_coefficients() with A and b will automatically run solve()
x = mysolver.get_coefficients(A,b)
Custom solver class
I’ve also added a custom-solver
subclass, where the user can provide their own solver function within solver_args
. The only limitations of the custom solver function are that it must accept A
and b
, and return the coefficients. Additional arguments can also be included in solver_args
, and they will be piped into the custom function as **kwargs
.
# As an example just copy the standard least-squares solver
def lsq(A, b, verbose=False):
alpha = np.linalg.lstsq(A, b, rcond=None)
if verbose is True:
print('The condition number of the matrix is '+str(np.linalg.cond(A))+'.')
return alpha[0]
mypoly = eq.Poly(parameters=myparam, basis=mybasis, method='custom-solver',
sampling_args={'mesh':'user-defined', 'sample-points':X, 'sample-outputs':y},
solver_args={'solve':lsq,'verbose':True})
This could be useful for prototyping new solvers!