Source code for pybamm.solvers.algebraic_solver

#
# Algebraic solver class
#
import casadi
import pybamm
import numpy as np
from scipy import optimize
from scipy.sparse import issparse


[docs] class AlgebraicSolver(pybamm.BaseSolver): """Solve a discretised model which contains only (time independent) algebraic equations using a root finding algorithm. Uses scipy.optimize.root. Note: this solver could be extended for quasi-static models, or models in which the time derivative is manually discretised and results in a (possibly nonlinear) algebaric system at each time level. Parameters ---------- method : str, optional The method to use to solve the system (default is "lm"). If it starts with "lsq", least-squares minimization is used. The method for least-squares can be specified in the form "lsq_methodname" tol : float, optional The tolerance for the solver (default is 1e-6). extra_options : dict, optional Any options to pass to the rootfinder. Vary depending on which method is chosen. Please consult `SciPy documentation <https://tinyurl.com/ybr6cfqs>`_ for details. """ def __init__(self, method="lm", tol=1e-6, extra_options=None): super().__init__(method=method) self.tol = tol self.extra_options = extra_options or {} self.name = f"Algebraic solver ({method})" self.algebraic_solver = True pybamm.citations.register("Virtanen2020") @property def tol(self): return self._tol @tol.setter def tol(self, value): self._tol = value def _integrate(self, model, t_eval, inputs_dict=None): """ Calculate the solution of the algebraic equations through root-finding Parameters ---------- model : :class:`pybamm.BaseModel` The model whose solution to calculate. t_eval : :class:`numpy.array`, size (k,) The times at which to compute the solution inputs_dict : dict, optional Any input parameters to pass to the model when solving """ inputs_dict = inputs_dict or {} if model.convert_to_format == "casadi": inputs = casadi.vertcat(*[x for x in inputs_dict.values()]) else: inputs = inputs_dict y0 = model.y0 if isinstance(y0, casadi.DM): y0 = y0.full() y0 = y0.flatten() # The casadi algebraic solver can read rhs equations, but leaves them unchanged # i.e. the part of the solution vector that corresponds to the differential # equations will be equal to the initial condition provided. This allows this # solver to be used for initialising the DAE solvers # Split y0 into differential and algebraic if model.rhs == {}: len_rhs = 0 else: len_rhs = model.rhs_eval(t_eval[0], y0, inputs).shape[0] y0_diff, y0_alg = np.split(y0, [len_rhs]) test_result = model.algebraic_eval(0, y0, inputs) if isinstance(test_result, casadi.DM): def algebraic(t, y): result = model.algebraic_eval(t, y, inputs) return result.full().flatten() else: def algebraic(t, y): result = model.algebraic_eval(t, y, inputs) return result.flatten() y_alg = np.empty((len(y0_alg), len(t_eval))) timer = pybamm.Timer() integration_time = 0 for idx, t in enumerate(t_eval): def root_fun(y_alg, t=t): "Evaluates algebraic using y" y = np.concatenate([y0_diff, y_alg]) out = algebraic(t, y) pybamm.logger.debug( f"Evaluating algebraic equations at t={t}, L2-norm is {np.linalg.norm(out)}" ) return out jac = model.jac_algebraic_eval if jac: if issparse(jac(t_eval[0], y0, inputs)): def jac_fn(y_alg, jac=jac): """ Evaluates Jacobian using y0_diff (fixed) and y_alg (varying) """ y = np.concatenate([y0_diff, y_alg]) return jac(0, y, inputs)[:, len_rhs:].toarray() else: def jac_fn(y_alg, jac=jac): """ Evaluates Jacobian using y0_diff (fixed) and y_alg (varying) """ y = np.concatenate([y0_diff, y_alg]) return jac(0, y, inputs)[:, len_rhs:] else: jac_fn = None itr = 0 maxiter = 2 success = False while not success: # Methods which use least-squares are specified as either "lsq", # which uses the default method, or with "lsq__methodname" if self.method.startswith("lsq"): if self.method == "lsq": method = "trf" else: method = self.method[5:] if jac_fn is None: jac_fn = "2-point" timer.reset() sol = optimize.least_squares( root_fun, y0_alg, method=method, ftol=self.tol, jac=jac_fn, bounds=model.bounds, **self.extra_options, ) integration_time += timer.time() # Methods which use minimize are specified as either "minimize", # which uses the default method, or with "minimize__methodname" elif self.method.startswith("minimize"): # Adapt the root function for minimize def root_norm(y): return np.sum(root_fun(y) ** 2) if jac_fn is None: jac_norm = None else: def jac_norm(y, jac_fn=jac_fn): return np.sum(2 * root_fun(y) * jac_fn(y), 0) if self.method == "minimize": method = None else: method = self.method[10:] extra_options = self.extra_options if np.any(model.bounds[0] != -np.inf) or np.any( model.bounds[1] != np.inf ): bounds = [ (lb, ub) for lb, ub in zip(model.bounds[0], model.bounds[1]) ] extra_options["bounds"] = bounds timer.reset() sol = optimize.minimize( root_norm, y0_alg, method=method, tol=self.tol, jac=jac_norm, **extra_options, ) integration_time += timer.time() else: timer.reset() sol = optimize.root( root_fun, y0_alg, method=self.method, tol=self.tol, jac=jac_fn, options=self.extra_options, ) integration_time += timer.time() if sol.success and np.all(abs(sol.fun) < self.tol): # update initial guess for the next iteration y0_alg = sol.x # update solution array y_alg[:, idx] = y0_alg success = True elif not sol.success: raise pybamm.SolverError( f"Could not find acceptable solution: {sol.message}" ) else: y0_alg = sol.x if itr > maxiter: raise pybamm.SolverError( "Could not find acceptable solution: solver terminated " "successfully, but maximum solution error " f"({np.max(abs(sol.fun))}) above tolerance ({self.tol})" ) itr += 1 # Concatenate differential part y_diff = np.r_[[y0_diff] * len(t_eval)].T y_sol = np.r_[y_diff, y_alg] # Return solution object (no events, so pass None to t_event, y_event) sol = pybamm.Solution( t_eval, y_sol, model, inputs_dict, termination="final time" ) sol.integration_time = integration_time return sol