From a59abb58dc4f3231a7ff2a4438491c43a88b99b6 Mon Sep 17 00:00:00 2001 From: Valentin Sulzer Date: Wed, 8 Jul 2020 18:28:18 -0400 Subject: [PATCH 01/73] #1100 starting to add SDAEs (odes only for now) --- pybamm/discretisations/discretisation.py | 5 + pybamm/solvers/base_solver.py | 82 ++++++++++++-- pybamm/solvers/casadi_solver.py | 5 +- pybamm/solvers/processed_symbolic_variable.py | 3 - pybamm/solvers/processed_variable.py | 75 +++++++++++++ pybamm/solvers/scipy_solver.py | 32 +++++- pybamm/solvers/solution.py | 102 ++++++++++++++++-- tests/unit/test_solvers/test_scipy_solver.py | 93 +++++++++++++++- 8 files changed, 366 insertions(+), 31 deletions(-) diff --git a/pybamm/discretisations/discretisation.py b/pybamm/discretisations/discretisation.py index 1e6de70f92..67a71cf542 100644 --- a/pybamm/discretisations/discretisation.py +++ b/pybamm/discretisations/discretisation.py @@ -205,6 +205,11 @@ def process_model(self, model, inplace=True, check_model=True): model_disc.rhs, model_disc.concatenated_rhs = rhs, concat_rhs model_disc.algebraic, model_disc.concatenated_algebraic = alg, concat_alg + # Save length of rhs and algebraic + model_disc.len_rhs = model_disc.concatenated_rhs.size + model_disc.len_alg = model_disc.concatenated_algebraic.size + model_disc.len_rhs_and_alg = model_disc.len_rhs + model_disc.len_alg + # Process events processed_events = [] pybamm.logger.info("Discretise events for {}".format(model.name)) diff --git a/pybamm/solvers/base_solver.py b/pybamm/solvers/base_solver.py index 2b26a36a16..6ad10a5d76 100644 --- a/pybamm/solvers/base_solver.py +++ b/pybamm/solvers/base_solver.py @@ -30,6 +30,12 @@ class BaseSolver(object): specified by 'root_method' (e.g. "lm", "hybr", ...) root_tol : float, optional The tolerance for the initial-condition solver (default is 1e-6). + solve_sensitivity_equations : bool, optional + Whether to explicitly formulate the sensitivity equations for sensitivity + to input parameters. The formulation is as per "Park, S., Kato, D., Gima, Z., + Klein, R., & Moura, S. (2018). Optimal experimental design for parameterization + of an electrochemical lithium-ion battery model. Journal of The Electrochemical + Society, 165(7), A1309.". See #1100 for details """ def __init__( @@ -40,6 +46,7 @@ def __init__( root_method=None, root_tol=1e-6, max_steps="deprecated", + solve_sensitivity_equations=False, ): self._method = method self._rtol = rtol @@ -57,6 +64,7 @@ def __init__( self.name = "Base solver" self.ode_solver = False self.algebraic_solver = False + self.solve_sensitivity_equations = solve_sensitivity_equations @property def method(self): @@ -191,17 +199,28 @@ def set_up(self, model, inputs=None): ) model.convert_to_format = "casadi" + # Only allow solving sensitivity equations with the casadi format for now + if ( + self.solve_sensitivity_equations is True + and model.convert_to_format != "casadi" + ): + raise NotImplementedError( + "model should be converted to casadi format in order to solve " + "sensitivity equations" + ) + if model.convert_to_format != "casadi": simp = pybamm.Simplification() # Create Jacobian from concatenated rhs and algebraic - y = pybamm.StateVector(slice(0, model.concatenated_initial_conditions.size)) + y = pybamm.StateVector(slice(0, model.len_rhs_and_alg)) # set up Jacobian object, for re-use of dict jacobian = pybamm.Jacobian() else: # Convert model attributes to casadi t_casadi = casadi.MX.sym("t") - y_diff = casadi.MX.sym("y_diff", model.concatenated_rhs.size) - y_alg = casadi.MX.sym("y_alg", model.concatenated_algebraic.size) + # Create the symbolic state vectors + y_diff = casadi.MX.sym("y_diff", model.len_rhs) + y_alg = casadi.MX.sym("y_alg", model.len_alg) y_casadi = casadi.vertcat(y_diff, y_alg) p_casadi = {} for name, value in inputs.items(): @@ -210,6 +229,13 @@ def set_up(self, model, inputs=None): else: p_casadi[name] = casadi.MX.sym(name, value.shape[0]) p_casadi_stacked = casadi.vertcat(*[p for p in p_casadi.values()]) + # sensitivity vectors + if self.solve_sensitivity_equations is True: + S_x = casadi.MX.sym("S_x", model.len_rhs * p_casadi_stacked.shape[0]) + S_z = casadi.MX.sym("S_z", model.len_alg * p_casadi_stacked.shape[0]) + y_and_S = casadi.vertcat(y_diff, S_x, y_alg, S_z) + else: + y_and_S = y_casadi def process(func, name, use_jacobian=None): def report(string): @@ -258,16 +284,40 @@ def report(string): # Process with CasADi report(f"Converting {name} to CasADi") func = func.to_casadi(t_casadi, y_casadi, inputs=p_casadi) + # Add sensitivity vectors to the rhs and algebraic equations + if self.solve_sensitivity_equations is True: + if name == "rhs": + report(f"Creating sensitivity equations for rhs using CasADi") + df_dx = casadi.jacobian(func, y_diff) + df_dp = casadi.jacobian(func, p_casadi_stacked) + if model.len_alg == 0: + S_rhs = df_dx @ S_x + df_dp + else: + df_dz = casadi.jacobian(func, y_alg) + S_rhs = df_dx @ S_x + df_dz @ S_z + df_dp + func = casadi.vertcat(func, S_rhs) + elif name == "initial_conditions": + if model.len_rhs == 0 or model.len_alg == 0: + S_0 = casadi.jacobian(func, p_casadi_stacked).reshape( + (-1, 1) + ) + func = casadi.vertcat(func, S_0) + else: + x0 = func[: model.len_rhs] + z0 = func[model.len_rhs :] + Sx_0 = casadi.jacobian(x0, p_casadi_stacked) + Sz_0 = casadi.jacobian(z0, p_casadi_stacked) + func = casadi.vertcat(x0, Sx_0, z0, Sz_0) if use_jacobian: report(f"Calculating jacobian for {name} using CasADi") - jac_casadi = casadi.jacobian(func, y_casadi) + jac_casadi = casadi.jacobian(func, y_and_S) jac = casadi.Function( - name, [t_casadi, y_casadi, p_casadi_stacked], [jac_casadi] + name, [t_casadi, y_and_S, p_casadi_stacked], [jac_casadi] ) else: jac = None func = casadi.Function( - name, [t_casadi, y_casadi, p_casadi_stacked], [func] + name, [t_casadi, y_and_S, p_casadi_stacked], [func] ) if name == "residuals": func_call = Residuals(func, name, model) @@ -277,6 +327,7 @@ def report(string): jac_call = SolverCallable(jac, name + "_jac", model) else: jac_call = None + return func, func_call, jac_call # Check for heaviside functions in rhs and algebraic and add discontinuity @@ -324,8 +375,18 @@ def report(string): )[0] init_eval = InitialConditions(initial_conditions, model) + if self.solve_sensitivity_equations is True: + init_eval.y_dummy = np.zeros( + ( + model.len_rhs_and_alg * (np.vstack(list(inputs.values())).size + 1), + 1, + ) + ) + else: + init_eval.y_dummy = np.zeros((model.len_rhs_and_alg, 1)) + # Process rhs, algebraic and event expressions - rhs, rhs_eval, jac_rhs = process(model.concatenated_rhs, "RHS") + rhs, rhs_eval, jac_rhs = process(model.concatenated_rhs, "rhs") algebraic, algebraic_eval, jac_algebraic = process( model.concatenated_algebraic, "algebraic" ) @@ -423,7 +484,7 @@ def _set_initial_conditions(self, model, inputs, update_rhs): y0_from_inputs = model.init_eval(inputs) # Reuse old solution for algebraic equations y0_from_model = model.y0 - len_rhs = model.concatenated_rhs.size + len_rhs = model.len_rhs # update model.y0, which is used for initialising the algebraic solver if len_rhs == 0: model.y0 = y0_from_model @@ -861,7 +922,7 @@ def __init__(self, function, name, model): def __call__(self, t, y, inputs): y = y.reshape(-1, 1) - if self.name in ["RHS", "algebraic", "residuals"]: + if self.name in ["rhs", "algebraic", "residuals"]: pybamm.logger.debug( "Evaluating {} for {} at t={}".format( self.name, self.model.name, t * self.timescale @@ -874,7 +935,7 @@ def __call__(self, t, y, inputs): def function(self, t, y, inputs): if self.form == "casadi": states_eval = self._function(t, y, inputs) - if self.name in ["RHS", "algebraic", "residuals", "event"]: + if self.name in ["rhs", "algebraic", "residuals", "event"]: return states_eval.full() else: # keep jacobians sparse @@ -901,7 +962,6 @@ class InitialConditions(SolverCallable): def __init__(self, function, model): super().__init__(function, "initial conditions", model) - self.y_dummy = np.zeros(model.concatenated_initial_conditions.shape) def __call__(self, inputs): if self.form == "casadi": diff --git a/pybamm/solvers/casadi_solver.py b/pybamm/solvers/casadi_solver.py index 311a450827..fc2a248eb7 100644 --- a/pybamm/solvers/casadi_solver.py +++ b/pybamm/solvers/casadi_solver.py @@ -363,9 +363,8 @@ def get_integrator(self, model, inputs, t_eval=None): def _run_integrator(self, model, y0, inputs, t_eval): integrator, use_grid = self.integrators[model] - len_rhs = model.concatenated_rhs.size - y0_diff = y0[:len_rhs] - y0_alg = y0[len_rhs:] + y0_diff = y0[: model.len_rhs] + y0_alg = y0[model.len_rhs :] try: # Try solving if use_grid is True: diff --git a/pybamm/solvers/processed_symbolic_variable.py b/pybamm/solvers/processed_symbolic_variable.py index 5e23a055d0..f6461d3a25 100644 --- a/pybamm/solvers/processed_symbolic_variable.py +++ b/pybamm/solvers/processed_symbolic_variable.py @@ -110,9 +110,6 @@ def initialise_0D(self): def initialise_1D(self): "Create a 1D variable" - len_space = self.base_eval.shape[0] - entries = np.empty((len_space, len(self.t_sol))) - # Evaluate the base_variable index-by-index for idx in range(len(self.t_sol)): t = self.t_sol[idx] diff --git a/pybamm/solvers/processed_variable.py b/pybamm/solvers/processed_variable.py index 5dce767dd9..531ee77f00 100644 --- a/pybamm/solvers/processed_variable.py +++ b/pybamm/solvers/processed_variable.py @@ -1,6 +1,7 @@ # # Processed Variable class # +import casadi import numbers import numpy as np import pybamm @@ -59,6 +60,10 @@ def __init__(self, base_variable, solution, known_evals=None, warn=True): self.known_evals = known_evals self.warn = warn + # Sensitivity starts off uninitialized, only set when called + self._sensitivity = None + self.solution_sensitivity = solution.sensitivity + # Set timescale self.timescale = solution.model.timescale.evaluate() self.t_pts = self.t_sol * self.timescale @@ -553,6 +558,76 @@ def data(self): "Same as entries, but different name" return self.entries + @property + def sensitivity(self): + """ + Returns a dictionary of sensitivity for each input parameter. + The keys are the input parameters, and the value is a matrix of size + (n_x * n_t, n_p), where n_x is the number of states, n_t is the number of time + points, and n_p is the size of the input parameter + """ + # No sensitivity if there are no inputs + if len(self.inputs) == 0: + return {} + # Otherwise initialise and return sensitivity + if self._sensitivity is None: + self.initialise_sensitivity() + return self._sensitivity + + def initialise_sensitivity(self): + "Set up the sensitivity dictionary" + inputs_stacked = casadi.vertcat(*[p for p in self.inputs.values()]) + + # Set up symbolic variables + t_casadi = casadi.MX.sym("t") + y_casadi = casadi.MX.sym("y", self.u_sol.shape[0]) + p_casadi = { + name: casadi.MX.sym(name, value.shape[0]) + for name, value in self.inputs.items() + } + p_casadi_stacked = casadi.vertcat(*[p for p in p_casadi.values()]) + + # Convert variable to casadi format for differentiating + var_casadi = self.base_variable.to_casadi(t_casadi, y_casadi, inputs=p_casadi) + dvar_dy = casadi.jacobian(var_casadi, y_casadi) + dvar_dp = casadi.jacobian(var_casadi, p_casadi_stacked) + + # Convert to functions and evaluate index-by-index + dvar_dy_func = casadi.Function( + "dvar_dy", [t_casadi, y_casadi, p_casadi_stacked], [dvar_dy] + ) + dvar_dp_func = casadi.Function( + "dvar_dp", [t_casadi, y_casadi, p_casadi_stacked], [dvar_dp] + ) + for idx in range(len(self.t_sol)): + t = self.t_sol[idx] + u = self.u_sol[:, idx] + inp = inputs_stacked[:, idx] + next_dvar_dy_eval = dvar_dy_func(t, u, inp) + next_dvar_dp_eval = dvar_dp_func(t, u, inp) + if idx == 0: + dvar_dy_eval = next_dvar_dy_eval + dvar_dp_eval = next_dvar_dp_eval + else: + dvar_dy_eval = casadi.diagcat(dvar_dy_eval, next_dvar_dy_eval) + dvar_dp_eval = casadi.vertcat(dvar_dp_eval, next_dvar_dp_eval) + + # Compute sensitivity + dy_dp = self.solution_sensitivity["all"] + S_var = dvar_dy_eval @ dy_dp + dvar_dp_eval + + sensitivity = {"all": S_var} + + # Add the individual sensitivity + start = 0 + for name, inp in self.inputs.items(): + end = start + inp.shape[0] + sensitivity[name] = S_var[:, start:end] + start = end + + # Save attribute + self._sensitivity = sensitivity + def eval_dimension_name(name, x, r, y, z): if name == "x": diff --git a/pybamm/solvers/scipy_solver.py b/pybamm/solvers/scipy_solver.py index 613ae72a51..d8abcb90fe 100644 --- a/pybamm/solvers/scipy_solver.py +++ b/pybamm/solvers/scipy_solver.py @@ -23,10 +23,24 @@ class ScipySolver(pybamm.BaseSolver): Any options to pass to the solver. Please consult `SciPy documentation `_ for details. + solve_sensitivity_equations : bool, optional + See :class:`pybamm.BaseSolver` """ - def __init__(self, method="BDF", rtol=1e-6, atol=1e-6, extra_options=None): - super().__init__(method, rtol, atol) + def __init__( + self, + method="BDF", + rtol=1e-6, + atol=1e-6, + extra_options=None, + solve_sensitivity_equations=False, + ): + super().__init__( + method=method, + rtol=rtol, + atol=atol, + solve_sensitivity_equations=solve_sensitivity_equations, + ) self.ode_solver = True self.extra_options = extra_options or {} self.name = "Scipy solver ({})".format(method) @@ -52,8 +66,10 @@ def _integrate(self, model, t_eval, inputs=None): various diagnostic messages. """ + # Save inputs dictionary, and if necessary convert inputs to a casadi vector + inputs_dict = inputs if model.convert_to_format == "casadi": - inputs = casadi.vertcat(*[x for x in inputs.values()]) + inputs = casadi.vertcat(*[x for x in inputs_dict.values()]) extra_options = {**self.extra_options, "rtol": self.rtol, "atol": self.atol} @@ -107,6 +123,14 @@ def event_fn(t, y): termination = "final time" t_event = None y_event = np.array(None) - return pybamm.Solution(sol.t, sol.y, t_event, y_event, termination) + return pybamm.Solution( + sol.t, + sol.y, + t_event, + y_event, + termination, + model=model, + inputs=inputs_dict, + ) else: raise pybamm.SolverError(sol.message) diff --git a/pybamm/solvers/solution.py b/pybamm/solvers/solution.py index c94b03791d..33b539a02f 100644 --- a/pybamm/solvers/solution.py +++ b/pybamm/solvers/solution.py @@ -34,29 +34,98 @@ class _BaseSolution(object): String to indicate why the solution terminated copy_this : :class:`pybamm.Solution`, optional A solution to copy, if provided. Default is None. + model : a pybamm model, optional + Model from which the solution was obtained. Default is None, in which case + :class:`pybamm.BaseModel` is used. + inputs : dict, optional + Inputs for the solution. Default is None (empty dict) """ def __init__( - self, t, y, t_event=None, y_event=None, termination="final time", copy_this=None + self, + t, + y, + t_event=None, + y_event=None, + termination="final time", + copy_this=None, + model=None, + inputs=None, ): self._t = t if isinstance(y, casadi.DM): y = y.full() - self._y = y + + # if model or inputs are None, initialize empty, to be populated later + self.inputs = inputs or pybamm.FuzzyDict() + self._model = model or pybamm.BaseModel() + + # If the model has been provided, split up y into solution and sensitivity + # Don't do this if the sensitivity equations have not been computed (i.e. if + # y only has the shape or the rhs and alg solution) + if model is None or model.len_rhs_and_alg == y.shape[0]: + self._y = y + else: + all_inputs_size = np.vstack(list(inputs.values())).size + # Get the point where the algebraic equations start + len_rhs_and_sens = all_inputs_size * model.len_rhs + # self._y gets the part of the solution vector that correspond to the + # actual ODE/DAE solution + self._y = np.vstack( + [ + y[: model.len_rhs, :], + y[len_rhs_and_sens : len_rhs_and_sens + model.len_alg, :], + ] + ) + # save sensitivities as a dictionary + # first save the whole sensitivity matrix + # reshape using Fortran order to get the right array: + # t0_x0_p0, t0_x0_p1, ..., t0_x0_pn + # t0_x1_p0, t0_x1_p1, ..., t0_x1_pn + # ... + # t0_xn_p0, t0_xn_p1, ..., t0_xn_pn + # t1_x0_p0, t1_x0_p1, ..., t1_x0_pn + # t1_x1_p0, t1_x1_p1, ..., t1_x1_pn + # ... + # t1_xn_p0, t1_xn_p1, ..., t1_xn_pn + # ... + # tn_x0_p0, tn_x0_p1, ..., tn_x0_pn + # tn_x1_p0, tn_x1_p1, ..., tn_x1_pn + # ... + # tn_xn_p0, tn_xn_p1, ..., tn_xn_pn + full_sens_matrix = np.vstack( + [ + y[model.len_rhs : len_rhs_and_sens, :], + y[len_rhs_and_sens + model.len_alg :, :], + ] + ).reshape(np.prod(self._y.shape), all_inputs_size, order="F") + sensitivity = {"all": full_sens_matrix} + # also save the sensitivity wrt each parameter + start_rhs = model.len_rhs + start_alg = len_rhs_and_sens + model.len_alg + for i, (name, inp) in enumerate(inputs.items()): + if isinstance(inp, numbers.Number): + input_size = 1 + else: + input_size = inp.shape[0] + end_rhs = start_rhs + model.len_rhs * input_size + end_alg = start_alg + model.len_alg * input_size + sensitivity[name] = np.vstack( + [y[start_rhs:end_rhs, :], y[start_alg:end_alg, :],] + ).reshape(-1, 1) + start_rhs = end_rhs + start_alg = end_alg + self.sensitivity = sensitivity + self._t_event = t_event self._y_event = y_event self._termination = termination if copy_this is None: - # initialize empty inputs and model, to be populated later - self._inputs = pybamm.FuzzyDict() - self._model = pybamm.BaseModel() self.set_up_time = None self.solve_time = None self.has_symbolic_inputs = False else: - self._inputs = copy.copy(copy_this.inputs) - self._model = copy_this.model self.set_up_time = copy_this.set_up_time self.solve_time = copy_this.solve_time self.has_symbolic_inputs = copy_this.has_symbolic_inputs @@ -271,8 +340,19 @@ class Solution(_BaseSolution): """ - def __init__(self, t, y, t_event=None, y_event=None, termination="final time"): - super().__init__(t, y, t_event, y_event, termination) + def __init__( + self, + t, + y, + t_event=None, + y_event=None, + termination="final time", + model=None, + inputs=None, + ): + super().__init__( + t, y, t_event, y_event, termination, model=model, inputs=inputs + ) self.base_solution_class = _BaseSolution @property @@ -311,6 +391,8 @@ def append(self, solution, start_index=1, create_sub_solutions=False): self.y_event, self.termination, copy_this=self, + model=self.model, + inputs=copy.copy(self.inputs), ) ] @@ -347,5 +429,7 @@ def append(self, solution, start_index=1, create_sub_solutions=False): solution.y_event, solution.termination, copy_this=solution, + model=self.model, + inputs=copy.copy(self.inputs), ) ) diff --git a/tests/unit/test_solvers/test_scipy_solver.py b/tests/unit/test_solvers/test_scipy_solver.py index fae2775b3f..c4b28d6d6e 100644 --- a/tests/unit/test_solvers/test_scipy_solver.py +++ b/tests/unit/test_solvers/test_scipy_solver.py @@ -4,12 +4,13 @@ import pybamm import unittest import numpy as np -from tests import get_mesh_for_testing +from tests import get_mesh_for_testing, get_discretisation_for_testing import warnings import sys from platform import system +@unittest.skip("") class TestScipySolver(unittest.TestCase): def test_model_solver_python_and_jax(self): @@ -348,6 +349,96 @@ def test_model_solver_manually_update_initial_conditions(self): ) +class TestScipySolverWithSensitivity(unittest.TestCase): + @unittest.skip("") + def test_solve_sensitivity_scalar_var_scalar_input(self): + # Create model + model = pybamm.BaseModel() + var = pybamm.Variable("var") + p = pybamm.InputParameter("p") + model.rhs = {var: p * var} + model.initial_conditions = {var: 1} + model.variables = {"var squared": var ** 2} + + # Solve + # Make sure that passing in extra options works + solver = pybamm.ScipySolver( + rtol=1e-10, atol=1e-10, solve_sensitivity_equations=True + ) + t_eval = np.linspace(0, 1, 80) + solution = solver.solve(model, t_eval, inputs={"p": 0.1}) + np.testing.assert_array_equal(solution.t, t_eval) + np.testing.assert_allclose(solution.y[0], np.exp(0.1 * solution.t)) + np.testing.assert_allclose( + solution.sensitivity["p"], + (solution.t * np.exp(0.1 * solution.t))[:, np.newaxis], + ) + np.testing.assert_allclose( + solution["var squared"].data, np.exp(0.1 * solution.t) ** 2 + ) + np.testing.assert_allclose( + solution["var squared"].sensitivity["p"], + (2 * np.exp(0.1 * solution.t) * solution.t * np.exp(0.1 * solution.t))[ + :, np.newaxis + ], + ) + + @unittest.skip("") + def test_solve_sensitivity_vector_var_scalar_input(self): + var = pybamm.Variable("var", "negative electrode") + model = pybamm.BaseModel() + param = pybamm.InputParameter("param") + model.rhs = {var: -param * var} + model.initial_conditions = {var: 2} + model.variables = {"var": var} + + # create discretisation + disc = get_discretisation_for_testing() + disc.process_model(model) + n = disc.mesh["negative electrode"].npts + + # Solve - scalar input + solver = pybamm.ScipySolver(solve_sensitivity_equations=True) + t_eval = np.linspace(0, 1) + solution = solver.solve(model, t_eval, inputs={"param": 7}) + np.testing.assert_array_almost_equal( + solution["var"].data, np.tile(2 * np.exp(-7 * t_eval), (n, 1)), decimal=4, + ) + np.testing.assert_array_almost_equal( + solution["var"].sensitivity["param"], + np.repeat(-2 * t_eval * np.exp(-7 * t_eval), n)[:, np.newaxis], + decimal=4, + ) + + def test_solve_sensitivity_scalar_var_vector_input(self): + var = pybamm.Variable("var", "negative electrode") + model = pybamm.BaseModel() + param = pybamm.InputParameter("param", "negative electrode") + model.rhs = {var: -param * var} + model.initial_conditions = {var: 2} + model.variables = {"x-average of var": pybamm.x_average(var)} + + # create discretisation + mesh = get_mesh_for_testing(xpts=5) + spatial_methods = {"macroscale": pybamm.FiniteVolume()} + disc = pybamm.Discretisation(mesh, spatial_methods) + disc.process_model(model) + n = disc.mesh["negative electrode"].npts + + # Solve - scalar input + solver = pybamm.ScipySolver(solve_sensitivity_equations=True) + t_eval = np.linspace(0, 1, 3) + solution = solver.solve(model, t_eval, inputs={"param": 7 * np.ones(n)}) + np.testing.assert_array_almost_equal( + solution["var"].data, np.tile(2 * np.exp(-7 * t_eval), (n, 1)), decimal=4, + ) + np.testing.assert_array_almost_equal( + solution["var"].sensitivity["param"], + np.repeat(-2 * t_eval * np.exp(-7 * t_eval), n)[:, np.newaxis], + decimal=4, + ) + + if __name__ == "__main__": print("Add -v for more debug output") From f97c9529b8443a150e0dff4ab20a5dbc13ca2d4d Mon Sep 17 00:00:00 2001 From: Valentin Sulzer Date: Thu, 9 Jul 2020 15:23:58 -0400 Subject: [PATCH 02/73] #1100 SODEs working with scipy --- pybamm/models/base_model.py | 13 ++ pybamm/solvers/base_solver.py | 7 +- pybamm/solvers/solution.py | 50 +++-- tests/unit/test_models/test_base_model.py | 6 + tests/unit/test_solvers/test_scipy_solver.py | 205 ++++++++++++++++++- 5 files changed, 253 insertions(+), 28 deletions(-) diff --git a/pybamm/models/base_model.py b/pybamm/models/base_model.py index d9d636073c..167bd61144 100644 --- a/pybamm/models/base_model.py +++ b/pybamm/models/base_model.py @@ -286,6 +286,19 @@ def timescale(self, value): "Set the timescale" self._timescale = value + @property + def length_scales(self): + "Length scales of model" + return self._length_scale + + @length_scales.setter + def length_scales(self, values): + "Set the length scale, converting any numbers to pybamm.Scalar" + for domain, scale in values.items(): + if isinstance(scale, numbers.Number): + values[domain] = pybamm.Scalar(scale) + self._length_scale = values + @property def parameters(self): "Returns all the parameters in the model" diff --git a/pybamm/solvers/base_solver.py b/pybamm/solvers/base_solver.py index 6ad10a5d76..f5c6b1bb85 100644 --- a/pybamm/solvers/base_solver.py +++ b/pybamm/solvers/base_solver.py @@ -290,11 +290,14 @@ def report(string): report(f"Creating sensitivity equations for rhs using CasADi") df_dx = casadi.jacobian(func, y_diff) df_dp = casadi.jacobian(func, p_casadi_stacked) + S_x_mat = S_x.reshape( + (model.len_rhs_and_alg, p_casadi_stacked.shape[0]) + ) if model.len_alg == 0: - S_rhs = df_dx @ S_x + df_dp + S_rhs = (df_dx @ S_x_mat + df_dp).reshape((-1, 1)) else: df_dz = casadi.jacobian(func, y_alg) - S_rhs = df_dx @ S_x + df_dz @ S_z + df_dp + S_rhs = df_dx @ S_x_mat + df_dz @ S_z + df_dp func = casadi.vertcat(func, S_rhs) elif name == "initial_conditions": if model.len_rhs == 0 or model.len_alg == 0: diff --git a/pybamm/solvers/solution.py b/pybamm/solvers/solution.py index 33b539a02f..caa84edad4 100644 --- a/pybamm/solvers/solution.py +++ b/pybamm/solvers/solution.py @@ -67,9 +67,11 @@ def __init__( if model is None or model.len_rhs_and_alg == y.shape[0]: self._y = y else: - all_inputs_size = np.vstack(list(inputs.values())).size + n_states = model.len_rhs_and_alg + n_t = len(t) + n_p = np.vstack(list(inputs.values())).size # Get the point where the algebraic equations start - len_rhs_and_sens = all_inputs_size * model.len_rhs + len_rhs_and_sens = (n_p + 1) * model.len_rhs # self._y gets the part of the solution vector that correspond to the # actual ODE/DAE solution self._y = np.vstack( @@ -94,28 +96,35 @@ def __init__( # tn_x1_p0, tn_x1_p1, ..., tn_x1_pn # ... # tn_xn_p0, tn_xn_p1, ..., tn_xn_pn + # 1. Extract the relevant parts of y + # This makes a (n_states * n_p, n_t) matrix full_sens_matrix = np.vstack( [ y[model.len_rhs : len_rhs_and_sens, :], y[len_rhs_and_sens + model.len_alg :, :], ] - ).reshape(np.prod(self._y.shape), all_inputs_size, order="F") + ) + # 2. Transpose into a (n_t, n_states * n_p) matrix + full_sens_matrix = full_sens_matrix.T + # 3. Reshape into a (n_t, n_p, n_states) matrix, + # then tranpose n_p and n_states to get (n_t, n_states, n_p) matrix + full_sens_matrix = full_sens_matrix.reshape(n_t, n_p, n_states).transpose( + 0, 2, 1 + ) + # 3. Stack time and space to get a (n_t * n_states, n_p) matrix + full_sens_matrix = full_sens_matrix.reshape(n_t * n_states, n_p) + + # Save the full sensitivity matrix + sensitivity = {"all": full_sens_matrix} - # also save the sensitivity wrt each parameter - start_rhs = model.len_rhs - start_alg = len_rhs_and_sens + model.len_alg - for i, (name, inp) in enumerate(inputs.items()): - if isinstance(inp, numbers.Number): - input_size = 1 - else: - input_size = inp.shape[0] - end_rhs = start_rhs + model.len_rhs * input_size - end_alg = start_alg + model.len_alg * input_size - sensitivity[name] = np.vstack( - [y[start_rhs:end_rhs, :], y[start_alg:end_alg, :],] - ).reshape(-1, 1) - start_rhs = end_rhs - start_alg = end_alg + # also save the sensitivity wrt each parameter (read the columns of the + # sensitivity matrix) + start = 0 + for i, (name, inp) in enumerate(self.inputs.items()): + input_size = inp.shape[0] + end = start + input_size + sensitivity[name] = full_sens_matrix[:, start:end] + start = end self.sensitivity = sensitivity self._t_event = t_event @@ -182,7 +191,10 @@ def inputs(self, inputs): inp = inp * np.ones((1, len(self.t))) # Tile a vector else: - inp = np.tile(inp, len(self.t)) + if inp.ndim == 1: + inp = np.tile(inp, (len(self.t), 1)).T + else: + inp = np.tile(inp, len(self.t)) self._inputs[name] = inp @property diff --git a/tests/unit/test_models/test_base_model.py b/tests/unit/test_models/test_base_model.py index de41f240f8..9f63ac6792 100644 --- a/tests/unit/test_models/test_base_model.py +++ b/tests/unit/test_models/test_base_model.py @@ -94,6 +94,12 @@ def test_boundary_conditions_set_get(self): with self.assertRaisesRegex(pybamm.ModelError, "boundary condition"): model.boundary_conditions = bad_bcs + def test_length_scales(self): + model = pybamm.BaseModel() + model.length_scales = {"a": 1.3} + self.assertIsInstance(model.length_scales["a"], pybamm.Scalar) + self.assertEqual(model.length_scales["a"].value, 1.3) + def test_variables_set_get(self): model = pybamm.BaseModel() variables = {"c": "alpha", "d": "beta"} diff --git a/tests/unit/test_solvers/test_scipy_solver.py b/tests/unit/test_solvers/test_scipy_solver.py index c4b28d6d6e..a06f79f51f 100644 --- a/tests/unit/test_solvers/test_scipy_solver.py +++ b/tests/unit/test_solvers/test_scipy_solver.py @@ -383,10 +383,81 @@ def test_solve_sensitivity_scalar_var_scalar_input(self): ], ) + # More complicated model + # Create model + model = pybamm.BaseModel() + var = pybamm.Variable("var") + p = pybamm.InputParameter("p") + q = pybamm.InputParameter("q") + r = pybamm.InputParameter("r") + s = pybamm.InputParameter("s") + model.rhs = {var: p * q} + model.initial_conditions = {var: r} + model.variables = {"var times s": var * s} + + # Solve + # Make sure that passing in extra options works + solver = pybamm.ScipySolver( + rtol=1e-10, atol=1e-10, solve_sensitivity_equations=True + ) + t_eval = np.linspace(0, 1, 80) + solution = solver.solve( + model, t_eval, inputs={"p": 0.1, "q": 2, "r": -1, "s": 0.5} + ) + np.testing.assert_allclose(solution.y[0], -1 + 0.2 * solution.t) + np.testing.assert_allclose( + solution.sensitivity["p"], (2 * solution.t)[:, np.newaxis], + ) + np.testing.assert_allclose( + solution.sensitivity["q"], (0.1 * solution.t)[:, np.newaxis], + ) + np.testing.assert_allclose(solution.sensitivity["r"], 1) + np.testing.assert_allclose(solution.sensitivity["s"], 0) + np.testing.assert_allclose( + solution.sensitivity["all"], + np.hstack( + [ + solution.sensitivity["p"], + solution.sensitivity["q"], + solution.sensitivity["r"], + solution.sensitivity["s"], + ] + ), + ) + np.testing.assert_allclose( + solution["var times s"].data, 0.5 * (-1 + 0.2 * solution.t) + ) + np.testing.assert_allclose( + solution["var times s"].sensitivity["p"], + 0.5 * (2 * solution.t)[:, np.newaxis], + ) + np.testing.assert_allclose( + solution["var times s"].sensitivity["q"], + 0.5 * (0.1 * solution.t)[:, np.newaxis], + ) + np.testing.assert_allclose(solution["var times s"].sensitivity["r"], 0.5) + np.testing.assert_allclose( + solution["var times s"].sensitivity["s"], + (-1 + 0.2 * solution.t)[:, np.newaxis], + ) + np.testing.assert_allclose( + solution["var times s"].sensitivity["all"], + np.hstack( + [ + solution["var times s"].sensitivity["p"], + solution["var times s"].sensitivity["q"], + solution["var times s"].sensitivity["r"], + solution["var times s"].sensitivity["s"], + ] + ), + ) + @unittest.skip("") def test_solve_sensitivity_vector_var_scalar_input(self): var = pybamm.Variable("var", "negative electrode") model = pybamm.BaseModel() + # Set length scales to avoid warning + model.length_scales = {"negative electrode": 1} param = pybamm.InputParameter("param") model.rhs = {var: -param * var} model.initial_conditions = {var: 2} @@ -410,32 +481,152 @@ def test_solve_sensitivity_vector_var_scalar_input(self): decimal=4, ) + # More complicated model + # Create model + model = pybamm.BaseModel() + # Set length scales to avoid warning + model.length_scales = {"negative electrode": 1} + var = pybamm.Variable("var", "negative electrode") + p = pybamm.InputParameter("p") + q = pybamm.InputParameter("q") + r = pybamm.InputParameter("r") + s = pybamm.InputParameter("s") + model.rhs = {var: p * q} + model.initial_conditions = {var: r} + model.variables = {"var times s": var * s} + + # Discretise + disc.process_model(model) + + # Solve + # Make sure that passing in extra options works + solver = pybamm.ScipySolver( + rtol=1e-10, atol=1e-10, solve_sensitivity_equations=True + ) + t_eval = np.linspace(0, 1, 80) + solution = solver.solve( + model, t_eval, inputs={"p": 0.1, "q": 2, "r": -1, "s": 0.5} + ) + np.testing.assert_allclose(solution.y, np.tile(-1 + 0.2 * solution.t, (n, 1))) + np.testing.assert_allclose( + solution.sensitivity["p"], np.repeat(2 * solution.t, n)[:, np.newaxis], + ) + np.testing.assert_allclose( + solution.sensitivity["q"], np.repeat(0.1 * solution.t, n)[:, np.newaxis], + ) + np.testing.assert_allclose(solution.sensitivity["r"], 1) + np.testing.assert_allclose(solution.sensitivity["s"], 0) + np.testing.assert_allclose( + solution.sensitivity["all"], + np.hstack( + [ + solution.sensitivity["p"], + solution.sensitivity["q"], + solution.sensitivity["r"], + solution.sensitivity["s"], + ] + ), + ) + np.testing.assert_allclose( + solution["var times s"].data, np.tile(0.5 * (-1 + 0.2 * solution.t), (n, 1)) + ) + np.testing.assert_allclose( + solution["var times s"].sensitivity["p"], + np.repeat(0.5 * (2 * solution.t), n)[:, np.newaxis], + ) + np.testing.assert_allclose( + solution["var times s"].sensitivity["q"], + np.repeat(0.5 * (0.1 * solution.t), n)[:, np.newaxis], + ) + np.testing.assert_allclose(solution["var times s"].sensitivity["r"], 0.5) + np.testing.assert_allclose( + solution["var times s"].sensitivity["s"], + np.repeat(-1 + 0.2 * solution.t, n)[:, np.newaxis], + ) + np.testing.assert_allclose( + solution["var times s"].sensitivity["all"], + np.hstack( + [ + solution["var times s"].sensitivity["p"], + solution["var times s"].sensitivity["q"], + solution["var times s"].sensitivity["r"], + solution["var times s"].sensitivity["s"], + ] + ), + ) + def test_solve_sensitivity_scalar_var_vector_input(self): var = pybamm.Variable("var", "negative electrode") model = pybamm.BaseModel() + # Set length scales to avoid warning + model.length_scales = {"negative electrode": 1} + param = pybamm.InputParameter("param", "negative electrode") model.rhs = {var: -param * var} model.initial_conditions = {var: 2} - model.variables = {"x-average of var": pybamm.x_average(var)} + model.variables = { + "var": var, + "integral of var": pybamm.Integral(var, pybamm.standard_spatial_vars.x_n), + } # create discretisation - mesh = get_mesh_for_testing(xpts=5) + mesh = get_mesh_for_testing() spatial_methods = {"macroscale": pybamm.FiniteVolume()} disc = pybamm.Discretisation(mesh, spatial_methods) disc.process_model(model) n = disc.mesh["negative electrode"].npts - # Solve - scalar input - solver = pybamm.ScipySolver(solve_sensitivity_equations=True) - t_eval = np.linspace(0, 1, 3) + # Solve - constant input + solver = pybamm.ScipySolver( + rtol=1e-10, atol=1e-10, solve_sensitivity_equations=True + ) + t_eval = np.linspace(0, 1) solution = solver.solve(model, t_eval, inputs={"param": 7 * np.ones(n)}) + l_n = mesh["negative electrode"].edges[-1] np.testing.assert_array_almost_equal( solution["var"].data, np.tile(2 * np.exp(-7 * t_eval), (n, 1)), decimal=4, ) + np.testing.assert_array_almost_equal( solution["var"].sensitivity["param"], - np.repeat(-2 * t_eval * np.exp(-7 * t_eval), n)[:, np.newaxis], - decimal=4, + np.vstack([np.eye(n) * -2 * t * np.exp(-7 * t) for t in t_eval]), + ) + np.testing.assert_array_almost_equal( + solution["integral of var"].data, 2 * np.exp(-7 * t_eval) * l_n, decimal=4, + ) + np.testing.assert_array_almost_equal( + solution["integral of var"].sensitivity["param"], + np.tile(-2 * t_eval * np.exp(-7 * t_eval) * l_n / 40, (40, 1)).T, + ) + + # Solve - linspace input + solver = pybamm.ScipySolver( + rtol=1e-10, atol=1e-10, solve_sensitivity_equations=True + ) + t_eval = np.linspace(0, 1) + p_eval = np.linspace(1, 2, n) + solution = solver.solve(model, t_eval, inputs={"param": p_eval}) + l_n = mesh["negative electrode"].edges[-1] + np.testing.assert_array_almost_equal( + solution["var"].data, 2 * np.exp(-p_eval[:, np.newaxis] * t_eval), decimal=4 + ) + np.testing.assert_array_almost_equal( + solution["var"].sensitivity["param"], + np.vstack([np.diag(-2 * t * np.exp(-p_eval * t)) for t in t_eval]), + ) + + np.testing.assert_array_almost_equal( + solution["integral of var"].data, + np.sum( + 2 + * np.exp(-p_eval[:, np.newaxis] * t_eval) + * mesh["negative electrode"].d_edges[:, np.newaxis], + axis=0, + ), + ) + np.testing.assert_array_almost_equal( + solution["integral of var"].sensitivity["param"], + np.vstack([-2 * t * np.exp(-p_eval * t) * l_n / 40 for t in t_eval]), ) From 54d45f94944d24293b814013da08655f442c09fc Mon Sep 17 00:00:00 2001 From: Valentin Sulzer Date: Thu, 9 Jul 2020 16:38:18 -0400 Subject: [PATCH 03/73] #1100 get SODEs working for casadi solver --- pybamm/solvers/base_solver.py | 20 +- pybamm/solvers/casadi_solver.py | 59 +++- pybamm/solvers/scipy_solver.py | 1 + pybamm/solvers/solution.py | 3 +- tests/unit/test_solvers/test_casadi_solver.py | 275 ++++++++++++++++++ tests/unit/test_solvers/test_scipy_solver.py | 7 +- 6 files changed, 336 insertions(+), 29 deletions(-) diff --git a/pybamm/solvers/base_solver.py b/pybamm/solvers/base_solver.py index f5c6b1bb85..29180e454e 100644 --- a/pybamm/solvers/base_solver.py +++ b/pybamm/solvers/base_solver.py @@ -8,6 +8,7 @@ import numpy as np import sys import itertools +from scipy.linalg import block_diag class BaseSolver(object): @@ -426,12 +427,21 @@ def report(string): ): # can use DAE solver to solve model with algebraic equations only if len(model.rhs) > 0: - mass_matrix_inv = casadi.MX(model.mass_matrix_inv.entries) + if self.solve_sensitivity_equations is True: + # Copy mass matrix blocks diagonally + single_mass_matrix_inv = model.mass_matrix_inv.entries.toarray() + n_inputs = p_casadi_stacked.shape[0] + block_mass_matrix = block_diag( + *[single_mass_matrix_inv] * (n_inputs + 1) + ) + mass_matrix_inv = casadi.MX(block_mass_matrix) + else: + mass_matrix_inv = casadi.MX(model.mass_matrix_inv.entries) explicit_rhs = mass_matrix_inv @ rhs( - t_casadi, y_casadi, p_casadi_stacked + t_casadi, y_and_S, p_casadi_stacked ) model.casadi_rhs = casadi.Function( - "rhs", [t_casadi, y_casadi, p_casadi_stacked], [explicit_rhs] + "rhs", [t_casadi, y_and_S, p_casadi_stacked], [explicit_rhs] ) model.casadi_algebraic = algebraic if len(model.rhs) == 0: @@ -703,10 +713,6 @@ def solve(self, model, t_eval=None, external_variables=None, inputs=None): solution.set_up_time = set_up_time solution.solve_time = timer.time() - # Add model and inputs to solution - solution.model = model - solution.inputs = ext_and_inputs - # Identify the event that caused termination termination = self.get_termination_reason(solution, model.events) diff --git a/pybamm/solvers/casadi_solver.py b/pybamm/solvers/casadi_solver.py index fc2a248eb7..243bcbddd9 100644 --- a/pybamm/solvers/casadi_solver.py +++ b/pybamm/solvers/casadi_solver.py @@ -55,6 +55,9 @@ class CasadiSolver(pybamm.BaseSolver): Any options to pass to the CasADi integrator when calling the integrator. Please consult `CasADi documentation `_ for details. + solve_sensitivity_equations : bool, optional + Whether to explicitly formulate and solve the forward sensitivity equations. + See :class:`pybamm.BaseSolver` """ @@ -69,8 +72,16 @@ def __init__( dt_max=None, extra_options_setup=None, extra_options_call=None, + solve_sensitivity_equations=False, ): - super().__init__("problem dependent", rtol, atol, root_method, root_tol) + super().__init__( + "problem dependent", + rtol, + atol, + root_method, + root_tol, + solve_sensitivity_equations=solve_sensitivity_equations, + ) if mode in ["safe", "fast"]: self.mode = mode else: @@ -106,16 +117,18 @@ def _integrate(self, model, t_eval, inputs=None): Any external variables or input parameters to pass to the model when solving """ # Record whether there are any symbolic inputs - inputs = inputs or {} - has_symbolic_inputs = any(isinstance(v, casadi.MX) for v in inputs.values()) + inputs_dict = inputs or {} + has_symbolic_inputs = any( + isinstance(v, casadi.MX) for v in inputs_dict.values() + ) # convert inputs to casadi format - inputs = casadi.vertcat(*[x for x in inputs.values()]) + inputs = casadi.vertcat(*[x for x in inputs_dict.values()]) if has_symbolic_inputs: # Create integrax`tor without grid to avoid having to create several times self.get_integrator(model, inputs) - solution = self._run_integrator(model, model.y0, inputs, t_eval) + solution = self._run_integrator(model, model.y0, inputs_dict, t_eval) solution.termination = "final time" return solution elif self.mode == "fast" or not model.events: @@ -123,7 +136,7 @@ def _integrate(self, model, t_eval, inputs=None): pybamm.logger.info("No events found, running fast mode") # Create an integrator with the grid (we just need to do this once) self.get_integrator(model, inputs, t_eval) - solution = self._run_integrator(model, model.y0, inputs, t_eval) + solution = self._run_integrator(model, model.y0, inputs_dict, t_eval) solution.termination = "final time" return solution elif self.mode == "safe": @@ -143,7 +156,9 @@ def _integrate(self, model, t_eval, inputs=None): pybamm.logger.info("Start solving {} with {}".format(model.name, self.name)) # Initialize solution - solution = pybamm.Solution(np.array([t]), y0[:, np.newaxis]) + solution = pybamm.Solution( + np.array([t]), y0[:, np.newaxis], model=model, inputs=inputs_dict + ) solution.solve_time = 0 # Try to integrate in global steps of size dt_max. Note: dt_max must @@ -178,7 +193,7 @@ def _integrate(self, model, t_eval, inputs=None): # halve the step size and try again. try: current_step_sol = self._run_integrator( - model, y0, inputs, t_window + model, y0, inputs_dict, t_window ) solved = True except pybamm.SolverError: @@ -257,7 +272,9 @@ def event_fun(t): t_window = np.array([t, t_event]) # integrator = self.get_integrator(model, t_window, inputs) - current_step_sol = self._run_integrator(model, y0, inputs, t_window) + current_step_sol = self._run_integrator( + model, y0, inputs_dict, t_window + ) # assign temporary solve time current_step_sol.solve_time = np.nan @@ -361,10 +378,18 @@ def get_integrator(self, model, inputs, t_eval=None): self.integrators[model] = (integrator, use_grid) return integrator - def _run_integrator(self, model, y0, inputs, t_eval): + def _run_integrator(self, model, y0, inputs_dict, t_eval): + inputs = casadi.vertcat(*[x for x in inputs_dict.values()]) integrator, use_grid = self.integrators[model] - y0_diff = y0[: model.len_rhs] - y0_alg = y0[model.len_rhs :] + # Split up initial conditions into differential and algebraic + # Check y0 to see if it includes sensitivities + if model.len_rhs_and_alg == y0.shape[0]: + len_rhs = model.len_rhs + else: + len_rhs = model.len_rhs * (inputs.shape[0] + 1) + y0_diff = y0[:len_rhs] + y0_alg = y0[len_rhs:] + # Solve try: # Try solving if use_grid is True: @@ -379,7 +404,7 @@ def _run_integrator(self, model, y0, inputs, t_eval): **self.extra_options_call ) y_sol = np.concatenate([sol["xf"].full(), sol["zf"].full()]) - return pybamm.Solution(t_eval, y_sol) + return pybamm.Solution(t_eval, y_sol, model=model, inputs=inputs_dict) else: # Repeated calls to the integrator x = y0_diff @@ -399,10 +424,14 @@ def _run_integrator(self, model, y0, inputs, t_eval): if not z.is_empty(): y_alg = casadi.horzcat(y_alg, z) if z.is_empty(): - return pybamm.Solution(t_eval, y_diff) + return pybamm.Solution( + t_eval, y_diff, model=model, inputs=inputs_dict + ) else: y_sol = casadi.vertcat(y_diff, y_alg) - return pybamm.Solution(t_eval, y_sol) + return pybamm.Solution( + t_eval, y_sol, model=model, inputs=inputs_dict + ) except RuntimeError as e: # If it doesn't work raise error raise pybamm.SolverError(e.args[0]) diff --git a/pybamm/solvers/scipy_solver.py b/pybamm/solvers/scipy_solver.py index d8abcb90fe..00ff8d5768 100644 --- a/pybamm/solvers/scipy_solver.py +++ b/pybamm/solvers/scipy_solver.py @@ -24,6 +24,7 @@ class ScipySolver(pybamm.BaseSolver): Please consult `SciPy documentation `_ for details. solve_sensitivity_equations : bool, optional + Whether to explicitly formulate and solve the forward sensitivity equations. See :class:`pybamm.BaseSolver` """ diff --git a/pybamm/solvers/solution.py b/pybamm/solvers/solution.py index caa84edad4..0923d8c3cb 100644 --- a/pybamm/solvers/solution.py +++ b/pybamm/solvers/solution.py @@ -66,6 +66,7 @@ def __init__( # y only has the shape or the rhs and alg solution) if model is None or model.len_rhs_and_alg == y.shape[0]: self._y = y + self.sensitivity = {} else: n_states = model.len_rhs_and_alg n_t = len(t) @@ -133,11 +134,9 @@ def __init__( if copy_this is None: self.set_up_time = None self.solve_time = None - self.has_symbolic_inputs = False else: self.set_up_time = copy_this.set_up_time self.solve_time = copy_this.solve_time - self.has_symbolic_inputs = copy_this.has_symbolic_inputs # initiaize empty variables and data self._variables = pybamm.FuzzyDict() diff --git a/tests/unit/test_solvers/test_casadi_solver.py b/tests/unit/test_solvers/test_casadi_solver.py index 6e957594e0..169446bd44 100644 --- a/tests/unit/test_solvers/test_casadi_solver.py +++ b/tests/unit/test_solvers/test_casadi_solver.py @@ -602,6 +602,281 @@ def objective(x): np.testing.assert_array_almost_equal(lsq_sol.x, [3, 3], decimal=3) +class TestCasadiSolverWithForwardSensitivityEquations(unittest.TestCase): + def test_solve_sensitivity_scalar_var_scalar_input(self): + # Create model + model = pybamm.BaseModel() + var = pybamm.Variable("var") + p = pybamm.InputParameter("p") + model.rhs = {var: p * var} + model.initial_conditions = {var: 1} + model.variables = {"var squared": var ** 2} + + # Solve + # Make sure that passing in extra options works + solver = pybamm.CasadiSolver( + mode="fast", rtol=1e-10, atol=1e-10, solve_sensitivity_equations=True + ) + t_eval = np.linspace(0, 1, 80) + solution = solver.solve(model, t_eval, inputs={"p": 0.1}) + np.testing.assert_array_equal(solution.t, t_eval) + np.testing.assert_allclose(solution.y[0], np.exp(0.1 * solution.t)) + np.testing.assert_allclose( + solution.sensitivity["p"], + (solution.t * np.exp(0.1 * solution.t))[:, np.newaxis], + ) + np.testing.assert_allclose( + solution["var squared"].data, np.exp(0.1 * solution.t) ** 2 + ) + np.testing.assert_allclose( + solution["var squared"].sensitivity["p"], + (2 * np.exp(0.1 * solution.t) * solution.t * np.exp(0.1 * solution.t))[ + :, np.newaxis + ], + ) + + # More complicated model + # Create model + model = pybamm.BaseModel() + var = pybamm.Variable("var") + p = pybamm.InputParameter("p") + q = pybamm.InputParameter("q") + r = pybamm.InputParameter("r") + s = pybamm.InputParameter("s") + model.rhs = {var: p * q} + model.initial_conditions = {var: r} + model.variables = {"var times s": var * s} + + # Solve + # Make sure that passing in extra options works + solver = pybamm.CasadiSolver( + rtol=1e-10, atol=1e-10, solve_sensitivity_equations=True + ) + t_eval = np.linspace(0, 1, 80) + solution = solver.solve( + model, t_eval, inputs={"p": 0.1, "q": 2, "r": -1, "s": 0.5} + ) + np.testing.assert_allclose(solution.y[0], -1 + 0.2 * solution.t) + np.testing.assert_allclose( + solution.sensitivity["p"], (2 * solution.t)[:, np.newaxis], + ) + np.testing.assert_allclose( + solution.sensitivity["q"], (0.1 * solution.t)[:, np.newaxis], + ) + np.testing.assert_allclose(solution.sensitivity["r"], 1) + np.testing.assert_allclose(solution.sensitivity["s"], 0) + np.testing.assert_allclose( + solution.sensitivity["all"], + np.hstack( + [ + solution.sensitivity["p"], + solution.sensitivity["q"], + solution.sensitivity["r"], + solution.sensitivity["s"], + ] + ), + ) + np.testing.assert_allclose( + solution["var times s"].data, 0.5 * (-1 + 0.2 * solution.t) + ) + np.testing.assert_allclose( + solution["var times s"].sensitivity["p"], + 0.5 * (2 * solution.t)[:, np.newaxis], + ) + np.testing.assert_allclose( + solution["var times s"].sensitivity["q"], + 0.5 * (0.1 * solution.t)[:, np.newaxis], + ) + np.testing.assert_allclose(solution["var times s"].sensitivity["r"], 0.5) + np.testing.assert_allclose( + solution["var times s"].sensitivity["s"], + (-1 + 0.2 * solution.t)[:, np.newaxis], + ) + np.testing.assert_allclose( + solution["var times s"].sensitivity["all"], + np.hstack( + [ + solution["var times s"].sensitivity["p"], + solution["var times s"].sensitivity["q"], + solution["var times s"].sensitivity["r"], + solution["var times s"].sensitivity["s"], + ] + ), + ) + + def test_solve_sensitivity_vector_var_scalar_input(self): + var = pybamm.Variable("var", "negative electrode") + model = pybamm.BaseModel() + # Set length scales to avoid warning + model.length_scales = {"negative electrode": 1} + param = pybamm.InputParameter("param") + model.rhs = {var: -param * var} + model.initial_conditions = {var: 2} + model.variables = {"var": var} + + # create discretisation + disc = get_discretisation_for_testing() + disc.process_model(model) + n = disc.mesh["negative electrode"].npts + + # Solve - scalar input + solver = pybamm.CasadiSolver(solve_sensitivity_equations=True) + t_eval = np.linspace(0, 1) + solution = solver.solve(model, t_eval, inputs={"param": 7}) + np.testing.assert_array_almost_equal( + solution["var"].data, np.tile(2 * np.exp(-7 * t_eval), (n, 1)), decimal=4, + ) + np.testing.assert_array_almost_equal( + solution["var"].sensitivity["param"], + np.repeat(-2 * t_eval * np.exp(-7 * t_eval), n)[:, np.newaxis], + decimal=4, + ) + + # More complicated model + # Create model + model = pybamm.BaseModel() + # Set length scales to avoid warning + model.length_scales = {"negative electrode": 1} + var = pybamm.Variable("var", "negative electrode") + p = pybamm.InputParameter("p") + q = pybamm.InputParameter("q") + r = pybamm.InputParameter("r") + s = pybamm.InputParameter("s") + model.rhs = {var: p * q} + model.initial_conditions = {var: r} + model.variables = {"var times s": var * s} + + # Discretise + disc.process_model(model) + + # Solve + # Make sure that passing in extra options works + solver = pybamm.CasadiSolver( + rtol=1e-10, atol=1e-10, solve_sensitivity_equations=True + ) + t_eval = np.linspace(0, 1, 80) + solution = solver.solve( + model, t_eval, inputs={"p": 0.1, "q": 2, "r": -1, "s": 0.5} + ) + np.testing.assert_allclose(solution.y, np.tile(-1 + 0.2 * solution.t, (n, 1))) + np.testing.assert_allclose( + solution.sensitivity["p"], np.repeat(2 * solution.t, n)[:, np.newaxis], + ) + np.testing.assert_allclose( + solution.sensitivity["q"], np.repeat(0.1 * solution.t, n)[:, np.newaxis], + ) + np.testing.assert_allclose(solution.sensitivity["r"], 1) + np.testing.assert_allclose(solution.sensitivity["s"], 0) + np.testing.assert_allclose( + solution.sensitivity["all"], + np.hstack( + [ + solution.sensitivity["p"], + solution.sensitivity["q"], + solution.sensitivity["r"], + solution.sensitivity["s"], + ] + ), + ) + np.testing.assert_allclose( + solution["var times s"].data, np.tile(0.5 * (-1 + 0.2 * solution.t), (n, 1)) + ) + np.testing.assert_allclose( + solution["var times s"].sensitivity["p"], + np.repeat(0.5 * (2 * solution.t), n)[:, np.newaxis], + ) + np.testing.assert_allclose( + solution["var times s"].sensitivity["q"], + np.repeat(0.5 * (0.1 * solution.t), n)[:, np.newaxis], + ) + np.testing.assert_allclose(solution["var times s"].sensitivity["r"], 0.5) + np.testing.assert_allclose( + solution["var times s"].sensitivity["s"], + np.repeat(-1 + 0.2 * solution.t, n)[:, np.newaxis], + ) + np.testing.assert_allclose( + solution["var times s"].sensitivity["all"], + np.hstack( + [ + solution["var times s"].sensitivity["p"], + solution["var times s"].sensitivity["q"], + solution["var times s"].sensitivity["r"], + solution["var times s"].sensitivity["s"], + ] + ), + ) + + def test_solve_sensitivity_scalar_var_vector_input(self): + var = pybamm.Variable("var", "negative electrode") + model = pybamm.BaseModel() + # Set length scales to avoid warning + model.length_scales = {"negative electrode": 1} + + param = pybamm.InputParameter("param", "negative electrode") + model.rhs = {var: -param * var} + model.initial_conditions = {var: 2} + model.variables = { + "var": var, + "integral of var": pybamm.Integral(var, pybamm.standard_spatial_vars.x_n), + } + + # create discretisation + mesh = get_mesh_for_testing(xpts=5) + spatial_methods = {"macroscale": pybamm.FiniteVolume()} + disc = pybamm.Discretisation(mesh, spatial_methods) + disc.process_model(model) + n = disc.mesh["negative electrode"].npts + + # Solve - constant input + solver = pybamm.CasadiSolver( + mode="fast", rtol=1e-10, atol=1e-10, solve_sensitivity_equations=True + ) + t_eval = np.linspace(0, 1) + solution = solver.solve(model, t_eval, inputs={"param": 7 * np.ones(n)}) + l_n = mesh["negative electrode"].edges[-1] + np.testing.assert_array_almost_equal( + solution["var"].data, np.tile(2 * np.exp(-7 * t_eval), (n, 1)), decimal=4, + ) + + np.testing.assert_array_almost_equal( + solution["var"].sensitivity["param"], + np.vstack([np.eye(n) * -2 * t * np.exp(-7 * t) for t in t_eval]), + ) + np.testing.assert_array_almost_equal( + solution["integral of var"].data, 2 * np.exp(-7 * t_eval) * l_n, decimal=4, + ) + np.testing.assert_array_almost_equal( + solution["integral of var"].sensitivity["param"], + np.tile(-2 * t_eval * np.exp(-7 * t_eval) * l_n / n, (n, 1)).T, + ) + + # Solve - linspace input + p_eval = np.linspace(1, 2, n) + solution = solver.solve(model, t_eval, inputs={"param": p_eval}) + l_n = mesh["negative electrode"].edges[-1] + np.testing.assert_array_almost_equal( + solution["var"].data, 2 * np.exp(-p_eval[:, np.newaxis] * t_eval), decimal=4 + ) + np.testing.assert_array_almost_equal( + solution["var"].sensitivity["param"], + np.vstack([np.diag(-2 * t * np.exp(-p_eval * t)) for t in t_eval]), + ) + + np.testing.assert_array_almost_equal( + solution["integral of var"].data, + np.sum( + 2 + * np.exp(-p_eval[:, np.newaxis] * t_eval) + * mesh["negative electrode"].d_edges[:, np.newaxis], + axis=0, + ), + ) + np.testing.assert_array_almost_equal( + solution["integral of var"].sensitivity["param"], + np.vstack([-2 * t * np.exp(-p_eval * t) * l_n / n for t in t_eval]), + ) + + if __name__ == "__main__": print("Add -v for more debug output") import sys diff --git a/tests/unit/test_solvers/test_scipy_solver.py b/tests/unit/test_solvers/test_scipy_solver.py index a06f79f51f..7c1d7f4e0c 100644 --- a/tests/unit/test_solvers/test_scipy_solver.py +++ b/tests/unit/test_solvers/test_scipy_solver.py @@ -10,7 +10,6 @@ from platform import system -@unittest.skip("") class TestScipySolver(unittest.TestCase): def test_model_solver_python_and_jax(self): @@ -350,7 +349,6 @@ def test_model_solver_manually_update_initial_conditions(self): class TestScipySolverWithSensitivity(unittest.TestCase): - @unittest.skip("") def test_solve_sensitivity_scalar_var_scalar_input(self): # Create model model = pybamm.BaseModel() @@ -452,7 +450,6 @@ def test_solve_sensitivity_scalar_var_scalar_input(self): ), ) - @unittest.skip("") def test_solve_sensitivity_vector_var_scalar_input(self): var = pybamm.Variable("var", "negative electrode") model = pybamm.BaseModel() @@ -596,7 +593,7 @@ def test_solve_sensitivity_scalar_var_vector_input(self): ) np.testing.assert_array_almost_equal( solution["integral of var"].sensitivity["param"], - np.tile(-2 * t_eval * np.exp(-7 * t_eval) * l_n / 40, (40, 1)).T, + np.tile(-2 * t_eval * np.exp(-7 * t_eval) * l_n / n, (n, 1)).T, ) # Solve - linspace input @@ -626,7 +623,7 @@ def test_solve_sensitivity_scalar_var_vector_input(self): ) np.testing.assert_array_almost_equal( solution["integral of var"].sensitivity["param"], - np.vstack([-2 * t * np.exp(-p_eval * t) * l_n / 40 for t in t_eval]), + np.vstack([-2 * t * np.exp(-p_eval * t) * l_n / n for t in t_eval]), ) From d438609a77e3ed015a780cd82e238545a93be409 Mon Sep 17 00:00:00 2001 From: Valentin Sulzer Date: Thu, 9 Jul 2020 16:43:07 -0400 Subject: [PATCH 04/73] #1100 flake8 --- pybamm/solvers/base_solver.py | 2 +- pybamm/solvers/processed_symbolic_variable.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/pybamm/solvers/base_solver.py b/pybamm/solvers/base_solver.py index 29180e454e..dd60e9dd79 100644 --- a/pybamm/solvers/base_solver.py +++ b/pybamm/solvers/base_solver.py @@ -288,7 +288,7 @@ def report(string): # Add sensitivity vectors to the rhs and algebraic equations if self.solve_sensitivity_equations is True: if name == "rhs": - report(f"Creating sensitivity equations for rhs using CasADi") + report("Creating sensitivity equations for rhs using CasADi") df_dx = casadi.jacobian(func, y_diff) df_dp = casadi.jacobian(func, p_casadi_stacked) S_x_mat = S_x.reshape( diff --git a/pybamm/solvers/processed_symbolic_variable.py b/pybamm/solvers/processed_symbolic_variable.py index f6461d3a25..ccc071c902 100644 --- a/pybamm/solvers/processed_symbolic_variable.py +++ b/pybamm/solvers/processed_symbolic_variable.py @@ -3,7 +3,6 @@ # import casadi import numbers -import numpy as np class ProcessedSymbolicVariable(object): From 4c6bf592e84955db40dd7547a44a3b680089f12d Mon Sep 17 00:00:00 2001 From: Valentin Sulzer Date: Thu, 9 Jul 2020 22:51:13 -0400 Subject: [PATCH 05/73] #1100 starting to get SDAEs working with casadi --- pybamm/__init__.py | 1 + pybamm/solvers/base_solver.py | 39 +- pybamm/solvers/casadi_algebraic_solver.py | 6 +- pybamm/solvers/solution.py | 29 +- tests/unit/test_solvers/test_casadi_solver.py | 1872 ++++++++++------- 5 files changed, 1132 insertions(+), 815 deletions(-) diff --git a/pybamm/__init__.py b/pybamm/__init__.py index f95fc07e90..e8c1721e6e 100644 --- a/pybamm/__init__.py +++ b/pybamm/__init__.py @@ -108,6 +108,7 @@ def version(formatted=False): to_python, EvaluatorPython, ) + if system() != "Windows": from .expression_tree.operations.evaluate import EvaluatorJax diff --git a/pybamm/solvers/base_solver.py b/pybamm/solvers/base_solver.py index dd60e9dd79..eea850baf4 100644 --- a/pybamm/solvers/base_solver.py +++ b/pybamm/solvers/base_solver.py @@ -287,19 +287,44 @@ def report(string): func = func.to_casadi(t_casadi, y_casadi, inputs=p_casadi) # Add sensitivity vectors to the rhs and algebraic equations if self.solve_sensitivity_equations is True: - if name == "rhs": + if name == "rhs" and model.len_rhs > 0: report("Creating sensitivity equations for rhs using CasADi") df_dx = casadi.jacobian(func, y_diff) df_dp = casadi.jacobian(func, p_casadi_stacked) S_x_mat = S_x.reshape( - (model.len_rhs_and_alg, p_casadi_stacked.shape[0]) + (model.len_rhs, p_casadi_stacked.shape[0]) ) if model.len_alg == 0: S_rhs = (df_dx @ S_x_mat + df_dp).reshape((-1, 1)) else: df_dz = casadi.jacobian(func, y_alg) - S_rhs = df_dx @ S_x_mat + df_dz @ S_z + df_dp + S_z_mat = S_z.reshape( + (model.len_rhs, p_casadi_stacked.shape[0]) + ) + S_rhs = (df_dx @ S_x_mat + df_dz @ S_z_mat + df_dp).reshape( + (-1, 1) + ) func = casadi.vertcat(func, S_rhs) + if name == "algebraic" and model.len_alg > 0: + report( + "Creating sensitivity equations for algebraic using CasADi" + ) + dg_dz = casadi.jacobian(func, y_alg) + dg_dp = casadi.jacobian(func, p_casadi_stacked) + S_z_mat = S_z.reshape( + (model.len_rhs, p_casadi_stacked.shape[0]) + ) + if model.len_rhs == 0: + S_alg = (dg_dz @ S_z_mat + dg_dp).reshape((-1, 1)) + else: + dg_dx = casadi.jacobian(func, y_diff) + S_x_mat = S_x.reshape( + (model.len_rhs, p_casadi_stacked.shape[0]) + ) + S_alg = (dg_dx @ S_x_mat + dg_dz @ S_z_mat + dg_dp).reshape( + (-1, 1) + ) + func = casadi.vertcat(func, S_alg) elif name == "initial_conditions": if model.len_rhs == 0 or model.len_alg == 0: S_0 = casadi.jacobian(func, p_casadi_stacked).reshape( @@ -309,8 +334,12 @@ def report(string): else: x0 = func[: model.len_rhs] z0 = func[model.len_rhs :] - Sx_0 = casadi.jacobian(x0, p_casadi_stacked) - Sz_0 = casadi.jacobian(z0, p_casadi_stacked) + Sx_0 = casadi.jacobian(x0, p_casadi_stacked).reshape( + (-1, 1) + ) + Sz_0 = casadi.jacobian(z0, p_casadi_stacked).reshape( + (-1, 1) + ) func = casadi.vertcat(x0, Sx_0, z0, Sz_0) if use_jacobian: report(f"Calculating jacobian for {name} using CasADi") diff --git a/pybamm/solvers/casadi_algebraic_solver.py b/pybamm/solvers/casadi_algebraic_solver.py index 25fe6aff1d..798793858c 100644 --- a/pybamm/solvers/casadi_algebraic_solver.py +++ b/pybamm/solvers/casadi_algebraic_solver.py @@ -75,7 +75,11 @@ def _integrate(self, model, t_eval, inputs=None): y0_diff = casadi.DM() y0_alg = y0 else: - len_rhs = model.concatenated_rhs.size + # Check y0 to see if it includes sensitivities + if model.len_rhs_and_alg == y0.shape[0]: + len_rhs = model.len_rhs + else: + len_rhs = model.len_rhs * (inputs.shape[0] + 1) y0_diff = y0[:len_rhs] y0_alg = y0[len_rhs:] diff --git a/pybamm/solvers/solution.py b/pybamm/solvers/solution.py index 0923d8c3cb..49e9817385 100644 --- a/pybamm/solvers/solution.py +++ b/pybamm/solvers/solution.py @@ -69,6 +69,8 @@ def __init__( self.sensitivity = {} else: n_states = model.len_rhs_and_alg + n_rhs = model.len_rhs + n_alg = model.len_alg n_t = len(t) n_p = np.vstack(list(inputs.values())).size # Get the point where the algebraic equations start @@ -97,26 +99,19 @@ def __init__( # tn_x1_p0, tn_x1_p1, ..., tn_x1_pn # ... # tn_xn_p0, tn_xn_p1, ..., tn_xn_pn - # 1. Extract the relevant parts of y - # This makes a (n_states * n_p, n_t) matrix - full_sens_matrix = np.vstack( - [ - y[model.len_rhs : len_rhs_and_sens, :], - y[len_rhs_and_sens + model.len_alg :, :], - ] + # 1, Extract rhs and alg sensitivities and reshape into 3D matrices + # with shape (n_p, n_states, n_t) + ode_sens = y[n_rhs:len_rhs_and_sens, :].reshape(n_p, n_rhs, n_t) + alg_sens = y[len_rhs_and_sens + n_alg :, :].reshape(n_p, n_alg, n_t) + # 2. Concatenate into a single 3D matrix with shape (n_p, n_states, n_t) + # i.e. along first axis + full_sens_matrix = np.concatenate([ode_sens, alg_sens], axis=1) + # Transpose and reshape into a (n_states * n_t, n_p) matrix + full_sens_matrix = full_sens_matrix.transpose(2, 1, 0).reshape( + n_t * n_states, n_p ) - # 2. Transpose into a (n_t, n_states * n_p) matrix - full_sens_matrix = full_sens_matrix.T - # 3. Reshape into a (n_t, n_p, n_states) matrix, - # then tranpose n_p and n_states to get (n_t, n_states, n_p) matrix - full_sens_matrix = full_sens_matrix.reshape(n_t, n_p, n_states).transpose( - 0, 2, 1 - ) - # 3. Stack time and space to get a (n_t * n_states, n_p) matrix - full_sens_matrix = full_sens_matrix.reshape(n_t * n_states, n_p) # Save the full sensitivity matrix - sensitivity = {"all": full_sens_matrix} # also save the sensitivity wrt each parameter (read the columns of the # sensitivity matrix) diff --git a/tests/unit/test_solvers/test_casadi_solver.py b/tests/unit/test_solvers/test_casadi_solver.py index 169446bd44..2185ca108b 100644 --- a/tests/unit/test_solvers/test_casadi_solver.py +++ b/tests/unit/test_solvers/test_casadi_solver.py @@ -9,608 +9,883 @@ from scipy.optimize import least_squares -class TestCasadiSolver(unittest.TestCase): - def test_bad_mode(self): - with self.assertRaisesRegex(ValueError, "invalid mode"): - pybamm.CasadiSolver(mode="bad mode") - - def test_model_solver(self): - # Create model - model = pybamm.BaseModel() - var = pybamm.Variable("var") - model.rhs = {var: 0.1 * var} - model.initial_conditions = {var: 1} - # No need to set parameters; can use base discretisation (no spatial operators) - - # create discretisation - disc = pybamm.Discretisation() - model_disc = disc.process_model(model, inplace=False) - # Solve - solver = pybamm.CasadiSolver(mode="fast", rtol=1e-8, atol=1e-8) - t_eval = np.linspace(0, 1, 100) - solution = solver.solve(model_disc, t_eval) - np.testing.assert_array_equal(solution.t, t_eval) - np.testing.assert_array_almost_equal( - solution.y[0], np.exp(0.1 * solution.t), decimal=5 - ) - - # Safe mode (enforce events that won't be triggered) - model.events = [pybamm.Event("an event", var + 1)] - disc.process_model(model) - solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) - t_eval = np.linspace(0, 1, 100) - solution = solver.solve(model, t_eval) - np.testing.assert_array_equal(solution.t, t_eval) - np.testing.assert_array_almost_equal( - solution.y[0], np.exp(0.1 * solution.t), decimal=5 - ) - - def test_model_solver_python(self): - # Create model - pybamm.set_logging_level("ERROR") - model = pybamm.BaseModel() - model.convert_to_format = "python" - var = pybamm.Variable("var") - model.rhs = {var: 0.1 * var} - model.initial_conditions = {var: 1} - # No need to set parameters; can use base discretisation (no spatial operators) - - # create discretisation - disc = pybamm.Discretisation() - disc.process_model(model) - # Solve - solver = pybamm.CasadiSolver(mode="fast", rtol=1e-8, atol=1e-8) - t_eval = np.linspace(0, 1, 100) - solution = solver.solve(model, t_eval) - np.testing.assert_array_equal(solution.t, t_eval) - np.testing.assert_array_almost_equal( - solution.y[0], np.exp(0.1 * solution.t), decimal=5 - ) - pybamm.set_logging_level("WARNING") - - def test_model_solver_failure(self): - # Create model - model = pybamm.BaseModel() - var = pybamm.Variable("var") - model.rhs = {var: -pybamm.sqrt(var)} - model.initial_conditions = {var: 1} - # add events so that safe mode is used (won't be triggered) - model.events = [pybamm.Event("10", var - 10)] - # No need to set parameters; can use base discretisation (no spatial operators) - - # create discretisation - disc = pybamm.Discretisation() - model_disc = disc.process_model(model, inplace=False) - - solver = pybamm.CasadiSolver(extra_options_call={"regularity_check": False}) - # Solve with failure at t=2 - t_eval = np.linspace(0, 20, 100) - with self.assertRaises(pybamm.SolverError): - solver.solve(model_disc, t_eval) - # Solve with failure at t=0 - model.initial_conditions = {var: 0} - model_disc = disc.process_model(model, inplace=False) - t_eval = np.linspace(0, 20, 100) - with self.assertRaises(pybamm.SolverError): - solver.solve(model_disc, t_eval) - - def test_model_solver_events(self): - # Create model - model = pybamm.BaseModel() - whole_cell = ["negative electrode", "separator", "positive electrode"] - var1 = pybamm.Variable("var1", domain=whole_cell) - var2 = pybamm.Variable("var2", domain=whole_cell) - model.rhs = {var1: 0.1 * var1} - model.algebraic = {var2: 2 * var1 - var2} - model.initial_conditions = {var1: 1, var2: 2} - model.events = [ - pybamm.Event("var1 = 1.5", pybamm.min(var1 - 1.5)), - pybamm.Event("var2 = 2.5", pybamm.min(var2 - 2.5)), - ] - disc = get_discretisation_for_testing() - disc.process_model(model) - - # Solve using "safe" mode - solver = pybamm.CasadiSolver(mode="safe", rtol=1e-8, atol=1e-8) - t_eval = np.linspace(0, 5, 100) - solution = solver.solve(model, t_eval) - np.testing.assert_array_less(solution.y[0], 1.5) - np.testing.assert_array_less(solution.y[-1], 2.5 + 1e-10) - np.testing.assert_array_almost_equal( - solution.y[0], np.exp(0.1 * solution.t), decimal=5 - ) - np.testing.assert_array_almost_equal( - solution.y[-1], 2 * np.exp(0.1 * solution.t), decimal=5 - ) - - # Solve using "safe" mode with debug off - pybamm.settings.debug_mode = False - solver = pybamm.CasadiSolver(mode="safe", rtol=1e-8, atol=1e-8, dt_max=1) - t_eval = np.linspace(0, 5, 100) - solution = solver.solve(model, t_eval) - np.testing.assert_array_less(solution.y[0], 1.5) - np.testing.assert_array_less(solution.y[-1], 2.5 + 1e-10) - # test the last entry is exactly 2.5 - np.testing.assert_array_almost_equal(solution.y[-1, -1], 2.5, decimal=2) - np.testing.assert_array_almost_equal( - solution.y[0], np.exp(0.1 * solution.t), decimal=5 - ) - np.testing.assert_array_almost_equal( - solution.y[-1], 2 * np.exp(0.1 * solution.t), decimal=5 - ) - pybamm.settings.debug_mode = True - - # Try dt_max=0 to enforce using all timesteps - solver = pybamm.CasadiSolver(dt_max=0, rtol=1e-8, atol=1e-8) - t_eval = np.linspace(0, 5, 100) - solution = solver.solve(model, t_eval) - np.testing.assert_array_less(solution.y[0], 1.5) - np.testing.assert_array_less(solution.y[-1], 2.5) - np.testing.assert_array_almost_equal( - solution.y[0], np.exp(0.1 * solution.t), decimal=5 - ) - np.testing.assert_array_almost_equal( - solution.y[-1], 2 * np.exp(0.1 * solution.t), decimal=5 - ) - - # Test when an event returns nan - model = pybamm.BaseModel() - var = pybamm.Variable("var") - model.rhs = {var: 0.1 * var} - model.initial_conditions = {var: 1} - model.events = [ - pybamm.Event("event", var - 1.02), - pybamm.Event("sqrt event", pybamm.sqrt(1.0199 - var)), - ] - disc = pybamm.Discretisation() - disc.process_model(model) - solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) - solution = solver.solve(model, t_eval) - np.testing.assert_array_less(solution.y[0], 1.02 + 1e-10) - np.testing.assert_array_almost_equal(solution.y[0, -1], 1.02, decimal=2) - - def test_model_step(self): - # Create model - model = pybamm.BaseModel() - domain = ["negative electrode", "separator", "positive electrode"] - var = pybamm.Variable("var", domain=domain) - model.rhs = {var: 0.1 * var} - model.initial_conditions = {var: 1} - # No need to set parameters; can use base discretisation (no spatial operators) - - # create discretisation - mesh = get_mesh_for_testing() - spatial_methods = {"macroscale": pybamm.FiniteVolume()} - disc = pybamm.Discretisation(mesh, spatial_methods) - disc.process_model(model) - - solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) - - # Step once - dt = 1 - step_sol = solver.step(None, model, dt) - np.testing.assert_array_equal(step_sol.t, [0, dt]) - np.testing.assert_array_almost_equal(step_sol.y[0], np.exp(0.1 * step_sol.t)) - - # Step again (return 5 points) - step_sol_2 = solver.step(step_sol, model, dt, npts=5) - np.testing.assert_array_equal( - step_sol_2.t, np.concatenate([np.array([0]), np.linspace(dt, 2 * dt, 5)]) - ) - np.testing.assert_array_almost_equal( - step_sol_2.y[0], np.exp(0.1 * step_sol_2.t) - ) - - # Check steps give same solution as solve - t_eval = step_sol.t - solution = solver.solve(model, t_eval) - np.testing.assert_array_almost_equal(solution.y[0], step_sol.y[0]) - - def test_model_step_with_input(self): - # Create model - model = pybamm.BaseModel() - var = pybamm.Variable("var") - a = pybamm.InputParameter("a") - model.rhs = {var: a * var} - model.initial_conditions = {var: 1} - model.variables = {"a": a} - # No need to set parameters; can use base discretisation (no spatial operators) - - # create discretisation - disc = pybamm.Discretisation() - disc.process_model(model) - - solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) - - # Step with an input - dt = 0.1 - step_sol = solver.step(None, model, dt, npts=5, inputs={"a": 0.1}) - np.testing.assert_array_equal(step_sol.t, np.linspace(0, dt, 5)) - np.testing.assert_allclose(step_sol.y[0], np.exp(0.1 * step_sol.t)) - - # Step again with different inputs - step_sol_2 = solver.step(step_sol, model, dt, npts=5, inputs={"a": -1}) - np.testing.assert_array_equal(step_sol_2.t, np.linspace(0, 2 * dt, 9)) - np.testing.assert_array_equal( - step_sol_2["a"].entries, np.array([0.1, 0.1, 0.1, 0.1, 0.1, -1, -1, -1, -1]) - ) - np.testing.assert_allclose( - step_sol_2.y[0], - np.concatenate( - [ - np.exp(0.1 * step_sol.t[:5]), - np.exp(0.1 * step_sol.t[4]) * np.exp(-(step_sol.t[5:] - dt)), - ] - ), - ) - - def test_model_step_events(self): - # Create model - model = pybamm.BaseModel() - var1 = pybamm.Variable("var1") - var2 = pybamm.Variable("var2") - model.rhs = {var1: 0.1 * var1} - model.algebraic = {var2: 2 * var1 - var2} - model.initial_conditions = {var1: 1, var2: 2} - model.events = [ - pybamm.Event("var1 = 1.5", pybamm.min(var1 - 1.5)), - pybamm.Event("var2 = 2.5", pybamm.min(var2 - 2.5)), - ] - disc = pybamm.Discretisation() - disc.process_model(model) - - # Solve - step_solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) - dt = 0.05 - time = 0 - end_time = 5 - step_solution = None - while time < end_time: - step_solution = step_solver.step(step_solution, model, dt=dt, npts=10) - time += dt - np.testing.assert_array_less(step_solution.y[0], 1.5) - np.testing.assert_array_less(step_solution.y[-1], 2.5001) - np.testing.assert_array_almost_equal( - step_solution.y[0], np.exp(0.1 * step_solution.t), decimal=5 - ) - np.testing.assert_array_almost_equal( - step_solution.y[-1], 2 * np.exp(0.1 * step_solution.t), decimal=4 - ) - - def test_model_solver_with_inputs(self): - # Create model - model = pybamm.BaseModel() - domain = ["negative electrode", "separator", "positive electrode"] - var = pybamm.Variable("var", domain=domain) - model.rhs = {var: -pybamm.InputParameter("rate") * var} - model.initial_conditions = {var: 1} - model.events = [pybamm.Event("var=0.5", pybamm.min(var - 0.5))] - # No need to set parameters; can use base discretisation (no spatial - # operators) - - # create discretisation - mesh = get_mesh_for_testing() - spatial_methods = {"macroscale": pybamm.FiniteVolume()} - disc = pybamm.Discretisation(mesh, spatial_methods) - disc.process_model(model) - # Solve - solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) - t_eval = np.linspace(0, 10, 100) - solution = solver.solve(model, t_eval, inputs={"rate": 0.1}) - self.assertLess(len(solution.t), len(t_eval)) - np.testing.assert_allclose(solution.y[0], np.exp(-0.1 * solution.t), rtol=1e-04) - - def test_model_solver_dae_inputs_in_initial_conditions(self): - # Create model - model = pybamm.BaseModel() - var1 = pybamm.Variable("var1") - var2 = pybamm.Variable("var2") - model.rhs = {var1: pybamm.InputParameter("rate") * var1} - model.algebraic = {var2: var1 - var2} - model.initial_conditions = { - var1: pybamm.InputParameter("ic 1"), - var2: pybamm.InputParameter("ic 2"), - } - - # Solve - solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) - t_eval = np.linspace(0, 5, 100) - solution = solver.solve( - model, t_eval, inputs={"rate": -1, "ic 1": 0.1, "ic 2": 2} - ) - np.testing.assert_array_almost_equal( - solution.y[0], 0.1 * np.exp(-solution.t), decimal=5 - ) - np.testing.assert_array_almost_equal( - solution.y[-1], 0.1 * np.exp(-solution.t), decimal=5 - ) - - # Solve again with different initial conditions - solution = solver.solve( - model, t_eval, inputs={"rate": -0.1, "ic 1": 1, "ic 2": 3} - ) - np.testing.assert_array_almost_equal( - solution.y[0], 1 * np.exp(-0.1 * solution.t), decimal=5 - ) - np.testing.assert_array_almost_equal( - solution.y[-1], 1 * np.exp(-0.1 * solution.t), decimal=5 - ) - - def test_model_solver_with_external(self): - # Create model - model = pybamm.BaseModel() - domain = ["negative electrode", "separator", "positive electrode"] - var1 = pybamm.Variable("var1", domain=domain) - var2 = pybamm.Variable("var2", domain=domain) - model.rhs = {var1: -var2} - model.initial_conditions = {var1: 1} - model.external_variables = [var2] - model.variables = {"var1": var1, "var2": var2} - # No need to set parameters; can use base discretisation (no spatial - # operators) - - # create discretisation - mesh = get_mesh_for_testing() - spatial_methods = {"macroscale": pybamm.FiniteVolume()} - disc = pybamm.Discretisation(mesh, spatial_methods) - disc.process_model(model) - # Solve - solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) - t_eval = np.linspace(0, 10, 100) - solution = solver.solve(model, t_eval, external_variables={"var2": 0.5}) - np.testing.assert_allclose(solution.y[0], 1 - 0.5 * solution.t, rtol=1e-06) - - def test_model_solver_with_non_identity_mass(self): - model = pybamm.BaseModel() - var1 = pybamm.Variable("var1", domain="negative electrode") - var2 = pybamm.Variable("var2", domain="negative electrode") - model.rhs = {var1: var1} - model.algebraic = {var2: 2 * var1 - var2} - model.initial_conditions = {var1: 1, var2: 2} - disc = get_discretisation_for_testing() - disc.process_model(model) - - # FV discretisation has identity mass. Manually set the mass matrix to - # be a diag of 10s here for testing. Note that the algebraic part is all - # zeros - mass_matrix = 10 * model.mass_matrix.entries - model.mass_matrix = pybamm.Matrix(mass_matrix) - - # Note that mass_matrix_inv is just the inverse of the ode block of the - # mass matrix - mass_matrix_inv = 0.1 * eye(int(mass_matrix.shape[0] / 2)) - model.mass_matrix_inv = pybamm.Matrix(mass_matrix_inv) - - # Solve - solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) - t_eval = np.linspace(0, 1, 100) - solution = solver.solve(model, t_eval) - np.testing.assert_array_equal(solution.t, t_eval) - np.testing.assert_allclose(solution.y[0], np.exp(0.1 * solution.t)) - np.testing.assert_allclose(solution.y[-1], 2 * np.exp(0.1 * solution.t)) - - def test_dae_solver_algebraic_model(self): - model = pybamm.BaseModel() - var = pybamm.Variable("var") - model.algebraic = {var: var + 1} - model.initial_conditions = {var: 0} - - disc = pybamm.Discretisation() - disc.process_model(model) - - solver = pybamm.CasadiSolver() - t_eval = np.linspace(0, 1) - with self.assertRaisesRegex( - pybamm.SolverError, "Cannot use CasadiSolver to solve algebraic model" - ): - solver.solve(model, t_eval) - - -class TestCasadiSolverSensitivity(unittest.TestCase): - def test_solve_with_symbolic_input(self): - # Simple system: a single differential equation - var = pybamm.Variable("var") - model = pybamm.BaseModel() - model.rhs = {var: pybamm.InputParameter("param")} - model.initial_conditions = {var: 2} - model.variables = {"var": var} - - # create discretisation - disc = pybamm.Discretisation() - disc.process_model(model) - - # Solve - solver = pybamm.CasadiSolver() - t_eval = np.linspace(0, 1) - solution = solver.solve(model, t_eval) - np.testing.assert_array_almost_equal( - solution["var"].value({"param": 7}).full().flatten(), 2 + 7 * t_eval - ) - np.testing.assert_array_almost_equal( - solution["var"].value({"param": -3}).full().flatten(), 2 - 3 * t_eval - ) - np.testing.assert_array_almost_equal( - solution["var"].sensitivity({"param": 3}).full().flatten(), t_eval - ) - - def test_least_squares_fit(self): - # Simple system: a single algebraic equation - var1 = pybamm.Variable("var1", domain="negative electrode") - var2 = pybamm.Variable("var2", domain="negative electrode") - model = pybamm.BaseModel() - p = pybamm.InputParameter("p") - q = pybamm.InputParameter("q") - model.rhs = {var1: -var1} - model.algebraic = {var2: (var2 - p)} - model.initial_conditions = {var1: 1, var2: 3} - model.variables = {"objective": (var2 - q) ** 2 + (p - 3) ** 2} - - # create discretisation - disc = get_discretisation_for_testing() - disc.process_model(model) - - # Solve - solver = pybamm.CasadiSolver() - solution = solver.solve(model, np.linspace(0, 1)) - sol_var = solution["objective"] - - def objective(x): - return sol_var.value({"p": x[0], "q": x[1]}).full().flatten() - - # without jacobian - lsq_sol = least_squares(objective, [2, 2], method="lm") - np.testing.assert_array_almost_equal(lsq_sol.x, [3, 3], decimal=3) - - def jac(x): - return sol_var.sensitivity({"p": x[0], "q": x[1]}) - - # with jacobian - lsq_sol = least_squares(objective, [2, 2], jac=jac, method="lm") - np.testing.assert_array_almost_equal(lsq_sol.x, [3, 3], decimal=3) - - def test_solve_with_symbolic_input_1D_scalar_input(self): - var = pybamm.Variable("var", "negative electrode") - model = pybamm.BaseModel() - param = pybamm.InputParameter("param") - model.rhs = {var: -param * var} - model.initial_conditions = {var: 2} - model.variables = {"var": var} - - # create discretisation - disc = get_discretisation_for_testing() - disc.process_model(model) - - # Solve - scalar input - solver = pybamm.CasadiSolver() - t_eval = np.linspace(0, 1) - solution = solver.solve(model, t_eval) - np.testing.assert_array_almost_equal( - solution["var"].value({"param": 7}), - np.repeat(2 * np.exp(-7 * t_eval), 40)[:, np.newaxis], - decimal=4, - ) - np.testing.assert_array_almost_equal( - solution["var"].value({"param": 3}), - np.repeat(2 * np.exp(-3 * t_eval), 40)[:, np.newaxis], - decimal=4, - ) - np.testing.assert_array_almost_equal( - solution["var"].sensitivity({"param": 3}), - np.repeat( - -2 * t_eval * np.exp(-3 * t_eval), disc.mesh["negative electrode"].npts - )[:, np.newaxis], - decimal=4, - ) - - def test_solve_with_symbolic_input_1D_vector_input(self): - var = pybamm.Variable("var", "negative electrode") - model = pybamm.BaseModel() - param = pybamm.InputParameter("param", "negative electrode") - model.rhs = {var: -param * var} - model.initial_conditions = {var: 2} - model.variables = {"var": var} - - # create discretisation - disc = get_discretisation_for_testing() - disc.process_model(model) - - # Solve - scalar input - solver = pybamm.CasadiSolver() - solution = solver.solve(model, np.linspace(0, 1)) - n = disc.mesh["negative electrode"].npts - - solver = pybamm.CasadiSolver() - t_eval = np.linspace(0, 1) - solution = solver.solve(model, t_eval) - p = np.linspace(0, 1, n)[:, np.newaxis] - np.testing.assert_array_almost_equal( - solution["var"].value({"param": 3 * np.ones(n)}), - np.repeat(2 * np.exp(-3 * t_eval), 40)[:, np.newaxis], - decimal=4, - ) - np.testing.assert_array_almost_equal( - solution["var"].value({"param": 2 * p}), - 2 * np.exp(-2 * p * t_eval).T.reshape(-1, 1), - decimal=4, - ) - np.testing.assert_array_almost_equal( - solution["var"].sensitivity({"param": 3 * np.ones(n)}), - np.kron(-2 * t_eval * np.exp(-3 * t_eval), np.eye(40)).T, - decimal=4, - ) - - sens = solution["var"].sensitivity({"param": p}).full() - for idx in range(len(t_eval)): - np.testing.assert_array_almost_equal( - sens[40 * idx : 40 * (idx + 1), :], - -2 * t_eval[idx] * np.exp(-p * t_eval[idx]) * np.eye(40), - decimal=4, - ) - - def test_solve_with_symbolic_input_in_initial_conditions(self): - # Simple system: a single algebraic equation - var = pybamm.Variable("var") - model = pybamm.BaseModel() - model.rhs = {var: -var} - model.initial_conditions = {var: pybamm.InputParameter("param")} - model.variables = {"var": var} - - # create discretisation - disc = pybamm.Discretisation() - disc.process_model(model) - - # Solve - solver = pybamm.CasadiSolver(atol=1e-10, rtol=1e-10) - t_eval = np.linspace(0, 1) - solution = solver.solve(model, t_eval) - np.testing.assert_array_almost_equal( - solution["var"].value({"param": 7}), 7 * np.exp(-t_eval)[np.newaxis, :] - ) - np.testing.assert_array_almost_equal( - solution["var"].value({"param": 3}), 3 * np.exp(-t_eval)[np.newaxis, :] - ) - np.testing.assert_array_almost_equal( - solution["var"].sensitivity({"param": 3}), np.exp(-t_eval)[:, np.newaxis] - ) - - def test_least_squares_fit_input_in_initial_conditions(self): - # Simple system: a single algebraic equation - var1 = pybamm.Variable("var1", domain="negative electrode") - var2 = pybamm.Variable("var2", domain="negative electrode") - model = pybamm.BaseModel() - p = pybamm.InputParameter("p") - q = pybamm.InputParameter("q") - model.rhs = {var1: -var1} - model.algebraic = {var2: (var2 - p)} - model.initial_conditions = {var1: 1, var2: p} - model.variables = {"objective": (var2 - q) ** 2 + (p - 3) ** 2} - - # create discretisation - disc = get_discretisation_for_testing() - disc.process_model(model) - - # Solve - solver = pybamm.CasadiSolver() - solution = solver.solve(model, np.linspace(0, 1)) - sol_var = solution["objective"] - - def objective(x): - return sol_var.value({"p": x[0], "q": x[1]}).full().flatten() - - # without jacobian - lsq_sol = least_squares(objective, [2, 2], method="lm") - np.testing.assert_array_almost_equal(lsq_sol.x, [3, 3], decimal=3) - - -class TestCasadiSolverWithForwardSensitivityEquations(unittest.TestCase): +# class TestCasadiSolver(unittest.TestCase): +# def test_bad_mode(self): +# with self.assertRaisesRegex(ValueError, "invalid mode"): +# pybamm.CasadiSolver(mode="bad mode") + +# def test_model_solver(self): +# # Create model +# model = pybamm.BaseModel() +# var = pybamm.Variable("var") +# model.rhs = {var: 0.1 * var} +# model.initial_conditions = {var: 1} +# # No need to set parameters; can use base discretisation (no spatial operators) + +# # create discretisation +# disc = pybamm.Discretisation() +# model_disc = disc.process_model(model, inplace=False) +# # Solve +# solver = pybamm.CasadiSolver(mode="fast", rtol=1e-8, atol=1e-8) +# t_eval = np.linspace(0, 1, 100) +# solution = solver.solve(model_disc, t_eval) +# np.testing.assert_array_equal(solution.t, t_eval) +# np.testing.assert_array_almost_equal( +# solution.y[0], np.exp(0.1 * solution.t), decimal=5 +# ) + +# # Safe mode (enforce events that won't be triggered) +# model.events = [pybamm.Event("an event", var + 1)] +# disc.process_model(model) +# solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) +# t_eval = np.linspace(0, 1, 100) +# solution = solver.solve(model, t_eval) +# np.testing.assert_array_equal(solution.t, t_eval) +# np.testing.assert_array_almost_equal( +# solution.y[0], np.exp(0.1 * solution.t), decimal=5 +# ) + +# def test_model_solver_python(self): +# # Create model +# pybamm.set_logging_level("ERROR") +# model = pybamm.BaseModel() +# model.convert_to_format = "python" +# var = pybamm.Variable("var") +# model.rhs = {var: 0.1 * var} +# model.initial_conditions = {var: 1} +# # No need to set parameters; can use base discretisation (no spatial operators) + +# # create discretisation +# disc = pybamm.Discretisation() +# disc.process_model(model) +# # Solve +# solver = pybamm.CasadiSolver(mode="fast", rtol=1e-8, atol=1e-8) +# t_eval = np.linspace(0, 1, 100) +# solution = solver.solve(model, t_eval) +# np.testing.assert_array_equal(solution.t, t_eval) +# np.testing.assert_array_almost_equal( +# solution.y[0], np.exp(0.1 * solution.t), decimal=5 +# ) +# pybamm.set_logging_level("WARNING") + +# def test_model_solver_failure(self): +# # Create model +# model = pybamm.BaseModel() +# var = pybamm.Variable("var") +# model.rhs = {var: -pybamm.sqrt(var)} +# model.initial_conditions = {var: 1} +# # add events so that safe mode is used (won't be triggered) +# model.events = [pybamm.Event("10", var - 10)] +# # No need to set parameters; can use base discretisation (no spatial operators) + +# # create discretisation +# disc = pybamm.Discretisation() +# model_disc = disc.process_model(model, inplace=False) + +# solver = pybamm.CasadiSolver(extra_options_call={"regularity_check": False}) +# # Solve with failure at t=2 +# t_eval = np.linspace(0, 20, 100) +# with self.assertRaises(pybamm.SolverError): +# solver.solve(model_disc, t_eval) +# # Solve with failure at t=0 +# model.initial_conditions = {var: 0} +# model_disc = disc.process_model(model, inplace=False) +# t_eval = np.linspace(0, 20, 100) +# with self.assertRaises(pybamm.SolverError): +# solver.solve(model_disc, t_eval) + +# def test_model_solver_events(self): +# # Create model +# model = pybamm.BaseModel() +# whole_cell = ["negative electrode", "separator", "positive electrode"] +# var1 = pybamm.Variable("var1", domain=whole_cell) +# var2 = pybamm.Variable("var2", domain=whole_cell) +# model.rhs = {var1: 0.1 * var1} +# model.algebraic = {var2: 2 * var1 - var2} +# model.initial_conditions = {var1: 1, var2: 2} +# model.events = [ +# pybamm.Event("var1 = 1.5", pybamm.min(var1 - 1.5)), +# pybamm.Event("var2 = 2.5", pybamm.min(var2 - 2.5)), +# ] +# disc = get_discretisation_for_testing() +# disc.process_model(model) + +# # Solve using "safe" mode +# solver = pybamm.CasadiSolver(mode="safe", rtol=1e-8, atol=1e-8) +# t_eval = np.linspace(0, 5, 100) +# solution = solver.solve(model, t_eval) +# np.testing.assert_array_less(solution.y[0], 1.5) +# np.testing.assert_array_less(solution.y[-1], 2.5 + 1e-10) +# np.testing.assert_array_almost_equal( +# solution.y[0], np.exp(0.1 * solution.t), decimal=5 +# ) +# np.testing.assert_array_almost_equal( +# solution.y[-1], 2 * np.exp(0.1 * solution.t), decimal=5 +# ) + +# # Solve using "safe" mode with debug off +# pybamm.settings.debug_mode = False +# solver = pybamm.CasadiSolver(mode="safe", rtol=1e-8, atol=1e-8, dt_max=1) +# t_eval = np.linspace(0, 5, 100) +# solution = solver.solve(model, t_eval) +# np.testing.assert_array_less(solution.y[0], 1.5) +# np.testing.assert_array_less(solution.y[-1], 2.5 + 1e-10) +# # test the last entry is exactly 2.5 +# np.testing.assert_array_almost_equal(solution.y[-1, -1], 2.5, decimal=2) +# np.testing.assert_array_almost_equal( +# solution.y[0], np.exp(0.1 * solution.t), decimal=5 +# ) +# np.testing.assert_array_almost_equal( +# solution.y[-1], 2 * np.exp(0.1 * solution.t), decimal=5 +# ) +# pybamm.settings.debug_mode = True + +# # Try dt_max=0 to enforce using all timesteps +# solver = pybamm.CasadiSolver(dt_max=0, rtol=1e-8, atol=1e-8) +# t_eval = np.linspace(0, 5, 100) +# solution = solver.solve(model, t_eval) +# np.testing.assert_array_less(solution.y[0], 1.5) +# np.testing.assert_array_less(solution.y[-1], 2.5) +# np.testing.assert_array_almost_equal( +# solution.y[0], np.exp(0.1 * solution.t), decimal=5 +# ) +# np.testing.assert_array_almost_equal( +# solution.y[-1], 2 * np.exp(0.1 * solution.t), decimal=5 +# ) + +# # Test when an event returns nan +# model = pybamm.BaseModel() +# var = pybamm.Variable("var") +# model.rhs = {var: 0.1 * var} +# model.initial_conditions = {var: 1} +# model.events = [ +# pybamm.Event("event", var - 1.02), +# pybamm.Event("sqrt event", pybamm.sqrt(1.0199 - var)), +# ] +# disc = pybamm.Discretisation() +# disc.process_model(model) +# solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) +# solution = solver.solve(model, t_eval) +# np.testing.assert_array_less(solution.y[0], 1.02 + 1e-10) +# np.testing.assert_array_almost_equal(solution.y[0, -1], 1.02, decimal=2) + +# def test_model_step(self): +# # Create model +# model = pybamm.BaseModel() +# domain = ["negative electrode", "separator", "positive electrode"] +# var = pybamm.Variable("var", domain=domain) +# model.rhs = {var: 0.1 * var} +# model.initial_conditions = {var: 1} +# # No need to set parameters; can use base discretisation (no spatial operators) + +# # create discretisation +# mesh = get_mesh_for_testing() +# spatial_methods = {"macroscale": pybamm.FiniteVolume()} +# disc = pybamm.Discretisation(mesh, spatial_methods) +# disc.process_model(model) + +# solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) + +# # Step once +# dt = 1 +# step_sol = solver.step(None, model, dt) +# np.testing.assert_array_equal(step_sol.t, [0, dt]) +# np.testing.assert_array_almost_equal(step_sol.y[0], np.exp(0.1 * step_sol.t)) + +# # Step again (return 5 points) +# step_sol_2 = solver.step(step_sol, model, dt, npts=5) +# np.testing.assert_array_equal( +# step_sol_2.t, np.concatenate([np.array([0]), np.linspace(dt, 2 * dt, 5)]) +# ) +# np.testing.assert_array_almost_equal( +# step_sol_2.y[0], np.exp(0.1 * step_sol_2.t) +# ) + +# # Check steps give same solution as solve +# t_eval = step_sol.t +# solution = solver.solve(model, t_eval) +# np.testing.assert_array_almost_equal(solution.y[0], step_sol.y[0]) + +# def test_model_step_with_input(self): +# # Create model +# model = pybamm.BaseModel() +# var = pybamm.Variable("var") +# a = pybamm.InputParameter("a") +# model.rhs = {var: a * var} +# model.initial_conditions = {var: 1} +# model.variables = {"a": a} +# # No need to set parameters; can use base discretisation (no spatial operators) + +# # create discretisation +# disc = pybamm.Discretisation() +# disc.process_model(model) + +# solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) + +# # Step with an input +# dt = 0.1 +# step_sol = solver.step(None, model, dt, npts=5, inputs={"a": 0.1}) +# np.testing.assert_array_equal(step_sol.t, np.linspace(0, dt, 5)) +# np.testing.assert_allclose(step_sol.y[0], np.exp(0.1 * step_sol.t)) + +# # Step again with different inputs +# step_sol_2 = solver.step(step_sol, model, dt, npts=5, inputs={"a": -1}) +# np.testing.assert_array_equal(step_sol_2.t, np.linspace(0, 2 * dt, 9)) +# np.testing.assert_array_equal( +# step_sol_2["a"].entries, np.array([0.1, 0.1, 0.1, 0.1, 0.1, -1, -1, -1, -1]) +# ) +# np.testing.assert_allclose( +# step_sol_2.y[0], +# np.concatenate( +# [ +# np.exp(0.1 * step_sol.t[:5]), +# np.exp(0.1 * step_sol.t[4]) * np.exp(-(step_sol.t[5:] - dt)), +# ] +# ), +# ) + +# def test_model_step_events(self): +# # Create model +# model = pybamm.BaseModel() +# var1 = pybamm.Variable("var1") +# var2 = pybamm.Variable("var2") +# model.rhs = {var1: 0.1 * var1} +# model.algebraic = {var2: 2 * var1 - var2} +# model.initial_conditions = {var1: 1, var2: 2} +# model.events = [ +# pybamm.Event("var1 = 1.5", pybamm.min(var1 - 1.5)), +# pybamm.Event("var2 = 2.5", pybamm.min(var2 - 2.5)), +# ] +# disc = pybamm.Discretisation() +# disc.process_model(model) + +# # Solve +# step_solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) +# dt = 0.05 +# time = 0 +# end_time = 5 +# step_solution = None +# while time < end_time: +# step_solution = step_solver.step(step_solution, model, dt=dt, npts=10) +# time += dt +# np.testing.assert_array_less(step_solution.y[0], 1.5) +# np.testing.assert_array_less(step_solution.y[-1], 2.5001) +# np.testing.assert_array_almost_equal( +# step_solution.y[0], np.exp(0.1 * step_solution.t), decimal=5 +# ) +# np.testing.assert_array_almost_equal( +# step_solution.y[-1], 2 * np.exp(0.1 * step_solution.t), decimal=4 +# ) + +# def test_model_solver_with_inputs(self): +# # Create model +# model = pybamm.BaseModel() +# domain = ["negative electrode", "separator", "positive electrode"] +# var = pybamm.Variable("var", domain=domain) +# model.rhs = {var: -pybamm.InputParameter("rate") * var} +# model.initial_conditions = {var: 1} +# model.events = [pybamm.Event("var=0.5", pybamm.min(var - 0.5))] +# # No need to set parameters; can use base discretisation (no spatial +# # operators) + +# # create discretisation +# mesh = get_mesh_for_testing() +# spatial_methods = {"macroscale": pybamm.FiniteVolume()} +# disc = pybamm.Discretisation(mesh, spatial_methods) +# disc.process_model(model) +# # Solve +# solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) +# t_eval = np.linspace(0, 10, 100) +# solution = solver.solve(model, t_eval, inputs={"rate": 0.1}) +# self.assertLess(len(solution.t), len(t_eval)) +# np.testing.assert_allclose(solution.y[0], np.exp(-0.1 * solution.t), rtol=1e-04) + +# def test_model_solver_dae_inputs_in_initial_conditions(self): +# # Create model +# model = pybamm.BaseModel() +# var1 = pybamm.Variable("var1") +# var2 = pybamm.Variable("var2") +# model.rhs = {var1: pybamm.InputParameter("rate") * var1} +# model.algebraic = {var2: var1 - var2} +# model.initial_conditions = { +# var1: pybamm.InputParameter("ic 1"), +# var2: pybamm.InputParameter("ic 2"), +# } + +# # Solve +# solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) +# t_eval = np.linspace(0, 5, 100) +# solution = solver.solve( +# model, t_eval, inputs={"rate": -1, "ic 1": 0.1, "ic 2": 2} +# ) +# np.testing.assert_array_almost_equal( +# solution.y[0], 0.1 * np.exp(-solution.t), decimal=5 +# ) +# np.testing.assert_array_almost_equal( +# solution.y[-1], 0.1 * np.exp(-solution.t), decimal=5 +# ) + +# # Solve again with different initial conditions +# solution = solver.solve( +# model, t_eval, inputs={"rate": -0.1, "ic 1": 1, "ic 2": 3} +# ) +# np.testing.assert_array_almost_equal( +# solution.y[0], 1 * np.exp(-0.1 * solution.t), decimal=5 +# ) +# np.testing.assert_array_almost_equal( +# solution.y[-1], 1 * np.exp(-0.1 * solution.t), decimal=5 +# ) + +# def test_model_solver_with_external(self): +# # Create model +# model = pybamm.BaseModel() +# domain = ["negative electrode", "separator", "positive electrode"] +# var1 = pybamm.Variable("var1", domain=domain) +# var2 = pybamm.Variable("var2", domain=domain) +# model.rhs = {var1: -var2} +# model.initial_conditions = {var1: 1} +# model.external_variables = [var2] +# model.variables = {"var1": var1, "var2": var2} +# # No need to set parameters; can use base discretisation (no spatial +# # operators) + +# # create discretisation +# mesh = get_mesh_for_testing() +# spatial_methods = {"macroscale": pybamm.FiniteVolume()} +# disc = pybamm.Discretisation(mesh, spatial_methods) +# disc.process_model(model) +# # Solve +# solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) +# t_eval = np.linspace(0, 10, 100) +# solution = solver.solve(model, t_eval, external_variables={"var2": 0.5}) +# np.testing.assert_allclose(solution.y[0], 1 - 0.5 * solution.t, rtol=1e-06) + +# def test_model_solver_with_non_identity_mass(self): +# model = pybamm.BaseModel() +# var1 = pybamm.Variable("var1", domain="negative electrode") +# var2 = pybamm.Variable("var2", domain="negative electrode") +# model.rhs = {var1: var1} +# model.algebraic = {var2: 2 * var1 - var2} +# model.initial_conditions = {var1: 1, var2: 2} +# disc = get_discretisation_for_testing() +# disc.process_model(model) + +# # FV discretisation has identity mass. Manually set the mass matrix to +# # be a diag of 10s here for testing. Note that the algebraic part is all +# # zeros +# mass_matrix = 10 * model.mass_matrix.entries +# model.mass_matrix = pybamm.Matrix(mass_matrix) + +# # Note that mass_matrix_inv is just the inverse of the ode block of the +# # mass matrix +# mass_matrix_inv = 0.1 * eye(int(mass_matrix.shape[0] / 2)) +# model.mass_matrix_inv = pybamm.Matrix(mass_matrix_inv) + +# # Solve +# solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) +# t_eval = np.linspace(0, 1, 100) +# solution = solver.solve(model, t_eval) +# np.testing.assert_array_equal(solution.t, t_eval) +# np.testing.assert_allclose(solution.y[0], np.exp(0.1 * solution.t)) +# np.testing.assert_allclose(solution.y[-1], 2 * np.exp(0.1 * solution.t)) + +# def test_dae_solver_algebraic_model(self): +# model = pybamm.BaseModel() +# var = pybamm.Variable("var") +# model.algebraic = {var: var + 1} +# model.initial_conditions = {var: 0} + +# disc = pybamm.Discretisation() +# disc.process_model(model) + +# solver = pybamm.CasadiSolver() +# t_eval = np.linspace(0, 1) +# with self.assertRaisesRegex( +# pybamm.SolverError, "Cannot use CasadiSolver to solve algebraic model" +# ): +# solver.solve(model, t_eval) + + +# class TestCasadiSolverSensitivity(unittest.TestCase): +# def test_solve_with_symbolic_input(self): +# # Simple system: a single differential equation +# var = pybamm.Variable("var") +# model = pybamm.BaseModel() +# model.rhs = {var: pybamm.InputParameter("param")} +# model.initial_conditions = {var: 2} +# model.variables = {"var": var} + +# # create discretisation +# disc = pybamm.Discretisation() +# disc.process_model(model) + +# # Solve +# solver = pybamm.CasadiSolver() +# t_eval = np.linspace(0, 1) +# solution = solver.solve(model, t_eval) +# np.testing.assert_array_almost_equal( +# solution["var"].value({"param": 7}).full().flatten(), 2 + 7 * t_eval +# ) +# np.testing.assert_array_almost_equal( +# solution["var"].value({"param": -3}).full().flatten(), 2 - 3 * t_eval +# ) +# np.testing.assert_array_almost_equal( +# solution["var"].sensitivity({"param": 3}).full().flatten(), t_eval +# ) + +# def test_least_squares_fit(self): +# # Simple system: a single algebraic equation +# var1 = pybamm.Variable("var1", domain="negative electrode") +# var2 = pybamm.Variable("var2", domain="negative electrode") +# model = pybamm.BaseModel() +# p = pybamm.InputParameter("p") +# q = pybamm.InputParameter("q") +# model.rhs = {var1: -var1} +# model.algebraic = {var2: (var2 - p)} +# model.initial_conditions = {var1: 1, var2: 3} +# model.variables = {"objective": (var2 - q) ** 2 + (p - 3) ** 2} + +# # create discretisation +# disc = get_discretisation_for_testing() +# disc.process_model(model) + +# # Solve +# solver = pybamm.CasadiSolver() +# solution = solver.solve(model, np.linspace(0, 1)) +# sol_var = solution["objective"] + +# def objective(x): +# return sol_var.value({"p": x[0], "q": x[1]}).full().flatten() + +# # without jacobian +# lsq_sol = least_squares(objective, [2, 2], method="lm") +# np.testing.assert_array_almost_equal(lsq_sol.x, [3, 3], decimal=3) + +# def jac(x): +# return sol_var.sensitivity({"p": x[0], "q": x[1]}) + +# # with jacobian +# lsq_sol = least_squares(objective, [2, 2], jac=jac, method="lm") +# np.testing.assert_array_almost_equal(lsq_sol.x, [3, 3], decimal=3) + +# def test_solve_with_symbolic_input_1D_scalar_input(self): +# var = pybamm.Variable("var", "negative electrode") +# model = pybamm.BaseModel() +# param = pybamm.InputParameter("param") +# model.rhs = {var: -param * var} +# model.initial_conditions = {var: 2} +# model.variables = {"var": var} + +# # create discretisation +# disc = get_discretisation_for_testing() +# disc.process_model(model) + +# # Solve - scalar input +# solver = pybamm.CasadiSolver() +# t_eval = np.linspace(0, 1) +# solution = solver.solve(model, t_eval) +# np.testing.assert_array_almost_equal( +# solution["var"].value({"param": 7}), +# np.repeat(2 * np.exp(-7 * t_eval), 40)[:, np.newaxis], +# decimal=4, +# ) +# np.testing.assert_array_almost_equal( +# solution["var"].value({"param": 3}), +# np.repeat(2 * np.exp(-3 * t_eval), 40)[:, np.newaxis], +# decimal=4, +# ) +# np.testing.assert_array_almost_equal( +# solution["var"].sensitivity({"param": 3}), +# np.repeat( +# -2 * t_eval * np.exp(-3 * t_eval), disc.mesh["negative electrode"].npts +# )[:, np.newaxis], +# decimal=4, +# ) + +# def test_solve_with_symbolic_input_1D_vector_input(self): +# var = pybamm.Variable("var", "negative electrode") +# model = pybamm.BaseModel() +# param = pybamm.InputParameter("param", "negative electrode") +# model.rhs = {var: -param * var} +# model.initial_conditions = {var: 2} +# model.variables = {"var": var} + +# # create discretisation +# disc = get_discretisation_for_testing() +# disc.process_model(model) + +# # Solve - scalar input +# solver = pybamm.CasadiSolver() +# solution = solver.solve(model, np.linspace(0, 1)) +# n = disc.mesh["negative electrode"].npts + +# solver = pybamm.CasadiSolver() +# t_eval = np.linspace(0, 1) +# solution = solver.solve(model, t_eval) +# p = np.linspace(0, 1, n)[:, np.newaxis] +# np.testing.assert_array_almost_equal( +# solution["var"].value({"param": 3 * np.ones(n)}), +# np.repeat(2 * np.exp(-3 * t_eval), 40)[:, np.newaxis], +# decimal=4, +# ) +# np.testing.assert_array_almost_equal( +# solution["var"].value({"param": 2 * p}), +# 2 * np.exp(-2 * p * t_eval).T.reshape(-1, 1), +# decimal=4, +# ) +# np.testing.assert_array_almost_equal( +# solution["var"].sensitivity({"param": 3 * np.ones(n)}), +# np.kron(-2 * t_eval * np.exp(-3 * t_eval), np.eye(40)).T, +# decimal=4, +# ) + +# sens = solution["var"].sensitivity({"param": p}).full() +# for idx in range(len(t_eval)): +# np.testing.assert_array_almost_equal( +# sens[40 * idx : 40 * (idx + 1), :], +# -2 * t_eval[idx] * np.exp(-p * t_eval[idx]) * np.eye(40), +# decimal=4, +# ) + +# def test_solve_with_symbolic_input_in_initial_conditions(self): +# # Simple system: a single algebraic equation +# var = pybamm.Variable("var") +# model = pybamm.BaseModel() +# model.rhs = {var: -var} +# model.initial_conditions = {var: pybamm.InputParameter("param")} +# model.variables = {"var": var} + +# # create discretisation +# disc = pybamm.Discretisation() +# disc.process_model(model) + +# # Solve +# solver = pybamm.CasadiSolver(atol=1e-10, rtol=1e-10) +# t_eval = np.linspace(0, 1) +# solution = solver.solve(model, t_eval) +# np.testing.assert_array_almost_equal( +# solution["var"].value({"param": 7}), 7 * np.exp(-t_eval)[np.newaxis, :] +# ) +# np.testing.assert_array_almost_equal( +# solution["var"].value({"param": 3}), 3 * np.exp(-t_eval)[np.newaxis, :] +# ) +# np.testing.assert_array_almost_equal( +# solution["var"].sensitivity({"param": 3}), np.exp(-t_eval)[:, np.newaxis] +# ) + +# def test_least_squares_fit_input_in_initial_conditions(self): +# # Simple system: a single algebraic equation +# var1 = pybamm.Variable("var1", domain="negative electrode") +# var2 = pybamm.Variable("var2", domain="negative electrode") +# model = pybamm.BaseModel() +# p = pybamm.InputParameter("p") +# q = pybamm.InputParameter("q") +# model.rhs = {var1: -var1} +# model.algebraic = {var2: (var2 - p)} +# model.initial_conditions = {var1: 1, var2: p} +# model.variables = {"objective": (var2 - q) ** 2 + (p - 3) ** 2} + +# # create discretisation +# disc = get_discretisation_for_testing() +# disc.process_model(model) + +# # Solve +# solver = pybamm.CasadiSolver() +# solution = solver.solve(model, np.linspace(0, 1)) +# sol_var = solution["objective"] + +# def objective(x): +# return sol_var.value({"p": x[0], "q": x[1]}).full().flatten() + +# # without jacobian +# lsq_sol = least_squares(objective, [2, 2], method="lm") +# np.testing.assert_array_almost_equal(lsq_sol.x, [3, 3], decimal=3) + + +# class TestCasadiSolverODEsWithForwardSensitivityEquations(unittest.TestCase): +# def test_solve_sensitivity_scalar_var_scalar_input(self): +# # Create model +# model = pybamm.BaseModel() +# var = pybamm.Variable("var") +# p = pybamm.InputParameter("p") +# model.rhs = {var: p * var} +# model.initial_conditions = {var: 1} +# model.variables = {"var squared": var ** 2} + +# # Solve +# # Make sure that passing in extra options works +# solver = pybamm.CasadiSolver( +# mode="fast", rtol=1e-10, atol=1e-10, solve_sensitivity_equations=True +# ) +# t_eval = np.linspace(0, 1, 80) +# solution = solver.solve(model, t_eval, inputs={"p": 0.1}) +# np.testing.assert_array_equal(solution.t, t_eval) +# np.testing.assert_allclose(solution.y[0], np.exp(0.1 * solution.t)) +# np.testing.assert_allclose( +# solution.sensitivity["p"], +# (solution.t * np.exp(0.1 * solution.t))[:, np.newaxis], +# ) +# np.testing.assert_allclose( +# solution["var squared"].data, np.exp(0.1 * solution.t) ** 2 +# ) +# np.testing.assert_allclose( +# solution["var squared"].sensitivity["p"], +# (2 * np.exp(0.1 * solution.t) * solution.t * np.exp(0.1 * solution.t))[ +# :, np.newaxis +# ], +# ) + +# # More complicated model +# # Create model +# model = pybamm.BaseModel() +# var = pybamm.Variable("var") +# p = pybamm.InputParameter("p") +# q = pybamm.InputParameter("q") +# r = pybamm.InputParameter("r") +# s = pybamm.InputParameter("s") +# model.rhs = {var: p * q} +# model.initial_conditions = {var: r} +# model.variables = {"var times s": var * s} + +# # Solve +# # Make sure that passing in extra options works +# solver = pybamm.CasadiSolver( +# rtol=1e-10, atol=1e-10, solve_sensitivity_equations=True +# ) +# t_eval = np.linspace(0, 1, 80) +# solution = solver.solve( +# model, t_eval, inputs={"p": 0.1, "q": 2, "r": -1, "s": 0.5} +# ) +# np.testing.assert_allclose(solution.y[0], -1 + 0.2 * solution.t) +# np.testing.assert_allclose( +# solution.sensitivity["p"], (2 * solution.t)[:, np.newaxis], +# ) +# np.testing.assert_allclose( +# solution.sensitivity["q"], (0.1 * solution.t)[:, np.newaxis], +# ) +# np.testing.assert_allclose(solution.sensitivity["r"], 1) +# np.testing.assert_allclose(solution.sensitivity["s"], 0) +# np.testing.assert_allclose( +# solution.sensitivity["all"], +# np.hstack( +# [ +# solution.sensitivity["p"], +# solution.sensitivity["q"], +# solution.sensitivity["r"], +# solution.sensitivity["s"], +# ] +# ), +# ) +# np.testing.assert_allclose( +# solution["var times s"].data, 0.5 * (-1 + 0.2 * solution.t) +# ) +# np.testing.assert_allclose( +# solution["var times s"].sensitivity["p"], +# 0.5 * (2 * solution.t)[:, np.newaxis], +# ) +# np.testing.assert_allclose( +# solution["var times s"].sensitivity["q"], +# 0.5 * (0.1 * solution.t)[:, np.newaxis], +# ) +# np.testing.assert_allclose(solution["var times s"].sensitivity["r"], 0.5) +# np.testing.assert_allclose( +# solution["var times s"].sensitivity["s"], +# (-1 + 0.2 * solution.t)[:, np.newaxis], +# ) +# np.testing.assert_allclose( +# solution["var times s"].sensitivity["all"], +# np.hstack( +# [ +# solution["var times s"].sensitivity["p"], +# solution["var times s"].sensitivity["q"], +# solution["var times s"].sensitivity["r"], +# solution["var times s"].sensitivity["s"], +# ] +# ), +# ) + +# def test_solve_sensitivity_vector_var_scalar_input(self): +# var = pybamm.Variable("var", "negative electrode") +# model = pybamm.BaseModel() +# # Set length scales to avoid warning +# model.length_scales = {"negative electrode": 1} +# param = pybamm.InputParameter("param") +# model.rhs = {var: -param * var} +# model.initial_conditions = {var: 2} +# model.variables = {"var": var} + +# # create discretisation +# disc = get_discretisation_for_testing() +# disc.process_model(model) +# n = disc.mesh["negative electrode"].npts + +# # Solve - scalar input +# solver = pybamm.CasadiSolver(solve_sensitivity_equations=True) +# t_eval = np.linspace(0, 1) +# solution = solver.solve(model, t_eval, inputs={"param": 7}) +# np.testing.assert_array_almost_equal( +# solution["var"].data, np.tile(2 * np.exp(-7 * t_eval), (n, 1)), decimal=4, +# ) +# np.testing.assert_array_almost_equal( +# solution["var"].sensitivity["param"], +# np.repeat(-2 * t_eval * np.exp(-7 * t_eval), n)[:, np.newaxis], +# decimal=4, +# ) + +# # More complicated model +# # Create model +# model = pybamm.BaseModel() +# # Set length scales to avoid warning +# model.length_scales = {"negative electrode": 1} +# var = pybamm.Variable("var", "negative electrode") +# p = pybamm.InputParameter("p") +# q = pybamm.InputParameter("q") +# r = pybamm.InputParameter("r") +# s = pybamm.InputParameter("s") +# model.rhs = {var: p * q} +# model.initial_conditions = {var: r} +# model.variables = {"var times s": var * s} + +# # Discretise +# disc.process_model(model) + +# # Solve +# # Make sure that passing in extra options works +# solver = pybamm.CasadiSolver( +# rtol=1e-10, atol=1e-10, solve_sensitivity_equations=True +# ) +# t_eval = np.linspace(0, 1, 80) +# solution = solver.solve( +# model, t_eval, inputs={"p": 0.1, "q": 2, "r": -1, "s": 0.5} +# ) +# np.testing.assert_allclose(solution.y, np.tile(-1 + 0.2 * solution.t, (n, 1))) +# np.testing.assert_allclose( +# solution.sensitivity["p"], np.repeat(2 * solution.t, n)[:, np.newaxis], +# ) +# np.testing.assert_allclose( +# solution.sensitivity["q"], np.repeat(0.1 * solution.t, n)[:, np.newaxis], +# ) +# np.testing.assert_allclose(solution.sensitivity["r"], 1) +# np.testing.assert_allclose(solution.sensitivity["s"], 0) +# np.testing.assert_allclose( +# solution.sensitivity["all"], +# np.hstack( +# [ +# solution.sensitivity["p"], +# solution.sensitivity["q"], +# solution.sensitivity["r"], +# solution.sensitivity["s"], +# ] +# ), +# ) +# np.testing.assert_allclose( +# solution["var times s"].data, np.tile(0.5 * (-1 + 0.2 * solution.t), (n, 1)) +# ) +# np.testing.assert_allclose( +# solution["var times s"].sensitivity["p"], +# np.repeat(0.5 * (2 * solution.t), n)[:, np.newaxis], +# ) +# np.testing.assert_allclose( +# solution["var times s"].sensitivity["q"], +# np.repeat(0.5 * (0.1 * solution.t), n)[:, np.newaxis], +# ) +# np.testing.assert_allclose(solution["var times s"].sensitivity["r"], 0.5) +# np.testing.assert_allclose( +# solution["var times s"].sensitivity["s"], +# np.repeat(-1 + 0.2 * solution.t, n)[:, np.newaxis], +# ) +# np.testing.assert_allclose( +# solution["var times s"].sensitivity["all"], +# np.hstack( +# [ +# solution["var times s"].sensitivity["p"], +# solution["var times s"].sensitivity["q"], +# solution["var times s"].sensitivity["r"], +# solution["var times s"].sensitivity["s"], +# ] +# ), +# ) + +# def test_solve_sensitivity_scalar_var_vector_input(self): +# var = pybamm.Variable("var", "negative electrode") +# model = pybamm.BaseModel() +# # Set length scales to avoid warning +# model.length_scales = {"negative electrode": 1} + +# param = pybamm.InputParameter("param", "negative electrode") +# model.rhs = {var: -param * var} +# model.initial_conditions = {var: 2} +# model.variables = { +# "var": var, +# "integral of var": pybamm.Integral(var, pybamm.standard_spatial_vars.x_n), +# } + +# # create discretisation +# mesh = get_mesh_for_testing(xpts=5) +# spatial_methods = {"macroscale": pybamm.FiniteVolume()} +# disc = pybamm.Discretisation(mesh, spatial_methods) +# disc.process_model(model) +# n = disc.mesh["negative electrode"].npts + +# # Solve - constant input +# solver = pybamm.CasadiSolver( +# mode="fast", rtol=1e-10, atol=1e-10, solve_sensitivity_equations=True +# ) +# t_eval = np.linspace(0, 1) +# solution = solver.solve(model, t_eval, inputs={"param": 7 * np.ones(n)}) +# l_n = mesh["negative electrode"].edges[-1] +# np.testing.assert_array_almost_equal( +# solution["var"].data, np.tile(2 * np.exp(-7 * t_eval), (n, 1)), decimal=4, +# ) + +# np.testing.assert_array_almost_equal( +# solution["var"].sensitivity["param"], +# np.vstack([np.eye(n) * -2 * t * np.exp(-7 * t) for t in t_eval]), +# ) +# np.testing.assert_array_almost_equal( +# solution["integral of var"].data, 2 * np.exp(-7 * t_eval) * l_n, decimal=4, +# ) +# np.testing.assert_array_almost_equal( +# solution["integral of var"].sensitivity["param"], +# np.tile(-2 * t_eval * np.exp(-7 * t_eval) * l_n / n, (n, 1)).T, +# ) + +# # Solve - linspace input +# p_eval = np.linspace(1, 2, n) +# solution = solver.solve(model, t_eval, inputs={"param": p_eval}) +# l_n = mesh["negative electrode"].edges[-1] +# np.testing.assert_array_almost_equal( +# solution["var"].data, 2 * np.exp(-p_eval[:, np.newaxis] * t_eval), decimal=4 +# ) +# np.testing.assert_array_almost_equal( +# solution["var"].sensitivity["param"], +# np.vstack([np.diag(-2 * t * np.exp(-p_eval * t)) for t in t_eval]), +# ) + +# np.testing.assert_array_almost_equal( +# solution["integral of var"].data, +# np.sum( +# 2 +# * np.exp(-p_eval[:, np.newaxis] * t_eval) +# * mesh["negative electrode"].d_edges[:, np.newaxis], +# axis=0, +# ), +# ) +# np.testing.assert_array_almost_equal( +# solution["integral of var"].sensitivity["param"], +# np.vstack([-2 * t * np.exp(-p_eval * t) * l_n / n for t in t_eval]), +# ) +class TestCasadiSolverDAEsWithForwardSensitivityEquations(unittest.TestCase): def test_solve_sensitivity_scalar_var_scalar_input(self): # Create model model = pybamm.BaseModel() var = pybamm.Variable("var") + var2 = pybamm.Variable("var2") p = pybamm.InputParameter("p") model.rhs = {var: p * var} - model.initial_conditions = {var: 1} - model.variables = {"var squared": var ** 2} + model.algebraic = {var2: var2 - p} + model.initial_conditions = {var: 1, var2: p} + model.variables = {"prod": var * var2} # Solve # Make sure that passing in extra options works @@ -621,151 +896,62 @@ def test_solve_sensitivity_scalar_var_scalar_input(self): solution = solver.solve(model, t_eval, inputs={"p": 0.1}) np.testing.assert_array_equal(solution.t, t_eval) np.testing.assert_allclose(solution.y[0], np.exp(0.1 * solution.t)) + np.testing.assert_allclose(solution.y[1], 0.1) np.testing.assert_allclose( solution.sensitivity["p"], - (solution.t * np.exp(0.1 * solution.t))[:, np.newaxis], - ) - np.testing.assert_allclose( - solution["var squared"].data, np.exp(0.1 * solution.t) ** 2 - ) - np.testing.assert_allclose( - solution["var squared"].sensitivity["p"], - (2 * np.exp(0.1 * solution.t) * solution.t * np.exp(0.1 * solution.t))[ - :, np.newaxis - ], - ) - - # More complicated model - # Create model - model = pybamm.BaseModel() - var = pybamm.Variable("var") - p = pybamm.InputParameter("p") - q = pybamm.InputParameter("q") - r = pybamm.InputParameter("r") - s = pybamm.InputParameter("s") - model.rhs = {var: p * q} - model.initial_conditions = {var: r} - model.variables = {"var times s": var * s} - - # Solve - # Make sure that passing in extra options works - solver = pybamm.CasadiSolver( - rtol=1e-10, atol=1e-10, solve_sensitivity_equations=True - ) - t_eval = np.linspace(0, 1, 80) - solution = solver.solve( - model, t_eval, inputs={"p": 0.1, "q": 2, "r": -1, "s": 0.5} - ) - np.testing.assert_allclose(solution.y[0], -1 + 0.2 * solution.t) - np.testing.assert_allclose( - solution.sensitivity["p"], (2 * solution.t)[:, np.newaxis], - ) - np.testing.assert_allclose( - solution.sensitivity["q"], (0.1 * solution.t)[:, np.newaxis], - ) - np.testing.assert_allclose(solution.sensitivity["r"], 1) - np.testing.assert_allclose(solution.sensitivity["s"], 0) - np.testing.assert_allclose( - solution.sensitivity["all"], np.hstack( [ - solution.sensitivity["p"], - solution.sensitivity["q"], - solution.sensitivity["r"], - solution.sensitivity["s"], + (solution.t * np.exp(0.1 * solution.t))[:, np.newaxis], + np.ones((len(t_eval), 1)), ] - ), - ) - np.testing.assert_allclose( - solution["var times s"].data, 0.5 * (-1 + 0.2 * solution.t) - ) - np.testing.assert_allclose( - solution["var times s"].sensitivity["p"], - 0.5 * (2 * solution.t)[:, np.newaxis], + ).reshape(2 * len(t_eval), 1), ) np.testing.assert_allclose( - solution["var times s"].sensitivity["q"], - 0.5 * (0.1 * solution.t)[:, np.newaxis], + solution["prod"].data, 0.1 * np.exp(0.1 * solution.t) ) - np.testing.assert_allclose(solution["var times s"].sensitivity["r"], 0.5) np.testing.assert_allclose( - solution["var times s"].sensitivity["s"], - (-1 + 0.2 * solution.t)[:, np.newaxis], - ) - np.testing.assert_allclose( - solution["var times s"].sensitivity["all"], - np.hstack( - [ - solution["var times s"].sensitivity["p"], - solution["var times s"].sensitivity["q"], - solution["var times s"].sensitivity["r"], - solution["var times s"].sensitivity["s"], - ] - ), - ) - - def test_solve_sensitivity_vector_var_scalar_input(self): - var = pybamm.Variable("var", "negative electrode") - model = pybamm.BaseModel() - # Set length scales to avoid warning - model.length_scales = {"negative electrode": 1} - param = pybamm.InputParameter("param") - model.rhs = {var: -param * var} - model.initial_conditions = {var: 2} - model.variables = {"var": var} - - # create discretisation - disc = get_discretisation_for_testing() - disc.process_model(model) - n = disc.mesh["negative electrode"].npts - - # Solve - scalar input - solver = pybamm.CasadiSolver(solve_sensitivity_equations=True) - t_eval = np.linspace(0, 1) - solution = solver.solve(model, t_eval, inputs={"param": 7}) - np.testing.assert_array_almost_equal( - solution["var"].data, np.tile(2 * np.exp(-7 * t_eval), (n, 1)), decimal=4, - ) - np.testing.assert_array_almost_equal( - solution["var"].sensitivity["param"], - np.repeat(-2 * t_eval * np.exp(-7 * t_eval), n)[:, np.newaxis], - decimal=4, + solution["prod"].sensitivity["p"], + ((1 + 0.1 * solution.t) * np.exp(0.1 * solution.t))[:, np.newaxis], ) # More complicated model # Create model model = pybamm.BaseModel() - # Set length scales to avoid warning - model.length_scales = {"negative electrode": 1} - var = pybamm.Variable("var", "negative electrode") + var = pybamm.Variable("var") + var2 = pybamm.Variable("var2") p = pybamm.InputParameter("p") q = pybamm.InputParameter("q") r = pybamm.InputParameter("r") s = pybamm.InputParameter("s") - model.rhs = {var: p * q} - model.initial_conditions = {var: r} - model.variables = {"var times s": var * s} - - # Discretise - disc.process_model(model) + model.rhs = {var: p} + model.algebraic = {var2: var2 - q} + model.initial_conditions = {var: r, var2: q} + model.variables = {"var prod times s": var * var2 * s} # Solve # Make sure that passing in extra options works solver = pybamm.CasadiSolver( rtol=1e-10, atol=1e-10, solve_sensitivity_equations=True ) - t_eval = np.linspace(0, 1, 80) + t_eval = np.linspace(0, 1, 3) solution = solver.solve( model, t_eval, inputs={"p": 0.1, "q": 2, "r": -1, "s": 0.5} ) - np.testing.assert_allclose(solution.y, np.tile(-1 + 0.2 * solution.t, (n, 1))) + np.testing.assert_allclose(solution.y[0], -1 + 0.1 * solution.t) + np.testing.assert_allclose(solution.y[1], 2) + n_t = len(t_eval) + zeros = np.zeros((n_t, 1)) + ones = np.ones((n_t, 1)) np.testing.assert_allclose( - solution.sensitivity["p"], np.repeat(2 * solution.t, n)[:, np.newaxis], + solution.sensitivity["p"], + np.hstack([solution.t[:, np.newaxis], zeros]).reshape(2 * n_t, 1), + ) + np.testing.assert_allclose( + solution.sensitivity["q"], np.hstack([zeros, ones]).reshape(2 * n_t, 1), ) np.testing.assert_allclose( - solution.sensitivity["q"], np.repeat(0.1 * solution.t, n)[:, np.newaxis], + solution.sensitivity["r"], np.hstack([ones, zeros]).reshape(2 * n_t, 1) ) - np.testing.assert_allclose(solution.sensitivity["r"], 1) np.testing.assert_allclose(solution.sensitivity["s"], 0) np.testing.assert_allclose( solution.sensitivity["all"], @@ -779,102 +965,204 @@ def test_solve_sensitivity_vector_var_scalar_input(self): ), ) np.testing.assert_allclose( - solution["var times s"].data, np.tile(0.5 * (-1 + 0.2 * solution.t), (n, 1)) + solution["var prod times s"].data, 0.5 * 2 * (-1 + 0.1 * solution.t) ) np.testing.assert_allclose( - solution["var times s"].sensitivity["p"], - np.repeat(0.5 * (2 * solution.t), n)[:, np.newaxis], + solution["var prod times s"].sensitivity["p"], + 0.5 * (2 * solution.t)[:, np.newaxis], ) np.testing.assert_allclose( - solution["var times s"].sensitivity["q"], - np.repeat(0.5 * (0.1 * solution.t), n)[:, np.newaxis], + solution["var prod times s"].sensitivity["q"], + 0.5 * (-1 + 0.1 * solution.t)[:, np.newaxis], ) - np.testing.assert_allclose(solution["var times s"].sensitivity["r"], 0.5) + np.testing.assert_allclose(solution["var prod times s"].sensitivity["r"], 1) np.testing.assert_allclose( - solution["var times s"].sensitivity["s"], - np.repeat(-1 + 0.2 * solution.t, n)[:, np.newaxis], + solution["var prod times s"].sensitivity["s"], + 2 * (-1 + 0.1 * solution.t)[:, np.newaxis], ) np.testing.assert_allclose( - solution["var times s"].sensitivity["all"], + solution["var prod times s"].sensitivity["all"], np.hstack( [ - solution["var times s"].sensitivity["p"], - solution["var times s"].sensitivity["q"], - solution["var times s"].sensitivity["r"], - solution["var times s"].sensitivity["s"], + solution["var prod times s"].sensitivity["p"], + solution["var prod times s"].sensitivity["q"], + solution["var prod times s"].sensitivity["r"], + solution["var prod times s"].sensitivity["s"], ] ), ) - def test_solve_sensitivity_scalar_var_vector_input(self): - var = pybamm.Variable("var", "negative electrode") - model = pybamm.BaseModel() - # Set length scales to avoid warning - model.length_scales = {"negative electrode": 1} - - param = pybamm.InputParameter("param", "negative electrode") - model.rhs = {var: -param * var} - model.initial_conditions = {var: 2} - model.variables = { - "var": var, - "integral of var": pybamm.Integral(var, pybamm.standard_spatial_vars.x_n), - } - - # create discretisation - mesh = get_mesh_for_testing(xpts=5) - spatial_methods = {"macroscale": pybamm.FiniteVolume()} - disc = pybamm.Discretisation(mesh, spatial_methods) - disc.process_model(model) - n = disc.mesh["negative electrode"].npts - - # Solve - constant input - solver = pybamm.CasadiSolver( - mode="fast", rtol=1e-10, atol=1e-10, solve_sensitivity_equations=True - ) - t_eval = np.linspace(0, 1) - solution = solver.solve(model, t_eval, inputs={"param": 7 * np.ones(n)}) - l_n = mesh["negative electrode"].edges[-1] - np.testing.assert_array_almost_equal( - solution["var"].data, np.tile(2 * np.exp(-7 * t_eval), (n, 1)), decimal=4, - ) - - np.testing.assert_array_almost_equal( - solution["var"].sensitivity["param"], - np.vstack([np.eye(n) * -2 * t * np.exp(-7 * t) for t in t_eval]), - ) - np.testing.assert_array_almost_equal( - solution["integral of var"].data, 2 * np.exp(-7 * t_eval) * l_n, decimal=4, - ) - np.testing.assert_array_almost_equal( - solution["integral of var"].sensitivity["param"], - np.tile(-2 * t_eval * np.exp(-7 * t_eval) * l_n / n, (n, 1)).T, - ) - - # Solve - linspace input - p_eval = np.linspace(1, 2, n) - solution = solver.solve(model, t_eval, inputs={"param": p_eval}) - l_n = mesh["negative electrode"].edges[-1] - np.testing.assert_array_almost_equal( - solution["var"].data, 2 * np.exp(-p_eval[:, np.newaxis] * t_eval), decimal=4 - ) - np.testing.assert_array_almost_equal( - solution["var"].sensitivity["param"], - np.vstack([np.diag(-2 * t * np.exp(-p_eval * t)) for t in t_eval]), - ) - - np.testing.assert_array_almost_equal( - solution["integral of var"].data, - np.sum( - 2 - * np.exp(-p_eval[:, np.newaxis] * t_eval) - * mesh["negative electrode"].d_edges[:, np.newaxis], - axis=0, - ), - ) - np.testing.assert_array_almost_equal( - solution["integral of var"].sensitivity["param"], - np.vstack([-2 * t * np.exp(-p_eval * t) * l_n / n for t in t_eval]), - ) + # def test_solve_sensitivity_vector_var_scalar_input(self): + # var = pybamm.Variable("var", "negative electrode") + # model = pybamm.BaseModel() + # # Set length scales to avoid warning + # model.length_scales = {"negative electrode": 1} + # param = pybamm.InputParameter("param") + # model.rhs = {var: -param * var} + # model.initial_conditions = {var: 2} + # model.variables = {"var": var} + + # # create discretisation + # disc = get_discretisation_for_testing() + # disc.process_model(model) + # n = disc.mesh["negative electrode"].npts + + # # Solve - scalar input + # solver = pybamm.CasadiSolver(solve_sensitivity_equations=True) + # t_eval = np.linspace(0, 1) + # solution = solver.solve(model, t_eval, inputs={"param": 7}) + # np.testing.assert_array_almost_equal( + # solution["var"].data, np.tile(2 * np.exp(-7 * t_eval), (n, 1)), decimal=4, + # ) + # np.testing.assert_array_almost_equal( + # solution["var"].sensitivity["param"], + # np.repeat(-2 * t_eval * np.exp(-7 * t_eval), n)[:, np.newaxis], + # decimal=4, + # ) + + # # More complicated model + # # Create model + # model = pybamm.BaseModel() + # # Set length scales to avoid warning + # model.length_scales = {"negative electrode": 1} + # var = pybamm.Variable("var", "negative electrode") + # p = pybamm.InputParameter("p") + # q = pybamm.InputParameter("q") + # r = pybamm.InputParameter("r") + # s = pybamm.InputParameter("s") + # model.rhs = {var: p * q} + # model.initial_conditions = {var: r} + # model.variables = {"var times s": var * s} + + # # Discretise + # disc.process_model(model) + + # # Solve + # # Make sure that passing in extra options works + # solver = pybamm.CasadiSolver( + # rtol=1e-10, atol=1e-10, solve_sensitivity_equations=True + # ) + # t_eval = np.linspace(0, 1, 80) + # solution = solver.solve( + # model, t_eval, inputs={"p": 0.1, "q": 2, "r": -1, "s": 0.5} + # ) + # np.testing.assert_allclose(solution.y, np.tile(-1 + 0.2 * solution.t, (n, 1))) + # np.testing.assert_allclose( + # solution.sensitivity["p"], np.repeat(2 * solution.t, n)[:, np.newaxis], + # ) + # np.testing.assert_allclose( + # solution.sensitivity["q"], np.repeat(0.1 * solution.t, n)[:, np.newaxis], + # ) + # np.testing.assert_allclose(solution.sensitivity["r"], 1) + # np.testing.assert_allclose(solution.sensitivity["s"], 0) + # np.testing.assert_allclose( + # solution.sensitivity["all"], + # np.hstack( + # [ + # solution.sensitivity["p"], + # solution.sensitivity["q"], + # solution.sensitivity["r"], + # solution.sensitivity["s"], + # ] + # ), + # ) + # np.testing.assert_allclose( + # solution["var times s"].data, np.tile(0.5 * (-1 + 0.2 * solution.t), (n, 1)) + # ) + # np.testing.assert_allclose( + # solution["var times s"].sensitivity["p"], + # np.repeat(0.5 * (2 * solution.t), n)[:, np.newaxis], + # ) + # np.testing.assert_allclose( + # solution["var times s"].sensitivity["q"], + # np.repeat(0.5 * (0.1 * solution.t), n)[:, np.newaxis], + # ) + # np.testing.assert_allclose(solution["var times s"].sensitivity["r"], 0.5) + # np.testing.assert_allclose( + # solution["var times s"].sensitivity["s"], + # np.repeat(-1 + 0.2 * solution.t, n)[:, np.newaxis], + # ) + # np.testing.assert_allclose( + # solution["var times s"].sensitivity["all"], + # np.hstack( + # [ + # solution["var times s"].sensitivity["p"], + # solution["var times s"].sensitivity["q"], + # solution["var times s"].sensitivity["r"], + # solution["var times s"].sensitivity["s"], + # ] + # ), + # ) + + # def test_solve_sensitivity_scalar_var_vector_input(self): + # var = pybamm.Variable("var", "negative electrode") + # model = pybamm.BaseModel() + # # Set length scales to avoid warning + # model.length_scales = {"negative electrode": 1} + + # param = pybamm.InputParameter("param", "negative electrode") + # model.rhs = {var: -param * var} + # model.initial_conditions = {var: 2} + # model.variables = { + # "var": var, + # "integral of var": pybamm.Integral(var, pybamm.standard_spatial_vars.x_n), + # } + + # # create discretisation + # mesh = get_mesh_for_testing(xpts=5) + # spatial_methods = {"macroscale": pybamm.FiniteVolume()} + # disc = pybamm.Discretisation(mesh, spatial_methods) + # disc.process_model(model) + # n = disc.mesh["negative electrode"].npts + + # # Solve - constant input + # solver = pybamm.CasadiSolver( + # mode="fast", rtol=1e-10, atol=1e-10, solve_sensitivity_equations=True + # ) + # t_eval = np.linspace(0, 1) + # solution = solver.solve(model, t_eval, inputs={"param": 7 * np.ones(n)}) + # l_n = mesh["negative electrode"].edges[-1] + # np.testing.assert_array_almost_equal( + # solution["var"].data, np.tile(2 * np.exp(-7 * t_eval), (n, 1)), decimal=4, + # ) + + # np.testing.assert_array_almost_equal( + # solution["var"].sensitivity["param"], + # np.vstack([np.eye(n) * -2 * t * np.exp(-7 * t) for t in t_eval]), + # ) + # np.testing.assert_array_almost_equal( + # solution["integral of var"].data, 2 * np.exp(-7 * t_eval) * l_n, decimal=4, + # ) + # np.testing.assert_array_almost_equal( + # solution["integral of var"].sensitivity["param"], + # np.tile(-2 * t_eval * np.exp(-7 * t_eval) * l_n / n, (n, 1)).T, + # ) + + # # Solve - linspace input + # p_eval = np.linspace(1, 2, n) + # solution = solver.solve(model, t_eval, inputs={"param": p_eval}) + # l_n = mesh["negative electrode"].edges[-1] + # np.testing.assert_array_almost_equal( + # solution["var"].data, 2 * np.exp(-p_eval[:, np.newaxis] * t_eval), decimal=4 + # ) + # np.testing.assert_array_almost_equal( + # solution["var"].sensitivity["param"], + # np.vstack([np.diag(-2 * t * np.exp(-p_eval * t)) for t in t_eval]), + # ) + + # np.testing.assert_array_almost_equal( + # solution["integral of var"].data, + # np.sum( + # 2 + # * np.exp(-p_eval[:, np.newaxis] * t_eval) + # * mesh["negative electrode"].d_edges[:, np.newaxis], + # axis=0, + # ), + # ) + # np.testing.assert_array_almost_equal( + # solution["integral of var"].sensitivity["param"], + # np.vstack([-2 * t * np.exp(-p_eval * t) * l_n / n for t in t_eval]), + # ) if __name__ == "__main__": From b67a45724559ba1042a8cd0169cd219abf10cf8c Mon Sep 17 00:00:00 2001 From: Valentin Sulzer Date: Mon, 13 Jul 2020 08:15:25 -0400 Subject: [PATCH 06/73] #1100 working on examples --- examples/notebooks/DFN-sensitivity.ipynb | 204 +- pybamm/solvers/base_solver.py | 10 +- sens-test-pybamm.py | 63 + sens-test.py | 51 + test-sensitivities.ipynb | 604 +++++ tests/unit/test_solvers/test_casadi_solver.py | 1936 ++++++++--------- 6 files changed, 1718 insertions(+), 1150 deletions(-) create mode 100644 sens-test-pybamm.py create mode 100644 sens-test.py create mode 100644 test-sensitivities.ipynb diff --git a/examples/notebooks/DFN-sensitivity.ipynb b/examples/notebooks/DFN-sensitivity.ipynb index f5e989bd1c..4183f0ae77 100644 --- a/examples/notebooks/DFN-sensitivity.ipynb +++ b/examples/notebooks/DFN-sensitivity.ipynb @@ -42,11 +42,11 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 14, "metadata": {}, "outputs": [], "source": [ - "model = pybamm.lithium_ion.DFN()" + "model = pybamm.lithium_ion.SPMe()" ] }, { @@ -64,56 +64,33 @@ }, { "cell_type": "code", - "execution_count": 62, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "7.900401128126567" - ] - }, - "execution_count": 62, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "ce_ref = param[\"Typical electrolyte concentration [mol.m-3]\"]\n", - "csn_ref = param[\"Maximum concentration in negative electrode [mol.m-3]\"]\n", - "T_ref = param[\"Reference temperature [K]\"]\n", - "param.evaluate(param[\"Negative electrode exchange-current density [A.m-2]\"](ce_ref, 0.5 * csn_ref, T_ref))" - ] - }, - { - "cell_type": "code", - "execution_count": 122, + "execution_count": 15, "metadata": {}, "outputs": [], "source": [ "param = model.default_parameter_values\n", "# Get reference values for evaluating functions\n", - "c_e_ref = param[\"Typical electrolyte concentration [mol.m-3]\"]\n", + "ce_ref = param[\"Typical electrolyte concentration [mol.m-3]\"]\n", "csn_ref = param[\"Maximum concentration in negative electrode [mol.m-3]\"]\n", "csp_ref = param[\"Maximum concentration in positive electrode [mol.m-3]\"]\n", "T_ref = param[\"Reference temperature [K]\"]\n", "# Evaluate functions at reference values\n", "Dsn_ref = param[\"Negative electrode diffusivity [m2.s-1]\"](0.5, T_ref).evaluate()\n", "Dsp_ref = param[\"Positive electrode diffusivity [m2.s-1]\"](0.5, T_ref).evaluate()\n", - "De_ref = param[\"Electrolyte diffusivity [m2.s-1]\"](c_e_ref, T_ref).evaluate()\n", - "kappae_ref = param[\"Electrolyte conductivity [S.m-1]\"](c_e_ref, T_ref).evaluate()\n", + "De_ref = param[\"Electrolyte diffusivity [m2.s-1]\"](ce_ref, T_ref).evaluate()\n", + "kappae_ref = param[\"Electrolyte conductivity [S.m-1]\"](ce_ref, T_ref).evaluate()\n", "j0n_ref = param.evaluate(param[\"Negative electrode exchange-current density [A.m-2]\"](ce_ref, 0.5 * csn_ref, T_ref))\n", "j0p_ref = param.evaluate(param[\"Positive electrode exchange-current density [A.m-2]\"](ce_ref, 0.5 * csp_ref, T_ref))" ] }, { "cell_type": "code", - "execution_count": 123, + "execution_count": 16, "metadata": {}, "outputs": [], "source": [ "param[\"Negative electrode diffusivity [m2.s-1]\"] = Dsn_ref * pybamm.InputParameter(\"Dsn\")\n", - "# param[\"Positive electrode diffusivity [m2.s-1]\"] = Dsp_ref * pybamm.InputParameter(\"Dsp\")\n", + "param[\"Positive electrode diffusivity [m2.s-1]\"] = Dsp_ref * pybamm.InputParameter(\"Dsp\")\n", "# param[\"Electrolyte diffusivity [m2.s-1]\"] = De_ref * pybamm.InputParameter(\"D_e\")\n", "# param[\"Electrolyte conductivity [S.m-1]\"] = kappae_ref * pybamm.InputParameter(\"kappa_e\")\n", "# param[\"Negative electrode exchange-current density [A.m-2]\"] = j0n_ref * pybamm.InputParameter(\"j0n\")\n", @@ -129,13 +106,33 @@ }, { "cell_type": "code", - "execution_count": 124, + "execution_count": 19, "metadata": {}, "outputs": [], "source": [ - "solver = pybamm.CasadiSolver(extra_options_setup={\"ad_weight\": 0})\n", + "solver = pybamm.CasadiSolver(mode=\"fast\", solve_sensitivity_equations=True)\n", "sim = pybamm.Simulation(model, parameter_values=param, solver=solver)\n", - "solution = sim.solve(t_eval=np.linspace(0,3600,5))" + "solution = sim.solve(t_eval=np.linspace(0,3600), inputs={\"Dsn\": 1, \"Dsp\": 1})" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "1.7811190900000042" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "solution.solve_time" ] }, { @@ -147,16 +144,47 @@ }, { "cell_type": "code", - "execution_count": 125, + "execution_count": 17, + "metadata": {}, + "outputs": [], + "source": [ + "solver = pybamm.CasadiSolver(mode=\"fast\")\n", + "sim = pybamm.Simulation(model, parameter_values=param, solver=solver)\n", + "solution = sim.solve(t_eval=np.linspace(0,3600), inputs={\"Dsn\": 1, \"Dsp\": 1})" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0.005656635999997661" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "solution.solve_time" + ] + }, + { + "cell_type": "code", + "execution_count": 11, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "" + "" ] }, - "execution_count": 125, + "execution_count": 11, "metadata": {}, "output_type": "execute_result" } @@ -175,28 +203,30 @@ }, { "cell_type": "code", - "execution_count": 128, + "execution_count": 12, "metadata": {}, "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "CPU times: user 144 ms, sys: 2.01 ms, total: 146 ms\n", - "Wall time: 146 ms\n" - ] - }, { "data": { "text/plain": [ - "DM([[3.77171, 3.67082, 3.61315, 3.58406, 3.16798]])" + "{'all': DM([0, 0.000305216, 0.000323451, 0.000307878, 0.000281315, 0.000252542, 0.000226269, 0.00020523, 0.000191031, 0.000184763, 0.000187613, 0.000201625, 0.000230913, 0.000283916, 0.000377805, 0.000547086, 0.000859691, 0.00144399, 0.00252183, 0.00440237, 0.00728527, 0.0106768, 0.0129031, 0.0123404, 0.00953625, 0.00642767, 0.00416935, 0.00284452, 0.00214823, 0.00179002, 0.00157891, 0.00140318, 0.00120162, 0.000947612, 0.000645371, 0.0003332, 8.76979e-05, 2.15342e-05, 0.000267893, 0.000949333, 0.00213811, 0.00382395, 0.00590563, 0.00821168, 0.0105401, 0.0127, 0.0145411, 0.0159704, 0.0169657, 0.017614]),\n", + " 'Dsn': DM([0, 0.000305216, 0.000323451, 0.000307878, 0.000281315, 0.000252542, 0.000226269, 0.00020523, 0.000191031, 0.000184763, 0.000187613, 0.000201625, 0.000230913, 0.000283916, 0.000377805, 0.000547086, 0.000859691, 0.00144399, 0.00252183, 0.00440237, 0.00728527, 0.0106768, 0.0129031, 0.0123404, 0.00953625, 0.00642767, 0.00416935, 0.00284452, 0.00214823, 0.00179002, 0.00157891, 0.00140318, 0.00120162, 0.000947612, 0.000645371, 0.0003332, 8.76979e-05, 2.15342e-05, 0.000267893, 0.000949333, 0.00213811, 0.00382395, 0.00590563, 0.00821168, 0.0105401, 0.0127, 0.0145411, 0.0159704, 0.0169657, 0.017614])}" ] }, - "execution_count": 128, + "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], + "source": [ + "V.sensitivity" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "%%time\n", "V.value({\"Dsn\": 1, \"Dsp\": 1, \"D_e\": 1, \"kappa_e\": 1, \"j0n\": 1, \"j0p\": 1})" @@ -211,18 +241,9 @@ }, { "cell_type": "code", - "execution_count": 129, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "CPU times: user 672 ms, sys: 7.61 ms, total: 679 ms\n", - "Wall time: 675 ms\n" - ] - } - ], + "outputs": [], "source": [ "%%time\n", "sens = V.sensitivity({\"Dsn\": 1, \"Dsp\": 1, \"D_e\": 1, \"kappa_e\": 1, \"j0n\": 1, \"j0p\": 1})" @@ -230,44 +251,27 @@ }, { "cell_type": "code", - "execution_count": 109, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "DM(\n", - "[[00, 00], \n", - " [0.00144585, 0.00372077], \n", - " [0.00652415, 0.00146917], \n", - " [0.00246126, 0.00137044], \n", - " [0.0174035, 0.223563]])" - ] - }, - "execution_count": 109, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "sens" ] }, { "cell_type": "code", - "execution_count": 84, + "execution_count": 13, "metadata": {}, "outputs": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "[[0, 0.000422464, 0.000444994, 0.000428074, 0.000407487, 0.000399877, 0.000428391, 0.000542233, 0.000778531, 0.00103886, 0.00122177, 0.0013259, 0.00137903, 0.0014072, 0.00143188, 0.00147267, 0.00155152, 0.00170014, 0.00197684, 0.002514, 0.00366832, 0.00623007, 0.00951514, 0.0100351, 0.00790636, 0.00546389, 0.00381094, 0.00281181, 0.0021213, 0.00151904, 0.00102273, 0.00104159, 0.00145708, 0.00182547, 0.00207236, 0.00222589, 0.00231668, 0.0023674, 0.00239664, 0.00242538, 0.00249296, 0.00270719, 0.00340515, 0.00526309, 0.0081467, 0.0109712, 0.0132931, 0.015046, 0.0162617, 0.0170824]]\n", - "[[0.00485232, 0.00523816, 0.0052792, 0.0052831, 0.00527848, 0.00525259, 0.00517244, 0.00495439, 0.00451041, 0.00397247, 0.00352681, 0.00321071, 0.00300636, 0.00289322, 0.0028568, 0.00288818, 0.00298289, 0.00314032, 0.0033631, 0.00365425, 0.00399468, 0.00423495, 0.00415848, 0.0040753, 0.00417632, 0.00439649, 0.00459593, 0.0047219, 0.00481469, 0.00494027, 0.00510006, 0.00503174, 0.00469119, 0.00435765, 0.00410709, 0.00393143, 0.00381349, 0.00374063, 0.00370672, 0.00371226, 0.0037682, 0.00390221, 0.00414353, 0.00439765, 0.00447031, 0.00444454, 0.00441415, 0.00439328, 0.00437972, 0.00437309]]\n", - "[[0, 0.00526514, 0.00652778, 0.00677179, 0.00659185, 0.00625442, 0.00586091, 0.00544997, 0.00503892, 0.00463729, 0.00425102, 0.00388392, 0.00353832, 0.00321543, 0.00291568, 0.00263888, 0.00238442, 0.00215139, 0.0019387, 0.0017451, 0.00156931, 0.00141004, 0.00126599, 0.00113594, 0.00101868, 0.000913098, 0.000818128, 0.000732787, 0.000656173, 0.000587471, 0.000525967, 0.000471096, 0.000422519, 0.000380349, 0.000345679, 0.000321775, 0.000316733, 0.000348904, 0.000455642, 0.000698224, 0.00113785, 0.00178227, 0.00261286, 0.00374895, 0.00570581, 0.0101298, 0.0222322, 0.0525921, 0.111765, 0.220066]]\n", - "[[0.043932, 0.043935, 0.0439292, 0.0439253, 0.0439234, 0.0439229, 0.0439232, 0.0439241, 0.0439253, 0.0439268, 0.0439285, 0.0439302, 0.043932, 0.0439339, 0.0439358, 0.0439377, 0.0439396, 0.0439414, 0.0439433, 0.0439451, 0.0439469, 0.0439487, 0.0439505, 0.0439521, 0.0439537, 0.0439551, 0.0439564, 0.0439576, 0.0439587, 0.0439598, 0.0439607, 0.0439614, 0.0439618, 0.0439616, 0.0439601, 0.0439554, 0.0439433, 0.0439135, 0.0438452, 0.0437043, 0.0434649, 0.0431603, 0.0428923, 0.0427639, 0.0428367, 0.0431063, 0.0433412, 0.0432045, 0.0434168, 0.0437639]]\n", - "[[0, 0.00453626, 0.00576241, 0.00593085, 0.0059697, 0.00599664, 0.00603969, 0.00614157, 0.00635712, 0.00665648, 0.00696915, 0.00727134, 0.00756004, 0.00783543, 0.00809717, 0.00834377, 0.00857198, 0.00877537, 0.00894056, 0.00903652, 0.00898227, 0.00859883, 0.00788577, 0.00720723, 0.0066758, 0.00634508, 0.00619816, 0.00613976, 0.00608265, 0.00597241, 0.00584504, 0.00594164, 0.00625905, 0.00659515, 0.00691037, 0.00721551, 0.00753136, 0.00789289, 0.00836534, 0.00905537, 0.0100645, 0.0113496, 0.0126403, 0.01357, 0.0139674, 0.0136313, 0.0119871, 0.00958296, 0.00846165, 0.00808178]]\n", - "[[0.00744968, 0.00706049, 0.00707755, 0.00710189, 0.00712197, 0.00715069, 0.00721447, 0.00738035, 0.00773676, 0.0082317, 0.00874522, 0.00923704, 0.00970383, 0.0101478, 0.0105697, 0.0109681, 0.0113379, 0.0116681, 0.0119346, 0.0120797, 0.0119521, 0.0112178, 0.00990156, 0.00874322, 0.00791797, 0.00744991, 0.00728365, 0.00725226, 0.00720922, 0.00706729, 0.00688395, 0.00704026, 0.00754735, 0.00808927, 0.00859748, 0.00908629, 0.00958379, 0.0101323, 0.0108059, 0.0117219, 0.0129838, 0.0145132, 0.015934, 0.0167318, 0.0167981, 0.0161305, 0.0141787, 0.0114777, 0.0101689, 0.00975205]]\n" + "ename": "AttributeError", + "evalue": "'ProcessedVariable' object has no attribute 'symbolic_inputs_dict'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mAttributeError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0minputs\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m{\u001b[0m\u001b[0mk\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;36m1\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mk\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mV\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msymbolic_inputs_dict\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mkeys\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m}\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 2\u001b[0m \u001b[0mh\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;36m1e-6\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 3\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mk\u001b[0m \u001b[0;32min\u001b[0m \u001b[0minputs\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mkeys\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 4\u001b[0m \u001b[0mV_down\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mV\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mvalue\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0minputs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 5\u001b[0m \u001b[0minputs\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mk\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;36m1\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0mh\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mAttributeError\u001b[0m: 'ProcessedVariable' object has no attribute 'symbolic_inputs_dict'" ] } ], @@ -285,31 +289,9 @@ }, { "cell_type": "code", - "execution_count": 82, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'Dsn': 1,\n", - " 'j0n': 1,\n", - " 'Dsp': 1,\n", - " 'j0p': 1,\n", - " 'D_e': 1,\n", - " 'kappa_e': 1,\n", - " 0: 1,\n", - " 1: 1,\n", - " 2: 1,\n", - " 3: 1,\n", - " 4: 1,\n", - " 5: 1}" - ] - }, - "execution_count": 82, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "inputs" ] diff --git a/pybamm/solvers/base_solver.py b/pybamm/solvers/base_solver.py index eea850baf4..06ad0e02d7 100644 --- a/pybamm/solvers/base_solver.py +++ b/pybamm/solvers/base_solver.py @@ -299,7 +299,7 @@ def report(string): else: df_dz = casadi.jacobian(func, y_alg) S_z_mat = S_z.reshape( - (model.len_rhs, p_casadi_stacked.shape[0]) + (model.len_alg, p_casadi_stacked.shape[0]) ) S_rhs = (df_dx @ S_x_mat + df_dz @ S_z_mat + df_dp).reshape( (-1, 1) @@ -312,7 +312,7 @@ def report(string): dg_dz = casadi.jacobian(func, y_alg) dg_dp = casadi.jacobian(func, p_casadi_stacked) S_z_mat = S_z.reshape( - (model.len_rhs, p_casadi_stacked.shape[0]) + (model.len_alg, p_casadi_stacked.shape[0]) ) if model.len_rhs == 0: S_alg = (dg_dz @ S_z_mat + dg_dp).reshape((-1, 1)) @@ -930,6 +930,12 @@ def _set_up_ext_and_inputs(self, model, external_variables, inputs): for input_param in model.input_parameters: name = input_param.name if name not in inputs: + # Don't allow symbolic inputs if using `solve_sensitivity_equations` + if self.solve_sensitivity_equations is True: + raise pybamm.SolverError( + "Cannot have symbolic inputs if explicitly solving forward" + "sensitivity equations" + ) # Only allow symbolic inputs for CasadiSolver and CasadiAlgebraicSolver if not isinstance( self, (pybamm.CasadiSolver, pybamm.CasadiAlgebraicSolver) diff --git a/sens-test-pybamm.py b/sens-test-pybamm.py new file mode 100644 index 0000000000..ca80df53eb --- /dev/null +++ b/sens-test-pybamm.py @@ -0,0 +1,63 @@ +import pybamm +import casadi + +model = pybamm.lithium_ion.SPMe() +param = model.default_parameter_values +param["Negative electrode porosity"] = 0.3 +param["Separator porosity"] = 0.3 +param["Positive electrode porosity"] = 0.3 +param["Cation transference number"] = pybamm.InputParameter("t") + +solver = pybamm.CasadiSolver(mode="fast") # , solve_sensitivity_equations=True) +sim = pybamm.Simulation(model, parameter_values=param, solver=solver) +sol = sim.solve([0, 3600], inputs={"t": 0.5}) + +# print(sol["X-averaged electrolyte concentration"].data) +var = sol["Terminal voltage [V]"] + +t = casadi.MX.sym("t") +y = casadi.MX.sym("y", sim.built_model.len_rhs) +p = casadi.MX.sym("p") + +rhs = sim.built_model.casadi_rhs(t, y, p) + +jac_x_func = casadi.Function("jac_x", [t, y, p], [casadi.jacobian(rhs, y)]) +jac_p_func = casadi.Function("jac_x", [t, y, p], [casadi.jacobian(rhs, p)]) +for idx in range(len(sol.t)): + t = sol.t[idx] + u = sol.y[:, idx] + inp = 0.5 + next_jac_x_eval = jac_x_func(t, u, inp) + next_jac_p_eval = jac_p_func(t, u, inp) + if idx == 0: + jac_x_eval = next_jac_x_eval + jac_p_eval = next_jac_p_eval + else: + jac_x_eval = casadi.diagcat(jac_x_eval, next_jac_x_eval) + jac_p_eval = casadi.diagcat(jac_p_eval, next_jac_p_eval) + +# Convert variable to casadi format for differentiating +# var_casadi = self.base_variable.to_casadi(t_casadi, y_casadi, inputs=p_casadi) +# dvar_dy = casadi.jacobian(var_casadi, y_casadi) +# dvar_dp = casadi.jacobian(var_casadi, p_casadi_stacked) + +# # Convert to functions and evaluate index-by-index +# dvar_dy_func = casadi.Function( +# "dvar_dy", [t_casadi, y_casadi, p_casadi_stacked], [dvar_dy] +# ) +# dvar_dp_func = casadi.Function( +# "dvar_dp", [t_casadi, y_casadi, p_casadi_stacked], [dvar_dp] +# ) +# for idx in range(len(self.t_sol)): +# t = self.t_sol[idx] +# u = self.u_sol[:, idx] +# inp = inputs_stacked[:, idx] +# next_dvar_dy_eval = dvar_dy_func(t, u, inp) +# next_dvar_dp_eval = dvar_dp_func(t, u, inp) +# if idx == 0: +# dvar_dy_eval = next_dvar_dy_eval +# dvar_dp_eval = next_dvar_dp_eval +# else: +# dvar_dy_eval = casadi.diagcat(dvar_dy_eval, next_dvar_dy_eval) +# dvar_dp_eval = casadi.vertcat(dvar_dp_eval, next_dvar_dp_eval) + diff --git a/sens-test.py b/sens-test.py new file mode 100644 index 0000000000..8c0eba8620 --- /dev/null +++ b/sens-test.py @@ -0,0 +1,51 @@ +import casadi +import numpy as np +import matplotlib.pyplot as plt + +t = casadi.MX.sym("t") +x = casadi.MX.sym("x") +ode = -x + +x0 = 1 +t_eval = np.linspace(0, 10, 20) + +sol_exact = np.exp(-t_eval) + +# Casadi +opts = {"grid": t_eval, "output_t0": True} +itg = casadi.integrator("F", "cvodes", {"t": t, "x": x, "ode": ode}, opts) +sol_casadi = itg(x0=x0)["xf"].full().flatten() + +# Forward Euler +ode_fn = casadi.Function("ode", [t, x], [ode]) +sol_fwd = [x0] +x = x0 +for i in range(len(t_eval) - 1): + dt = t_eval[i + 1] - t_eval[i] + step = dt * ode_fn(t_eval[i], x) + x += step + sol_fwd.append(x) + +# Backward Euler +sol_back = [x0] +x = x0 +for i in range(len(t_eval) - 1): + dt = t_eval[i + 1] - t_eval[i] + x = x / (1 + dt) + sol_back.append(x) + +# Crank-Nicolson +sol_CN = [x0] +x = x0 +for i in range(len(t_eval) - 1): + dt = t_eval[i + 1] - t_eval[i] + x = (1 - dt / 2) * x / (1 + dt / 2) + sol_CN.append(x) + +plt.plot(t_eval, sol_exact, "o", label="exact") +plt.plot(t_eval, sol_casadi, label="casadi") +plt.plot(t_eval, sol_fwd, label="fwd") +plt.plot(t_eval, sol_back, label="back") +plt.plot(t_eval, sol_CN, label="CN") +plt.legend() +plt.show() diff --git a/test-sensitivities.ipynb b/test-sensitivities.ipynb new file mode 100644 index 0000000000..dabfdbc103 --- /dev/null +++ b/test-sensitivities.ipynb @@ -0,0 +1,604 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import pybamm\n", + "import casadi\n", + "import numpy as np\n", + "from scipy.sparse import eye, linalg, csr_matrix" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "model = pybamm.lithium_ion.DFN()\n", + "param = model.default_parameter_values\n", + "param[\"Negative electrode porosity\"] = 0.3\n", + "param[\"Separator porosity\"] = 0.3\n", + "param[\"Positive electrode porosity\"] = 0.3\n", + "param[\"Cation transference number\"] = pybamm.InputParameter(\"t\")\n", + "\n", + "# param = pybamm.ParameterValues({})\n", + "# model = pybamm.BaseModel()\n", + "# v = pybamm.Variable(\"v\")\n", + "# t = pybamm.InputParameter(\"t\")\n", + "# model.rhs = {v: t}\n", + "# model.initial_conditions = {v: 1}\n", + "# model.variables = {\"Terminal voltage [V]\": v}\n", + "\n", + "solver = pybamm.CasadiSolver(mode=\"fast\") # , solve_sensitivity_equations=True)\n", + "sim = pybamm.Simulation(model, parameter_values=param, solver=solver)\n", + "t_eval = np.linspace(0,3600,50)\n", + "sol = sim.solve(t_eval, inputs={\"t\": 0.5})\n", + "\n", + "# print(sol[\"X-averaged electrolyte concentration\"].data)\n", + "var = sol[\"Terminal voltage [V]\"]\n", + "\n", + "t = casadi.MX.sym(\"t\")\n", + "y = casadi.MX.sym(\"y\", sim.built_model.len_rhs_and_alg)\n", + "p = casadi.MX.sym(\"p\")\n", + "\n", + "rhs = casadi.vertcat(\n", + " sim.built_model.casadi_rhs(t, y, p),\n", + " sim.built_model.casadi_algebraic(t, y, p),\n", + ")\n", + "\n", + "jac_x_func = casadi.Function(\"jac_x\", [t, y, p], [casadi.jacobian(rhs, y)])\n", + "jac_p_func = casadi.Function(\"jac_x\", [t, y, p], [casadi.jacobian(rhs, p)])" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "# sim_with_sens = pybamm.Simulation(model, parameter_values=param, \n", + "# # solver=pybamm.CasadiSolver(mode=\"fast\", solve_sensitivity_equations=True)\n", + "# solver=pybamm.CasadiSolver(mode=\"fast\", solve_sensitivity_equations=True)\n", + "# )\n", + "# sol_with_sens = sim_with_sens.solve(\n", + "# np.linspace(0,3600,50), \n", + "# inputs={\"t\": 0.5}, \n", + "# )" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0.10660402900000054" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sol.solve_time" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "# sol_with_sens.solve_time" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "inp = 0.5\n", + "x0 = sim.built_model.init_eval(p)\n", + "S_0 = casadi.Function(\"S_0\", [p], [casadi.jacobian(x0,p)])(inp)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 170 ms, sys: 21.4 ms, total: 191 ms\n", + "Wall time: 190 ms\n" + ] + } + ], + "source": [ + "%%time\n", + "n = sim.built_model.len_rhs_and_alg\n", + "for idx in range(len(sol.t)):\n", + " ti = sol.t[idx]\n", + " ui = sol.y[:, idx]\n", + " next_jac_x_eval = jac_x_func(ti, ui, inp)\n", + " next_jac_p_eval = jac_p_func(ti, ui, inp)\n", + " if idx == 0:\n", + " jac_x_eval = next_jac_x_eval\n", + " jac_p_eval = next_jac_p_eval\n", + " else:\n", + " jac_x_eval = casadi.diagcat(jac_x_eval, next_jac_x_eval)\n", + " jac_p_eval = casadi.vertcat(jac_p_eval, next_jac_p_eval)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(68050, 68050)" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "jac_x_eval.shape" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(68050, 1)" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "jac_p_eval.shape" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 301 µs, sys: 6 µs, total: 307 µs\n", + "Wall time: 312 µs\n" + ] + }, + { + "data": { + "text/plain": [ + "DM(sparse: 1361-by-1361, 4868 nnz\n", + " (1, 1) -> -31728.4\n", + " (2, 1) -> 3525.38\n", + " (1, 2) -> 31728.4\n", + " ...\n", + " (1300, 1360) -> -1.21814\n", + " (1359, 1360) -> -1670.37\n", + " (1360, 1360) -> 1671.59)" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%time\n", + "i=0\n", + "jac_x_eval[n*(i+1):n*(i+2),n*(i+1):n*(i+2)]" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 42.1 s, sys: 6.11 s, total: 48.2 s\n", + "Wall time: 6.39 s\n" + ] + } + ], + "source": [ + "%%time\n", + "# Solve for sensitivities symbolically\n", + "# Forward Euler\n", + "# Sx_all = Sx_0.full()\n", + "# S_x = Sx_0\n", + "# n = sim.built_model.len_rhs\n", + "# for i in range(len(sol.t)-1):\n", + "# dt = sol.t[i+1] - sol.t[i]\n", + "# S_x = dt * S_x + jac_x_eval[n*i:n*(i+1),n*i:n*(i+1)] @ S_x + jac_p_eval[n*i:n*(i+1)]\n", + "# Sx_all = np.hstack([Sx_all, S_x.full()])\n", + "# Backward Euler\n", + "# Sx_all = Sx_0.full()\n", + "# S_x = Sx_0\n", + "# n = sim.built_model.len_rhs\n", + "# for i in range(len(sol.t)-1):\n", + "# dt = sol.t[i+1] - sol.t[i]\n", + "# A = np.eye(n) - dt * jac_x_eval[n*(i+1):n*(i+2),n*(i+1):n*(i+2)]\n", + "# b = dt * jac_p_eval[n*(i+1):n*(i+2)] + S_x\n", + "# S_x = np.linalg.solve(A,b)\n", + "# Sx_all = np.hstack([Sx_all, S_x])\n", + "# Crank-Nicolson\n", + "Sx_all = S_0.full()\n", + "S_x = S_0\n", + "\n", + "timer = pybamm.Timer()\n", + "I = casadi.DM.eye(n)\n", + "I2 = np.eye(n)\n", + "# jxf = jac_x_eval.full()\n", + "for i in range(len(sol.t)-1):\n", + "# print(1, timer.time())\n", + "# timer.reset()\n", + " dt = sol.t[i+1] - sol.t[i]\n", + "# print(2, timer.time())\n", + "# timer.reset()\n", + " A = (\n", + "# I2 - dt / 2 * jac_x_eval[n*(i+1):n*(i+2),n*(i+1):n*(i+2)].full()\n", + " I - dt / 2 * jac_x_eval[n*(i+1):n*(i+2),n*(i+1):n*(i+2)]\n", + " ).full()\n", + "# print(3, timer.time())\n", + "# timer.reset()\n", + " b = (\n", + " dt / 2 * (jac_p_eval[n*i:n*(i+1)] + jac_p_eval[n*(i+1):n*(i+2)])\n", + " + (I + dt / 2 * jac_x_eval[n*i:n*(i+1),n*i:n*(i+1)]) @ S_x\n", + " ).full()\n", + "# print(4, timer.time())\n", + "# timer.reset()\n", + " S_x = np.linalg.solve(A,b)\n", + "# print(5, timer.time())\n", + "# timer.reset()\n", + " Sx_all = np.hstack([Sx_all, S_x])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Solve with casadi integrator" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [], + "source": [ + "S_x = casadi.SX.sym(\"S_x\", n)\n", + "ode = jac_" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "DM(sparse: 1361-by-1361, 4868 nnz\n", + " (1, 1) -> -31728.4\n", + " (2, 1) -> 3525.38\n", + " (1, 2) -> 31728.4\n", + " ...\n", + " (1300, 1360) -> -1.30835\n", + " (1359, 1360) -> -1643.65\n", + " (1360, 1360) -> 1644.96)" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "next_jac_x_eval" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "%%time\n", + "np.linalg.solve(A,b)\n", + "b.shape" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "jac_x_eval[n*(i+1):n*(i+2),n*(i+1):n*(i+2)]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": false + }, + "outputs": [], + "source": [ + "Sx_all[:,1][61:] / (sol_with_sens.sensitivity[\"t\"][121:242])[61:].T" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Sx_all[:,1][61:] - (sol_with_sens.sensitivity[\"t\"][121:242])[61:].T" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%time\n", + "# Convert variable to casadi format for differentiating\n", + "var_casadi = var.base_variable.to_casadi(t, y, inputs={\"t\": p})\n", + "dvar_dy = casadi.jacobian(var_casadi, y)\n", + "dvar_dp = casadi.jacobian(var_casadi, p)\n", + "\n", + "# Convert to functions and evaluate index-by-index\n", + "dvar_dy_func = casadi.Function(\n", + " \"dvar_dy\", [t, y, p], [dvar_dy]\n", + ")\n", + "dvar_dp_func = casadi.Function(\n", + " \"dvar_dp\", [t, y, p], [dvar_dp]\n", + ")\n", + "for idx in range(len(var.t_sol)):\n", + " ti = var.t_sol[idx]\n", + " ui = var.u_sol[:, idx]\n", + " next_dvar_dy_eval = dvar_dy_func(ti, ui, inp)\n", + " next_dvar_dp_eval = dvar_dp_func(ti, ui, inp)\n", + " if idx == 0:\n", + " dvar_dy_eval = next_dvar_dy_eval\n", + " dvar_dp_eval = next_dvar_dp_eval\n", + " else:\n", + " dvar_dy_eval = casadi.vertcat(dvar_dy_eval, next_dvar_dy_eval)\n", + " dvar_dp_eval = casadi.vertcat(dvar_dp_eval, next_dvar_dp_eval)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "S_var = dvar_dy_eval @ Sx_all + dvar_dp_eval" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "dvar_dy_eval.shape" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sol_with_sens[\"Terminal voltage [V]\"].sensitivity[\"all\"].shape" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "np.diag(S_var - sol_with_sens[\"Terminal voltage [V]\"].sensitivity[\"all\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Finite difference for comparison" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 227 ms, sys: 6.25 ms, total: 233 ms\n", + "Wall time: 231 ms\n" + ] + } + ], + "source": [ + "%%time\n", + "h = 1e-8\n", + "sol_fd = (\n", + " sim.solve(t_eval, inputs={\"t\": 0.5+h})[\"Terminal voltage [V]\"].data\n", + " - sim.solve(t_eval, inputs={\"t\": 0.5})[\"Terminal voltage [V]\"].data\n", + ") / h" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sol_fd- sol_with_sens[\"Terminal voltage [V]\"].sensitivity[\"all\"]" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 122 ms, sys: 6.74 ms, total: 129 ms\n", + "Wall time: 127 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%time\n", + "sim.solve(t_eval, inputs={\"t\": 0.5})" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [], + "source": [ + "from scipy.interpolate import interp1d" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 165 ms, sys: 99.4 ms, total: 264 ms\n", + "Wall time: 263 ms\n" + ] + } + ], + "source": [ + "%%time\n", + "n = sim.built_model.len_rhs_and_alg\n", + "for idx in range(len(sol.t)):\n", + " ti = sol.t[idx]\n", + " ui = sol.y[:, idx]\n", + " next_jac_x_eval = jac_x_func(ti, ui, inp)\n", + " next_jac_p_eval = jac_p_func(ti, ui, inp)\n", + " if idx == 0:\n", + " jac_x_eval = next_jac_x_eval\n", + " jac_p_eval = next_jac_p_eval\n", + " else:\n", + " jac_x_eval = casadi.diagcat(jac_x_eval, next_jac_x_eval)\n", + " jac_p_eval = casadi.vertcat(jac_p_eval, next_jac_p_eval)" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "DM([3.00095, -3.09973, 0.730214, 0.427567, 2.73288])" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "A = casadi.DM(np.random.rand(5,5))\n", + "b = casadi.DM([1,2,3,4,5])\n", + "casadi.solve(A,b)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.7" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/tests/unit/test_solvers/test_casadi_solver.py b/tests/unit/test_solvers/test_casadi_solver.py index 2185ca108b..5646fa7736 100644 --- a/tests/unit/test_solvers/test_casadi_solver.py +++ b/tests/unit/test_solvers/test_casadi_solver.py @@ -9,872 +9,874 @@ from scipy.optimize import least_squares -# class TestCasadiSolver(unittest.TestCase): -# def test_bad_mode(self): -# with self.assertRaisesRegex(ValueError, "invalid mode"): -# pybamm.CasadiSolver(mode="bad mode") - -# def test_model_solver(self): -# # Create model -# model = pybamm.BaseModel() -# var = pybamm.Variable("var") -# model.rhs = {var: 0.1 * var} -# model.initial_conditions = {var: 1} -# # No need to set parameters; can use base discretisation (no spatial operators) - -# # create discretisation -# disc = pybamm.Discretisation() -# model_disc = disc.process_model(model, inplace=False) -# # Solve -# solver = pybamm.CasadiSolver(mode="fast", rtol=1e-8, atol=1e-8) -# t_eval = np.linspace(0, 1, 100) -# solution = solver.solve(model_disc, t_eval) -# np.testing.assert_array_equal(solution.t, t_eval) -# np.testing.assert_array_almost_equal( -# solution.y[0], np.exp(0.1 * solution.t), decimal=5 -# ) - -# # Safe mode (enforce events that won't be triggered) -# model.events = [pybamm.Event("an event", var + 1)] -# disc.process_model(model) -# solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) -# t_eval = np.linspace(0, 1, 100) -# solution = solver.solve(model, t_eval) -# np.testing.assert_array_equal(solution.t, t_eval) -# np.testing.assert_array_almost_equal( -# solution.y[0], np.exp(0.1 * solution.t), decimal=5 -# ) - -# def test_model_solver_python(self): -# # Create model -# pybamm.set_logging_level("ERROR") -# model = pybamm.BaseModel() -# model.convert_to_format = "python" -# var = pybamm.Variable("var") -# model.rhs = {var: 0.1 * var} -# model.initial_conditions = {var: 1} -# # No need to set parameters; can use base discretisation (no spatial operators) - -# # create discretisation -# disc = pybamm.Discretisation() -# disc.process_model(model) -# # Solve -# solver = pybamm.CasadiSolver(mode="fast", rtol=1e-8, atol=1e-8) -# t_eval = np.linspace(0, 1, 100) -# solution = solver.solve(model, t_eval) -# np.testing.assert_array_equal(solution.t, t_eval) -# np.testing.assert_array_almost_equal( -# solution.y[0], np.exp(0.1 * solution.t), decimal=5 -# ) -# pybamm.set_logging_level("WARNING") - -# def test_model_solver_failure(self): -# # Create model -# model = pybamm.BaseModel() -# var = pybamm.Variable("var") -# model.rhs = {var: -pybamm.sqrt(var)} -# model.initial_conditions = {var: 1} -# # add events so that safe mode is used (won't be triggered) -# model.events = [pybamm.Event("10", var - 10)] -# # No need to set parameters; can use base discretisation (no spatial operators) - -# # create discretisation -# disc = pybamm.Discretisation() -# model_disc = disc.process_model(model, inplace=False) - -# solver = pybamm.CasadiSolver(extra_options_call={"regularity_check": False}) -# # Solve with failure at t=2 -# t_eval = np.linspace(0, 20, 100) -# with self.assertRaises(pybamm.SolverError): -# solver.solve(model_disc, t_eval) -# # Solve with failure at t=0 -# model.initial_conditions = {var: 0} -# model_disc = disc.process_model(model, inplace=False) -# t_eval = np.linspace(0, 20, 100) -# with self.assertRaises(pybamm.SolverError): -# solver.solve(model_disc, t_eval) - -# def test_model_solver_events(self): -# # Create model -# model = pybamm.BaseModel() -# whole_cell = ["negative electrode", "separator", "positive electrode"] -# var1 = pybamm.Variable("var1", domain=whole_cell) -# var2 = pybamm.Variable("var2", domain=whole_cell) -# model.rhs = {var1: 0.1 * var1} -# model.algebraic = {var2: 2 * var1 - var2} -# model.initial_conditions = {var1: 1, var2: 2} -# model.events = [ -# pybamm.Event("var1 = 1.5", pybamm.min(var1 - 1.5)), -# pybamm.Event("var2 = 2.5", pybamm.min(var2 - 2.5)), -# ] -# disc = get_discretisation_for_testing() -# disc.process_model(model) - -# # Solve using "safe" mode -# solver = pybamm.CasadiSolver(mode="safe", rtol=1e-8, atol=1e-8) -# t_eval = np.linspace(0, 5, 100) -# solution = solver.solve(model, t_eval) -# np.testing.assert_array_less(solution.y[0], 1.5) -# np.testing.assert_array_less(solution.y[-1], 2.5 + 1e-10) -# np.testing.assert_array_almost_equal( -# solution.y[0], np.exp(0.1 * solution.t), decimal=5 -# ) -# np.testing.assert_array_almost_equal( -# solution.y[-1], 2 * np.exp(0.1 * solution.t), decimal=5 -# ) - -# # Solve using "safe" mode with debug off -# pybamm.settings.debug_mode = False -# solver = pybamm.CasadiSolver(mode="safe", rtol=1e-8, atol=1e-8, dt_max=1) -# t_eval = np.linspace(0, 5, 100) -# solution = solver.solve(model, t_eval) -# np.testing.assert_array_less(solution.y[0], 1.5) -# np.testing.assert_array_less(solution.y[-1], 2.5 + 1e-10) -# # test the last entry is exactly 2.5 -# np.testing.assert_array_almost_equal(solution.y[-1, -1], 2.5, decimal=2) -# np.testing.assert_array_almost_equal( -# solution.y[0], np.exp(0.1 * solution.t), decimal=5 -# ) -# np.testing.assert_array_almost_equal( -# solution.y[-1], 2 * np.exp(0.1 * solution.t), decimal=5 -# ) -# pybamm.settings.debug_mode = True - -# # Try dt_max=0 to enforce using all timesteps -# solver = pybamm.CasadiSolver(dt_max=0, rtol=1e-8, atol=1e-8) -# t_eval = np.linspace(0, 5, 100) -# solution = solver.solve(model, t_eval) -# np.testing.assert_array_less(solution.y[0], 1.5) -# np.testing.assert_array_less(solution.y[-1], 2.5) -# np.testing.assert_array_almost_equal( -# solution.y[0], np.exp(0.1 * solution.t), decimal=5 -# ) -# np.testing.assert_array_almost_equal( -# solution.y[-1], 2 * np.exp(0.1 * solution.t), decimal=5 -# ) - -# # Test when an event returns nan -# model = pybamm.BaseModel() -# var = pybamm.Variable("var") -# model.rhs = {var: 0.1 * var} -# model.initial_conditions = {var: 1} -# model.events = [ -# pybamm.Event("event", var - 1.02), -# pybamm.Event("sqrt event", pybamm.sqrt(1.0199 - var)), -# ] -# disc = pybamm.Discretisation() -# disc.process_model(model) -# solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) -# solution = solver.solve(model, t_eval) -# np.testing.assert_array_less(solution.y[0], 1.02 + 1e-10) -# np.testing.assert_array_almost_equal(solution.y[0, -1], 1.02, decimal=2) - -# def test_model_step(self): -# # Create model -# model = pybamm.BaseModel() -# domain = ["negative electrode", "separator", "positive electrode"] -# var = pybamm.Variable("var", domain=domain) -# model.rhs = {var: 0.1 * var} -# model.initial_conditions = {var: 1} -# # No need to set parameters; can use base discretisation (no spatial operators) - -# # create discretisation -# mesh = get_mesh_for_testing() -# spatial_methods = {"macroscale": pybamm.FiniteVolume()} -# disc = pybamm.Discretisation(mesh, spatial_methods) -# disc.process_model(model) - -# solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) - -# # Step once -# dt = 1 -# step_sol = solver.step(None, model, dt) -# np.testing.assert_array_equal(step_sol.t, [0, dt]) -# np.testing.assert_array_almost_equal(step_sol.y[0], np.exp(0.1 * step_sol.t)) - -# # Step again (return 5 points) -# step_sol_2 = solver.step(step_sol, model, dt, npts=5) -# np.testing.assert_array_equal( -# step_sol_2.t, np.concatenate([np.array([0]), np.linspace(dt, 2 * dt, 5)]) -# ) -# np.testing.assert_array_almost_equal( -# step_sol_2.y[0], np.exp(0.1 * step_sol_2.t) -# ) - -# # Check steps give same solution as solve -# t_eval = step_sol.t -# solution = solver.solve(model, t_eval) -# np.testing.assert_array_almost_equal(solution.y[0], step_sol.y[0]) - -# def test_model_step_with_input(self): -# # Create model -# model = pybamm.BaseModel() -# var = pybamm.Variable("var") -# a = pybamm.InputParameter("a") -# model.rhs = {var: a * var} -# model.initial_conditions = {var: 1} -# model.variables = {"a": a} -# # No need to set parameters; can use base discretisation (no spatial operators) - -# # create discretisation -# disc = pybamm.Discretisation() -# disc.process_model(model) - -# solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) - -# # Step with an input -# dt = 0.1 -# step_sol = solver.step(None, model, dt, npts=5, inputs={"a": 0.1}) -# np.testing.assert_array_equal(step_sol.t, np.linspace(0, dt, 5)) -# np.testing.assert_allclose(step_sol.y[0], np.exp(0.1 * step_sol.t)) - -# # Step again with different inputs -# step_sol_2 = solver.step(step_sol, model, dt, npts=5, inputs={"a": -1}) -# np.testing.assert_array_equal(step_sol_2.t, np.linspace(0, 2 * dt, 9)) -# np.testing.assert_array_equal( -# step_sol_2["a"].entries, np.array([0.1, 0.1, 0.1, 0.1, 0.1, -1, -1, -1, -1]) -# ) -# np.testing.assert_allclose( -# step_sol_2.y[0], -# np.concatenate( -# [ -# np.exp(0.1 * step_sol.t[:5]), -# np.exp(0.1 * step_sol.t[4]) * np.exp(-(step_sol.t[5:] - dt)), -# ] -# ), -# ) - -# def test_model_step_events(self): -# # Create model -# model = pybamm.BaseModel() -# var1 = pybamm.Variable("var1") -# var2 = pybamm.Variable("var2") -# model.rhs = {var1: 0.1 * var1} -# model.algebraic = {var2: 2 * var1 - var2} -# model.initial_conditions = {var1: 1, var2: 2} -# model.events = [ -# pybamm.Event("var1 = 1.5", pybamm.min(var1 - 1.5)), -# pybamm.Event("var2 = 2.5", pybamm.min(var2 - 2.5)), -# ] -# disc = pybamm.Discretisation() -# disc.process_model(model) - -# # Solve -# step_solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) -# dt = 0.05 -# time = 0 -# end_time = 5 -# step_solution = None -# while time < end_time: -# step_solution = step_solver.step(step_solution, model, dt=dt, npts=10) -# time += dt -# np.testing.assert_array_less(step_solution.y[0], 1.5) -# np.testing.assert_array_less(step_solution.y[-1], 2.5001) -# np.testing.assert_array_almost_equal( -# step_solution.y[0], np.exp(0.1 * step_solution.t), decimal=5 -# ) -# np.testing.assert_array_almost_equal( -# step_solution.y[-1], 2 * np.exp(0.1 * step_solution.t), decimal=4 -# ) - -# def test_model_solver_with_inputs(self): -# # Create model -# model = pybamm.BaseModel() -# domain = ["negative electrode", "separator", "positive electrode"] -# var = pybamm.Variable("var", domain=domain) -# model.rhs = {var: -pybamm.InputParameter("rate") * var} -# model.initial_conditions = {var: 1} -# model.events = [pybamm.Event("var=0.5", pybamm.min(var - 0.5))] -# # No need to set parameters; can use base discretisation (no spatial -# # operators) - -# # create discretisation -# mesh = get_mesh_for_testing() -# spatial_methods = {"macroscale": pybamm.FiniteVolume()} -# disc = pybamm.Discretisation(mesh, spatial_methods) -# disc.process_model(model) -# # Solve -# solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) -# t_eval = np.linspace(0, 10, 100) -# solution = solver.solve(model, t_eval, inputs={"rate": 0.1}) -# self.assertLess(len(solution.t), len(t_eval)) -# np.testing.assert_allclose(solution.y[0], np.exp(-0.1 * solution.t), rtol=1e-04) - -# def test_model_solver_dae_inputs_in_initial_conditions(self): -# # Create model -# model = pybamm.BaseModel() -# var1 = pybamm.Variable("var1") -# var2 = pybamm.Variable("var2") -# model.rhs = {var1: pybamm.InputParameter("rate") * var1} -# model.algebraic = {var2: var1 - var2} -# model.initial_conditions = { -# var1: pybamm.InputParameter("ic 1"), -# var2: pybamm.InputParameter("ic 2"), -# } - -# # Solve -# solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) -# t_eval = np.linspace(0, 5, 100) -# solution = solver.solve( -# model, t_eval, inputs={"rate": -1, "ic 1": 0.1, "ic 2": 2} -# ) -# np.testing.assert_array_almost_equal( -# solution.y[0], 0.1 * np.exp(-solution.t), decimal=5 -# ) -# np.testing.assert_array_almost_equal( -# solution.y[-1], 0.1 * np.exp(-solution.t), decimal=5 -# ) - -# # Solve again with different initial conditions -# solution = solver.solve( -# model, t_eval, inputs={"rate": -0.1, "ic 1": 1, "ic 2": 3} -# ) -# np.testing.assert_array_almost_equal( -# solution.y[0], 1 * np.exp(-0.1 * solution.t), decimal=5 -# ) -# np.testing.assert_array_almost_equal( -# solution.y[-1], 1 * np.exp(-0.1 * solution.t), decimal=5 -# ) - -# def test_model_solver_with_external(self): -# # Create model -# model = pybamm.BaseModel() -# domain = ["negative electrode", "separator", "positive electrode"] -# var1 = pybamm.Variable("var1", domain=domain) -# var2 = pybamm.Variable("var2", domain=domain) -# model.rhs = {var1: -var2} -# model.initial_conditions = {var1: 1} -# model.external_variables = [var2] -# model.variables = {"var1": var1, "var2": var2} -# # No need to set parameters; can use base discretisation (no spatial -# # operators) - -# # create discretisation -# mesh = get_mesh_for_testing() -# spatial_methods = {"macroscale": pybamm.FiniteVolume()} -# disc = pybamm.Discretisation(mesh, spatial_methods) -# disc.process_model(model) -# # Solve -# solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) -# t_eval = np.linspace(0, 10, 100) -# solution = solver.solve(model, t_eval, external_variables={"var2": 0.5}) -# np.testing.assert_allclose(solution.y[0], 1 - 0.5 * solution.t, rtol=1e-06) - -# def test_model_solver_with_non_identity_mass(self): -# model = pybamm.BaseModel() -# var1 = pybamm.Variable("var1", domain="negative electrode") -# var2 = pybamm.Variable("var2", domain="negative electrode") -# model.rhs = {var1: var1} -# model.algebraic = {var2: 2 * var1 - var2} -# model.initial_conditions = {var1: 1, var2: 2} -# disc = get_discretisation_for_testing() -# disc.process_model(model) - -# # FV discretisation has identity mass. Manually set the mass matrix to -# # be a diag of 10s here for testing. Note that the algebraic part is all -# # zeros -# mass_matrix = 10 * model.mass_matrix.entries -# model.mass_matrix = pybamm.Matrix(mass_matrix) - -# # Note that mass_matrix_inv is just the inverse of the ode block of the -# # mass matrix -# mass_matrix_inv = 0.1 * eye(int(mass_matrix.shape[0] / 2)) -# model.mass_matrix_inv = pybamm.Matrix(mass_matrix_inv) - -# # Solve -# solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) -# t_eval = np.linspace(0, 1, 100) -# solution = solver.solve(model, t_eval) -# np.testing.assert_array_equal(solution.t, t_eval) -# np.testing.assert_allclose(solution.y[0], np.exp(0.1 * solution.t)) -# np.testing.assert_allclose(solution.y[-1], 2 * np.exp(0.1 * solution.t)) - -# def test_dae_solver_algebraic_model(self): -# model = pybamm.BaseModel() -# var = pybamm.Variable("var") -# model.algebraic = {var: var + 1} -# model.initial_conditions = {var: 0} - -# disc = pybamm.Discretisation() -# disc.process_model(model) - -# solver = pybamm.CasadiSolver() -# t_eval = np.linspace(0, 1) -# with self.assertRaisesRegex( -# pybamm.SolverError, "Cannot use CasadiSolver to solve algebraic model" -# ): -# solver.solve(model, t_eval) - - -# class TestCasadiSolverSensitivity(unittest.TestCase): -# def test_solve_with_symbolic_input(self): -# # Simple system: a single differential equation -# var = pybamm.Variable("var") -# model = pybamm.BaseModel() -# model.rhs = {var: pybamm.InputParameter("param")} -# model.initial_conditions = {var: 2} -# model.variables = {"var": var} - -# # create discretisation -# disc = pybamm.Discretisation() -# disc.process_model(model) - -# # Solve -# solver = pybamm.CasadiSolver() -# t_eval = np.linspace(0, 1) -# solution = solver.solve(model, t_eval) -# np.testing.assert_array_almost_equal( -# solution["var"].value({"param": 7}).full().flatten(), 2 + 7 * t_eval -# ) -# np.testing.assert_array_almost_equal( -# solution["var"].value({"param": -3}).full().flatten(), 2 - 3 * t_eval -# ) -# np.testing.assert_array_almost_equal( -# solution["var"].sensitivity({"param": 3}).full().flatten(), t_eval -# ) - -# def test_least_squares_fit(self): -# # Simple system: a single algebraic equation -# var1 = pybamm.Variable("var1", domain="negative electrode") -# var2 = pybamm.Variable("var2", domain="negative electrode") -# model = pybamm.BaseModel() -# p = pybamm.InputParameter("p") -# q = pybamm.InputParameter("q") -# model.rhs = {var1: -var1} -# model.algebraic = {var2: (var2 - p)} -# model.initial_conditions = {var1: 1, var2: 3} -# model.variables = {"objective": (var2 - q) ** 2 + (p - 3) ** 2} - -# # create discretisation -# disc = get_discretisation_for_testing() -# disc.process_model(model) - -# # Solve -# solver = pybamm.CasadiSolver() -# solution = solver.solve(model, np.linspace(0, 1)) -# sol_var = solution["objective"] - -# def objective(x): -# return sol_var.value({"p": x[0], "q": x[1]}).full().flatten() - -# # without jacobian -# lsq_sol = least_squares(objective, [2, 2], method="lm") -# np.testing.assert_array_almost_equal(lsq_sol.x, [3, 3], decimal=3) - -# def jac(x): -# return sol_var.sensitivity({"p": x[0], "q": x[1]}) - -# # with jacobian -# lsq_sol = least_squares(objective, [2, 2], jac=jac, method="lm") -# np.testing.assert_array_almost_equal(lsq_sol.x, [3, 3], decimal=3) - -# def test_solve_with_symbolic_input_1D_scalar_input(self): -# var = pybamm.Variable("var", "negative electrode") -# model = pybamm.BaseModel() -# param = pybamm.InputParameter("param") -# model.rhs = {var: -param * var} -# model.initial_conditions = {var: 2} -# model.variables = {"var": var} - -# # create discretisation -# disc = get_discretisation_for_testing() -# disc.process_model(model) - -# # Solve - scalar input -# solver = pybamm.CasadiSolver() -# t_eval = np.linspace(0, 1) -# solution = solver.solve(model, t_eval) -# np.testing.assert_array_almost_equal( -# solution["var"].value({"param": 7}), -# np.repeat(2 * np.exp(-7 * t_eval), 40)[:, np.newaxis], -# decimal=4, -# ) -# np.testing.assert_array_almost_equal( -# solution["var"].value({"param": 3}), -# np.repeat(2 * np.exp(-3 * t_eval), 40)[:, np.newaxis], -# decimal=4, -# ) -# np.testing.assert_array_almost_equal( -# solution["var"].sensitivity({"param": 3}), -# np.repeat( -# -2 * t_eval * np.exp(-3 * t_eval), disc.mesh["negative electrode"].npts -# )[:, np.newaxis], -# decimal=4, -# ) - -# def test_solve_with_symbolic_input_1D_vector_input(self): -# var = pybamm.Variable("var", "negative electrode") -# model = pybamm.BaseModel() -# param = pybamm.InputParameter("param", "negative electrode") -# model.rhs = {var: -param * var} -# model.initial_conditions = {var: 2} -# model.variables = {"var": var} - -# # create discretisation -# disc = get_discretisation_for_testing() -# disc.process_model(model) - -# # Solve - scalar input -# solver = pybamm.CasadiSolver() -# solution = solver.solve(model, np.linspace(0, 1)) -# n = disc.mesh["negative electrode"].npts - -# solver = pybamm.CasadiSolver() -# t_eval = np.linspace(0, 1) -# solution = solver.solve(model, t_eval) -# p = np.linspace(0, 1, n)[:, np.newaxis] -# np.testing.assert_array_almost_equal( -# solution["var"].value({"param": 3 * np.ones(n)}), -# np.repeat(2 * np.exp(-3 * t_eval), 40)[:, np.newaxis], -# decimal=4, -# ) -# np.testing.assert_array_almost_equal( -# solution["var"].value({"param": 2 * p}), -# 2 * np.exp(-2 * p * t_eval).T.reshape(-1, 1), -# decimal=4, -# ) -# np.testing.assert_array_almost_equal( -# solution["var"].sensitivity({"param": 3 * np.ones(n)}), -# np.kron(-2 * t_eval * np.exp(-3 * t_eval), np.eye(40)).T, -# decimal=4, -# ) - -# sens = solution["var"].sensitivity({"param": p}).full() -# for idx in range(len(t_eval)): -# np.testing.assert_array_almost_equal( -# sens[40 * idx : 40 * (idx + 1), :], -# -2 * t_eval[idx] * np.exp(-p * t_eval[idx]) * np.eye(40), -# decimal=4, -# ) - -# def test_solve_with_symbolic_input_in_initial_conditions(self): -# # Simple system: a single algebraic equation -# var = pybamm.Variable("var") -# model = pybamm.BaseModel() -# model.rhs = {var: -var} -# model.initial_conditions = {var: pybamm.InputParameter("param")} -# model.variables = {"var": var} - -# # create discretisation -# disc = pybamm.Discretisation() -# disc.process_model(model) - -# # Solve -# solver = pybamm.CasadiSolver(atol=1e-10, rtol=1e-10) -# t_eval = np.linspace(0, 1) -# solution = solver.solve(model, t_eval) -# np.testing.assert_array_almost_equal( -# solution["var"].value({"param": 7}), 7 * np.exp(-t_eval)[np.newaxis, :] -# ) -# np.testing.assert_array_almost_equal( -# solution["var"].value({"param": 3}), 3 * np.exp(-t_eval)[np.newaxis, :] -# ) -# np.testing.assert_array_almost_equal( -# solution["var"].sensitivity({"param": 3}), np.exp(-t_eval)[:, np.newaxis] -# ) - -# def test_least_squares_fit_input_in_initial_conditions(self): -# # Simple system: a single algebraic equation -# var1 = pybamm.Variable("var1", domain="negative electrode") -# var2 = pybamm.Variable("var2", domain="negative electrode") -# model = pybamm.BaseModel() -# p = pybamm.InputParameter("p") -# q = pybamm.InputParameter("q") -# model.rhs = {var1: -var1} -# model.algebraic = {var2: (var2 - p)} -# model.initial_conditions = {var1: 1, var2: p} -# model.variables = {"objective": (var2 - q) ** 2 + (p - 3) ** 2} - -# # create discretisation -# disc = get_discretisation_for_testing() -# disc.process_model(model) - -# # Solve -# solver = pybamm.CasadiSolver() -# solution = solver.solve(model, np.linspace(0, 1)) -# sol_var = solution["objective"] - -# def objective(x): -# return sol_var.value({"p": x[0], "q": x[1]}).full().flatten() - -# # without jacobian -# lsq_sol = least_squares(objective, [2, 2], method="lm") -# np.testing.assert_array_almost_equal(lsq_sol.x, [3, 3], decimal=3) - - -# class TestCasadiSolverODEsWithForwardSensitivityEquations(unittest.TestCase): -# def test_solve_sensitivity_scalar_var_scalar_input(self): -# # Create model -# model = pybamm.BaseModel() -# var = pybamm.Variable("var") -# p = pybamm.InputParameter("p") -# model.rhs = {var: p * var} -# model.initial_conditions = {var: 1} -# model.variables = {"var squared": var ** 2} - -# # Solve -# # Make sure that passing in extra options works -# solver = pybamm.CasadiSolver( -# mode="fast", rtol=1e-10, atol=1e-10, solve_sensitivity_equations=True -# ) -# t_eval = np.linspace(0, 1, 80) -# solution = solver.solve(model, t_eval, inputs={"p": 0.1}) -# np.testing.assert_array_equal(solution.t, t_eval) -# np.testing.assert_allclose(solution.y[0], np.exp(0.1 * solution.t)) -# np.testing.assert_allclose( -# solution.sensitivity["p"], -# (solution.t * np.exp(0.1 * solution.t))[:, np.newaxis], -# ) -# np.testing.assert_allclose( -# solution["var squared"].data, np.exp(0.1 * solution.t) ** 2 -# ) -# np.testing.assert_allclose( -# solution["var squared"].sensitivity["p"], -# (2 * np.exp(0.1 * solution.t) * solution.t * np.exp(0.1 * solution.t))[ -# :, np.newaxis -# ], -# ) - -# # More complicated model -# # Create model -# model = pybamm.BaseModel() -# var = pybamm.Variable("var") -# p = pybamm.InputParameter("p") -# q = pybamm.InputParameter("q") -# r = pybamm.InputParameter("r") -# s = pybamm.InputParameter("s") -# model.rhs = {var: p * q} -# model.initial_conditions = {var: r} -# model.variables = {"var times s": var * s} - -# # Solve -# # Make sure that passing in extra options works -# solver = pybamm.CasadiSolver( -# rtol=1e-10, atol=1e-10, solve_sensitivity_equations=True -# ) -# t_eval = np.linspace(0, 1, 80) -# solution = solver.solve( -# model, t_eval, inputs={"p": 0.1, "q": 2, "r": -1, "s": 0.5} -# ) -# np.testing.assert_allclose(solution.y[0], -1 + 0.2 * solution.t) -# np.testing.assert_allclose( -# solution.sensitivity["p"], (2 * solution.t)[:, np.newaxis], -# ) -# np.testing.assert_allclose( -# solution.sensitivity["q"], (0.1 * solution.t)[:, np.newaxis], -# ) -# np.testing.assert_allclose(solution.sensitivity["r"], 1) -# np.testing.assert_allclose(solution.sensitivity["s"], 0) -# np.testing.assert_allclose( -# solution.sensitivity["all"], -# np.hstack( -# [ -# solution.sensitivity["p"], -# solution.sensitivity["q"], -# solution.sensitivity["r"], -# solution.sensitivity["s"], -# ] -# ), -# ) -# np.testing.assert_allclose( -# solution["var times s"].data, 0.5 * (-1 + 0.2 * solution.t) -# ) -# np.testing.assert_allclose( -# solution["var times s"].sensitivity["p"], -# 0.5 * (2 * solution.t)[:, np.newaxis], -# ) -# np.testing.assert_allclose( -# solution["var times s"].sensitivity["q"], -# 0.5 * (0.1 * solution.t)[:, np.newaxis], -# ) -# np.testing.assert_allclose(solution["var times s"].sensitivity["r"], 0.5) -# np.testing.assert_allclose( -# solution["var times s"].sensitivity["s"], -# (-1 + 0.2 * solution.t)[:, np.newaxis], -# ) -# np.testing.assert_allclose( -# solution["var times s"].sensitivity["all"], -# np.hstack( -# [ -# solution["var times s"].sensitivity["p"], -# solution["var times s"].sensitivity["q"], -# solution["var times s"].sensitivity["r"], -# solution["var times s"].sensitivity["s"], -# ] -# ), -# ) - -# def test_solve_sensitivity_vector_var_scalar_input(self): -# var = pybamm.Variable("var", "negative electrode") -# model = pybamm.BaseModel() -# # Set length scales to avoid warning -# model.length_scales = {"negative electrode": 1} -# param = pybamm.InputParameter("param") -# model.rhs = {var: -param * var} -# model.initial_conditions = {var: 2} -# model.variables = {"var": var} - -# # create discretisation -# disc = get_discretisation_for_testing() -# disc.process_model(model) -# n = disc.mesh["negative electrode"].npts - -# # Solve - scalar input -# solver = pybamm.CasadiSolver(solve_sensitivity_equations=True) -# t_eval = np.linspace(0, 1) -# solution = solver.solve(model, t_eval, inputs={"param": 7}) -# np.testing.assert_array_almost_equal( -# solution["var"].data, np.tile(2 * np.exp(-7 * t_eval), (n, 1)), decimal=4, -# ) -# np.testing.assert_array_almost_equal( -# solution["var"].sensitivity["param"], -# np.repeat(-2 * t_eval * np.exp(-7 * t_eval), n)[:, np.newaxis], -# decimal=4, -# ) - -# # More complicated model -# # Create model -# model = pybamm.BaseModel() -# # Set length scales to avoid warning -# model.length_scales = {"negative electrode": 1} -# var = pybamm.Variable("var", "negative electrode") -# p = pybamm.InputParameter("p") -# q = pybamm.InputParameter("q") -# r = pybamm.InputParameter("r") -# s = pybamm.InputParameter("s") -# model.rhs = {var: p * q} -# model.initial_conditions = {var: r} -# model.variables = {"var times s": var * s} - -# # Discretise -# disc.process_model(model) - -# # Solve -# # Make sure that passing in extra options works -# solver = pybamm.CasadiSolver( -# rtol=1e-10, atol=1e-10, solve_sensitivity_equations=True -# ) -# t_eval = np.linspace(0, 1, 80) -# solution = solver.solve( -# model, t_eval, inputs={"p": 0.1, "q": 2, "r": -1, "s": 0.5} -# ) -# np.testing.assert_allclose(solution.y, np.tile(-1 + 0.2 * solution.t, (n, 1))) -# np.testing.assert_allclose( -# solution.sensitivity["p"], np.repeat(2 * solution.t, n)[:, np.newaxis], -# ) -# np.testing.assert_allclose( -# solution.sensitivity["q"], np.repeat(0.1 * solution.t, n)[:, np.newaxis], -# ) -# np.testing.assert_allclose(solution.sensitivity["r"], 1) -# np.testing.assert_allclose(solution.sensitivity["s"], 0) -# np.testing.assert_allclose( -# solution.sensitivity["all"], -# np.hstack( -# [ -# solution.sensitivity["p"], -# solution.sensitivity["q"], -# solution.sensitivity["r"], -# solution.sensitivity["s"], -# ] -# ), -# ) -# np.testing.assert_allclose( -# solution["var times s"].data, np.tile(0.5 * (-1 + 0.2 * solution.t), (n, 1)) -# ) -# np.testing.assert_allclose( -# solution["var times s"].sensitivity["p"], -# np.repeat(0.5 * (2 * solution.t), n)[:, np.newaxis], -# ) -# np.testing.assert_allclose( -# solution["var times s"].sensitivity["q"], -# np.repeat(0.5 * (0.1 * solution.t), n)[:, np.newaxis], -# ) -# np.testing.assert_allclose(solution["var times s"].sensitivity["r"], 0.5) -# np.testing.assert_allclose( -# solution["var times s"].sensitivity["s"], -# np.repeat(-1 + 0.2 * solution.t, n)[:, np.newaxis], -# ) -# np.testing.assert_allclose( -# solution["var times s"].sensitivity["all"], -# np.hstack( -# [ -# solution["var times s"].sensitivity["p"], -# solution["var times s"].sensitivity["q"], -# solution["var times s"].sensitivity["r"], -# solution["var times s"].sensitivity["s"], -# ] -# ), -# ) - -# def test_solve_sensitivity_scalar_var_vector_input(self): -# var = pybamm.Variable("var", "negative electrode") -# model = pybamm.BaseModel() -# # Set length scales to avoid warning -# model.length_scales = {"negative electrode": 1} - -# param = pybamm.InputParameter("param", "negative electrode") -# model.rhs = {var: -param * var} -# model.initial_conditions = {var: 2} -# model.variables = { -# "var": var, -# "integral of var": pybamm.Integral(var, pybamm.standard_spatial_vars.x_n), -# } - -# # create discretisation -# mesh = get_mesh_for_testing(xpts=5) -# spatial_methods = {"macroscale": pybamm.FiniteVolume()} -# disc = pybamm.Discretisation(mesh, spatial_methods) -# disc.process_model(model) -# n = disc.mesh["negative electrode"].npts - -# # Solve - constant input -# solver = pybamm.CasadiSolver( -# mode="fast", rtol=1e-10, atol=1e-10, solve_sensitivity_equations=True -# ) -# t_eval = np.linspace(0, 1) -# solution = solver.solve(model, t_eval, inputs={"param": 7 * np.ones(n)}) -# l_n = mesh["negative electrode"].edges[-1] -# np.testing.assert_array_almost_equal( -# solution["var"].data, np.tile(2 * np.exp(-7 * t_eval), (n, 1)), decimal=4, -# ) - -# np.testing.assert_array_almost_equal( -# solution["var"].sensitivity["param"], -# np.vstack([np.eye(n) * -2 * t * np.exp(-7 * t) for t in t_eval]), -# ) -# np.testing.assert_array_almost_equal( -# solution["integral of var"].data, 2 * np.exp(-7 * t_eval) * l_n, decimal=4, -# ) -# np.testing.assert_array_almost_equal( -# solution["integral of var"].sensitivity["param"], -# np.tile(-2 * t_eval * np.exp(-7 * t_eval) * l_n / n, (n, 1)).T, -# ) - -# # Solve - linspace input -# p_eval = np.linspace(1, 2, n) -# solution = solver.solve(model, t_eval, inputs={"param": p_eval}) -# l_n = mesh["negative electrode"].edges[-1] -# np.testing.assert_array_almost_equal( -# solution["var"].data, 2 * np.exp(-p_eval[:, np.newaxis] * t_eval), decimal=4 -# ) -# np.testing.assert_array_almost_equal( -# solution["var"].sensitivity["param"], -# np.vstack([np.diag(-2 * t * np.exp(-p_eval * t)) for t in t_eval]), -# ) - -# np.testing.assert_array_almost_equal( -# solution["integral of var"].data, -# np.sum( -# 2 -# * np.exp(-p_eval[:, np.newaxis] * t_eval) -# * mesh["negative electrode"].d_edges[:, np.newaxis], -# axis=0, -# ), -# ) -# np.testing.assert_array_almost_equal( -# solution["integral of var"].sensitivity["param"], -# np.vstack([-2 * t * np.exp(-p_eval * t) * l_n / n for t in t_eval]), -# ) +class TestCasadiSolver(unittest.TestCase): + def test_bad_mode(self): + with self.assertRaisesRegex(ValueError, "invalid mode"): + pybamm.CasadiSolver(mode="bad mode") + + def test_model_solver(self): + # Create model + model = pybamm.BaseModel() + var = pybamm.Variable("var") + model.rhs = {var: 0.1 * var} + model.initial_conditions = {var: 1} + # No need to set parameters; can use base discretisation (no spatial operators) + + # create discretisation + disc = pybamm.Discretisation() + model_disc = disc.process_model(model, inplace=False) + # Solve + solver = pybamm.CasadiSolver(mode="fast", rtol=1e-8, atol=1e-8) + t_eval = np.linspace(0, 1, 100) + solution = solver.solve(model_disc, t_eval) + np.testing.assert_array_equal(solution.t, t_eval) + np.testing.assert_array_almost_equal( + solution.y[0], np.exp(0.1 * solution.t), decimal=5 + ) + + # Safe mode (enforce events that won't be triggered) + model.events = [pybamm.Event("an event", var + 1)] + disc.process_model(model) + solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) + t_eval = np.linspace(0, 1, 100) + solution = solver.solve(model, t_eval) + np.testing.assert_array_equal(solution.t, t_eval) + np.testing.assert_array_almost_equal( + solution.y[0], np.exp(0.1 * solution.t), decimal=5 + ) + + def test_model_solver_python(self): + # Create model + pybamm.set_logging_level("ERROR") + model = pybamm.BaseModel() + model.convert_to_format = "python" + var = pybamm.Variable("var") + model.rhs = {var: 0.1 * var} + model.initial_conditions = {var: 1} + # No need to set parameters; can use base discretisation (no spatial operators) + + # create discretisation + disc = pybamm.Discretisation() + disc.process_model(model) + # Solve + solver = pybamm.CasadiSolver(mode="fast", rtol=1e-8, atol=1e-8) + t_eval = np.linspace(0, 1, 100) + solution = solver.solve(model, t_eval) + np.testing.assert_array_equal(solution.t, t_eval) + np.testing.assert_array_almost_equal( + solution.y[0], np.exp(0.1 * solution.t), decimal=5 + ) + pybamm.set_logging_level("WARNING") + + def test_model_solver_failure(self): + # Create model + model = pybamm.BaseModel() + var = pybamm.Variable("var") + model.rhs = {var: -pybamm.sqrt(var)} + model.initial_conditions = {var: 1} + # add events so that safe mode is used (won't be triggered) + model.events = [pybamm.Event("10", var - 10)] + # No need to set parameters; can use base discretisation (no spatial operators) + + # create discretisation + disc = pybamm.Discretisation() + model_disc = disc.process_model(model, inplace=False) + + solver = pybamm.CasadiSolver(extra_options_call={"regularity_check": False}) + # Solve with failure at t=2 + t_eval = np.linspace(0, 20, 100) + with self.assertRaises(pybamm.SolverError): + solver.solve(model_disc, t_eval) + # Solve with failure at t=0 + model.initial_conditions = {var: 0} + model_disc = disc.process_model(model, inplace=False) + t_eval = np.linspace(0, 20, 100) + with self.assertRaises(pybamm.SolverError): + solver.solve(model_disc, t_eval) + + def test_model_solver_events(self): + # Create model + model = pybamm.BaseModel() + whole_cell = ["negative electrode", "separator", "positive electrode"] + var1 = pybamm.Variable("var1", domain=whole_cell) + var2 = pybamm.Variable("var2", domain=whole_cell) + model.rhs = {var1: 0.1 * var1} + model.algebraic = {var2: 2 * var1 - var2} + model.initial_conditions = {var1: 1, var2: 2} + model.events = [ + pybamm.Event("var1 = 1.5", pybamm.min(var1 - 1.5)), + pybamm.Event("var2 = 2.5", pybamm.min(var2 - 2.5)), + ] + disc = get_discretisation_for_testing() + disc.process_model(model) + + # Solve using "safe" mode + solver = pybamm.CasadiSolver(mode="safe", rtol=1e-8, atol=1e-8) + t_eval = np.linspace(0, 5, 100) + solution = solver.solve(model, t_eval) + np.testing.assert_array_less(solution.y[0], 1.5) + np.testing.assert_array_less(solution.y[-1], 2.5 + 1e-10) + np.testing.assert_array_almost_equal( + solution.y[0], np.exp(0.1 * solution.t), decimal=5 + ) + np.testing.assert_array_almost_equal( + solution.y[-1], 2 * np.exp(0.1 * solution.t), decimal=5 + ) + + # Solve using "safe" mode with debug off + pybamm.settings.debug_mode = False + solver = pybamm.CasadiSolver(mode="safe", rtol=1e-8, atol=1e-8, dt_max=1) + t_eval = np.linspace(0, 5, 100) + solution = solver.solve(model, t_eval) + np.testing.assert_array_less(solution.y[0], 1.5) + np.testing.assert_array_less(solution.y[-1], 2.5 + 1e-10) + # test the last entry is exactly 2.5 + np.testing.assert_array_almost_equal(solution.y[-1, -1], 2.5, decimal=2) + np.testing.assert_array_almost_equal( + solution.y[0], np.exp(0.1 * solution.t), decimal=5 + ) + np.testing.assert_array_almost_equal( + solution.y[-1], 2 * np.exp(0.1 * solution.t), decimal=5 + ) + pybamm.settings.debug_mode = True + + # Try dt_max=0 to enforce using all timesteps + solver = pybamm.CasadiSolver(dt_max=0, rtol=1e-8, atol=1e-8) + t_eval = np.linspace(0, 5, 100) + solution = solver.solve(model, t_eval) + np.testing.assert_array_less(solution.y[0], 1.5) + np.testing.assert_array_less(solution.y[-1], 2.5) + np.testing.assert_array_almost_equal( + solution.y[0], np.exp(0.1 * solution.t), decimal=5 + ) + np.testing.assert_array_almost_equal( + solution.y[-1], 2 * np.exp(0.1 * solution.t), decimal=5 + ) + + # Test when an event returns nan + model = pybamm.BaseModel() + var = pybamm.Variable("var") + model.rhs = {var: 0.1 * var} + model.initial_conditions = {var: 1} + model.events = [ + pybamm.Event("event", var - 1.02), + pybamm.Event("sqrt event", pybamm.sqrt(1.0199 - var)), + ] + disc = pybamm.Discretisation() + disc.process_model(model) + solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) + solution = solver.solve(model, t_eval) + np.testing.assert_array_less(solution.y[0], 1.02 + 1e-10) + np.testing.assert_array_almost_equal(solution.y[0, -1], 1.02, decimal=2) + + def test_model_step(self): + # Create model + model = pybamm.BaseModel() + domain = ["negative electrode", "separator", "positive electrode"] + var = pybamm.Variable("var", domain=domain) + model.rhs = {var: 0.1 * var} + model.initial_conditions = {var: 1} + # No need to set parameters; can use base discretisation (no spatial operators) + + # create discretisation + mesh = get_mesh_for_testing() + spatial_methods = {"macroscale": pybamm.FiniteVolume()} + disc = pybamm.Discretisation(mesh, spatial_methods) + disc.process_model(model) + + solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) + + # Step once + dt = 1 + step_sol = solver.step(None, model, dt) + np.testing.assert_array_equal(step_sol.t, [0, dt]) + np.testing.assert_array_almost_equal(step_sol.y[0], np.exp(0.1 * step_sol.t)) + + # Step again (return 5 points) + step_sol_2 = solver.step(step_sol, model, dt, npts=5) + np.testing.assert_array_equal( + step_sol_2.t, np.concatenate([np.array([0]), np.linspace(dt, 2 * dt, 5)]) + ) + np.testing.assert_array_almost_equal( + step_sol_2.y[0], np.exp(0.1 * step_sol_2.t) + ) + + # Check steps give same solution as solve + t_eval = step_sol.t + solution = solver.solve(model, t_eval) + np.testing.assert_array_almost_equal(solution.y[0], step_sol.y[0]) + + def test_model_step_with_input(self): + # Create model + model = pybamm.BaseModel() + var = pybamm.Variable("var") + a = pybamm.InputParameter("a") + model.rhs = {var: a * var} + model.initial_conditions = {var: 1} + model.variables = {"a": a} + # No need to set parameters; can use base discretisation (no spatial operators) + + # create discretisation + disc = pybamm.Discretisation() + disc.process_model(model) + + solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) + + # Step with an input + dt = 0.1 + step_sol = solver.step(None, model, dt, npts=5, inputs={"a": 0.1}) + np.testing.assert_array_equal(step_sol.t, np.linspace(0, dt, 5)) + np.testing.assert_allclose(step_sol.y[0], np.exp(0.1 * step_sol.t)) + + # Step again with different inputs + step_sol_2 = solver.step(step_sol, model, dt, npts=5, inputs={"a": -1}) + np.testing.assert_array_equal(step_sol_2.t, np.linspace(0, 2 * dt, 9)) + np.testing.assert_array_equal( + step_sol_2["a"].entries, np.array([0.1, 0.1, 0.1, 0.1, 0.1, -1, -1, -1, -1]) + ) + np.testing.assert_allclose( + step_sol_2.y[0], + np.concatenate( + [ + np.exp(0.1 * step_sol.t[:5]), + np.exp(0.1 * step_sol.t[4]) * np.exp(-(step_sol.t[5:] - dt)), + ] + ), + ) + + def test_model_step_events(self): + # Create model + model = pybamm.BaseModel() + var1 = pybamm.Variable("var1") + var2 = pybamm.Variable("var2") + model.rhs = {var1: 0.1 * var1} + model.algebraic = {var2: 2 * var1 - var2} + model.initial_conditions = {var1: 1, var2: 2} + model.events = [ + pybamm.Event("var1 = 1.5", pybamm.min(var1 - 1.5)), + pybamm.Event("var2 = 2.5", pybamm.min(var2 - 2.5)), + ] + disc = pybamm.Discretisation() + disc.process_model(model) + + # Solve + step_solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) + dt = 0.05 + time = 0 + end_time = 5 + step_solution = None + while time < end_time: + step_solution = step_solver.step(step_solution, model, dt=dt, npts=10) + time += dt + np.testing.assert_array_less(step_solution.y[0], 1.5) + np.testing.assert_array_less(step_solution.y[-1], 2.5001) + np.testing.assert_array_almost_equal( + step_solution.y[0], np.exp(0.1 * step_solution.t), decimal=5 + ) + np.testing.assert_array_almost_equal( + step_solution.y[-1], 2 * np.exp(0.1 * step_solution.t), decimal=4 + ) + + def test_model_solver_with_inputs(self): + # Create model + model = pybamm.BaseModel() + domain = ["negative electrode", "separator", "positive electrode"] + var = pybamm.Variable("var", domain=domain) + model.rhs = {var: -pybamm.InputParameter("rate") * var} + model.initial_conditions = {var: 1} + model.events = [pybamm.Event("var=0.5", pybamm.min(var - 0.5))] + # No need to set parameters; can use base discretisation (no spatial + # operators) + + # create discretisation + mesh = get_mesh_for_testing() + spatial_methods = {"macroscale": pybamm.FiniteVolume()} + disc = pybamm.Discretisation(mesh, spatial_methods) + disc.process_model(model) + # Solve + solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) + t_eval = np.linspace(0, 10, 100) + solution = solver.solve(model, t_eval, inputs={"rate": 0.1}) + self.assertLess(len(solution.t), len(t_eval)) + np.testing.assert_allclose(solution.y[0], np.exp(-0.1 * solution.t), rtol=1e-04) + + def test_model_solver_dae_inputs_in_initial_conditions(self): + # Create model + model = pybamm.BaseModel() + var1 = pybamm.Variable("var1") + var2 = pybamm.Variable("var2") + model.rhs = {var1: pybamm.InputParameter("rate") * var1} + model.algebraic = {var2: var1 - var2} + model.initial_conditions = { + var1: pybamm.InputParameter("ic 1"), + var2: pybamm.InputParameter("ic 2"), + } + + # Solve + solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) + t_eval = np.linspace(0, 5, 100) + solution = solver.solve( + model, t_eval, inputs={"rate": -1, "ic 1": 0.1, "ic 2": 2} + ) + np.testing.assert_array_almost_equal( + solution.y[0], 0.1 * np.exp(-solution.t), decimal=5 + ) + np.testing.assert_array_almost_equal( + solution.y[-1], 0.1 * np.exp(-solution.t), decimal=5 + ) + + # Solve again with different initial conditions + solution = solver.solve( + model, t_eval, inputs={"rate": -0.1, "ic 1": 1, "ic 2": 3} + ) + np.testing.assert_array_almost_equal( + solution.y[0], 1 * np.exp(-0.1 * solution.t), decimal=5 + ) + np.testing.assert_array_almost_equal( + solution.y[-1], 1 * np.exp(-0.1 * solution.t), decimal=5 + ) + + def test_model_solver_with_external(self): + # Create model + model = pybamm.BaseModel() + domain = ["negative electrode", "separator", "positive electrode"] + var1 = pybamm.Variable("var1", domain=domain) + var2 = pybamm.Variable("var2", domain=domain) + model.rhs = {var1: -var2} + model.initial_conditions = {var1: 1} + model.external_variables = [var2] + model.variables = {"var1": var1, "var2": var2} + # No need to set parameters; can use base discretisation (no spatial + # operators) + + # create discretisation + mesh = get_mesh_for_testing() + spatial_methods = {"macroscale": pybamm.FiniteVolume()} + disc = pybamm.Discretisation(mesh, spatial_methods) + disc.process_model(model) + # Solve + solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) + t_eval = np.linspace(0, 10, 100) + solution = solver.solve(model, t_eval, external_variables={"var2": 0.5}) + np.testing.assert_allclose(solution.y[0], 1 - 0.5 * solution.t, rtol=1e-06) + + def test_model_solver_with_non_identity_mass(self): + model = pybamm.BaseModel() + var1 = pybamm.Variable("var1", domain="negative electrode") + var2 = pybamm.Variable("var2", domain="negative electrode") + model.rhs = {var1: var1} + model.algebraic = {var2: 2 * var1 - var2} + model.initial_conditions = {var1: 1, var2: 2} + disc = get_discretisation_for_testing() + disc.process_model(model) + + # FV discretisation has identity mass. Manually set the mass matrix to + # be a diag of 10s here for testing. Note that the algebraic part is all + # zeros + mass_matrix = 10 * model.mass_matrix.entries + model.mass_matrix = pybamm.Matrix(mass_matrix) + + # Note that mass_matrix_inv is just the inverse of the ode block of the + # mass matrix + mass_matrix_inv = 0.1 * eye(int(mass_matrix.shape[0] / 2)) + model.mass_matrix_inv = pybamm.Matrix(mass_matrix_inv) + + # Solve + solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) + t_eval = np.linspace(0, 1, 100) + solution = solver.solve(model, t_eval) + np.testing.assert_array_equal(solution.t, t_eval) + np.testing.assert_allclose(solution.y[0], np.exp(0.1 * solution.t)) + np.testing.assert_allclose(solution.y[-1], 2 * np.exp(0.1 * solution.t)) + + def test_dae_solver_algebraic_model(self): + model = pybamm.BaseModel() + var = pybamm.Variable("var") + model.algebraic = {var: var + 1} + model.initial_conditions = {var: 0} + + disc = pybamm.Discretisation() + disc.process_model(model) + + solver = pybamm.CasadiSolver() + t_eval = np.linspace(0, 1) + with self.assertRaisesRegex( + pybamm.SolverError, "Cannot use CasadiSolver to solve algebraic model" + ): + solver.solve(model, t_eval) + + +class TestCasadiSolverSensitivity(unittest.TestCase): + def test_solve_with_symbolic_input(self): + # Simple system: a single differential equation + var = pybamm.Variable("var") + model = pybamm.BaseModel() + model.rhs = {var: pybamm.InputParameter("param")} + model.initial_conditions = {var: 2} + model.variables = {"var": var} + + # create discretisation + disc = pybamm.Discretisation() + disc.process_model(model) + + # Solve + solver = pybamm.CasadiSolver() + t_eval = np.linspace(0, 1) + solution = solver.solve(model, t_eval) + np.testing.assert_array_almost_equal( + solution["var"].value({"param": 7}).full().flatten(), 2 + 7 * t_eval + ) + np.testing.assert_array_almost_equal( + solution["var"].value({"param": -3}).full().flatten(), 2 - 3 * t_eval + ) + np.testing.assert_array_almost_equal( + solution["var"].sensitivity({"param": 3}).full().flatten(), t_eval + ) + + def test_least_squares_fit(self): + # Simple system: a single algebraic equation + var1 = pybamm.Variable("var1", domain="negative electrode") + var2 = pybamm.Variable("var2", domain="negative electrode") + model = pybamm.BaseModel() + p = pybamm.InputParameter("p") + q = pybamm.InputParameter("q") + model.rhs = {var1: -var1} + model.algebraic = {var2: (var2 - p)} + model.initial_conditions = {var1: 1, var2: 3} + model.variables = {"objective": (var2 - q) ** 2 + (p - 3) ** 2} + + # create discretisation + disc = get_discretisation_for_testing() + disc.process_model(model) + + # Solve + solver = pybamm.CasadiSolver() + solution = solver.solve(model, np.linspace(0, 1)) + sol_var = solution["objective"] + + def objective(x): + return sol_var.value({"p": x[0], "q": x[1]}).full().flatten() + + # without jacobian + lsq_sol = least_squares(objective, [2, 2], method="lm") + np.testing.assert_array_almost_equal(lsq_sol.x, [3, 3], decimal=3) + + def jac(x): + return sol_var.sensitivity({"p": x[0], "q": x[1]}) + + # with jacobian + lsq_sol = least_squares(objective, [2, 2], jac=jac, method="lm") + np.testing.assert_array_almost_equal(lsq_sol.x, [3, 3], decimal=3) + + def test_solve_with_symbolic_input_1D_scalar_input(self): + var = pybamm.Variable("var", "negative electrode") + model = pybamm.BaseModel() + param = pybamm.InputParameter("param") + model.rhs = {var: -param * var} + model.initial_conditions = {var: 2} + model.variables = {"var": var} + + # create discretisation + disc = get_discretisation_for_testing() + disc.process_model(model) + + # Solve - scalar input + solver = pybamm.CasadiSolver() + t_eval = np.linspace(0, 1) + solution = solver.solve(model, t_eval) + np.testing.assert_array_almost_equal( + solution["var"].value({"param": 7}), + np.repeat(2 * np.exp(-7 * t_eval), 40)[:, np.newaxis], + decimal=4, + ) + np.testing.assert_array_almost_equal( + solution["var"].value({"param": 3}), + np.repeat(2 * np.exp(-3 * t_eval), 40)[:, np.newaxis], + decimal=4, + ) + np.testing.assert_array_almost_equal( + solution["var"].sensitivity({"param": 3}), + np.repeat( + -2 * t_eval * np.exp(-3 * t_eval), disc.mesh["negative electrode"].npts + )[:, np.newaxis], + decimal=4, + ) + + def test_solve_with_symbolic_input_1D_vector_input(self): + var = pybamm.Variable("var", "negative electrode") + model = pybamm.BaseModel() + param = pybamm.InputParameter("param", "negative electrode") + model.rhs = {var: -param * var} + model.initial_conditions = {var: 2} + model.variables = {"var": var} + + # create discretisation + disc = get_discretisation_for_testing() + disc.process_model(model) + + # Solve - scalar input + solver = pybamm.CasadiSolver() + solution = solver.solve(model, np.linspace(0, 1)) + n = disc.mesh["negative electrode"].npts + + solver = pybamm.CasadiSolver() + t_eval = np.linspace(0, 1) + solution = solver.solve(model, t_eval) + p = np.linspace(0, 1, n)[:, np.newaxis] + np.testing.assert_array_almost_equal( + solution["var"].value({"param": 3 * np.ones(n)}), + np.repeat(2 * np.exp(-3 * t_eval), 40)[:, np.newaxis], + decimal=4, + ) + np.testing.assert_array_almost_equal( + solution["var"].value({"param": 2 * p}), + 2 * np.exp(-2 * p * t_eval).T.reshape(-1, 1), + decimal=4, + ) + np.testing.assert_array_almost_equal( + solution["var"].sensitivity({"param": 3 * np.ones(n)}), + np.kron(-2 * t_eval * np.exp(-3 * t_eval), np.eye(40)).T, + decimal=4, + ) + + sens = solution["var"].sensitivity({"param": p}).full() + for idx in range(len(t_eval)): + np.testing.assert_array_almost_equal( + sens[40 * idx : 40 * (idx + 1), :], + -2 * t_eval[idx] * np.exp(-p * t_eval[idx]) * np.eye(40), + decimal=4, + ) + + def test_solve_with_symbolic_input_in_initial_conditions(self): + # Simple system: a single algebraic equation + var = pybamm.Variable("var") + model = pybamm.BaseModel() + model.rhs = {var: -var} + model.initial_conditions = {var: pybamm.InputParameter("param")} + model.variables = {"var": var} + + # create discretisation + disc = pybamm.Discretisation() + disc.process_model(model) + + # Solve + solver = pybamm.CasadiSolver(atol=1e-10, rtol=1e-10) + t_eval = np.linspace(0, 1) + solution = solver.solve(model, t_eval) + np.testing.assert_array_almost_equal( + solution["var"].value({"param": 7}), 7 * np.exp(-t_eval)[np.newaxis, :] + ) + np.testing.assert_array_almost_equal( + solution["var"].value({"param": 3}), 3 * np.exp(-t_eval)[np.newaxis, :] + ) + np.testing.assert_array_almost_equal( + solution["var"].sensitivity({"param": 3}), np.exp(-t_eval)[:, np.newaxis] + ) + + def test_least_squares_fit_input_in_initial_conditions(self): + # Simple system: a single algebraic equation + var1 = pybamm.Variable("var1", domain="negative electrode") + var2 = pybamm.Variable("var2", domain="negative electrode") + model = pybamm.BaseModel() + p = pybamm.InputParameter("p") + q = pybamm.InputParameter("q") + model.rhs = {var1: -var1} + model.algebraic = {var2: (var2 - p)} + model.initial_conditions = {var1: 1, var2: p} + model.variables = {"objective": (var2 - q) ** 2 + (p - 3) ** 2} + + # create discretisation + disc = get_discretisation_for_testing() + disc.process_model(model) + + # Solve + solver = pybamm.CasadiSolver() + solution = solver.solve(model, np.linspace(0, 1)) + sol_var = solution["objective"] + + def objective(x): + return sol_var.value({"p": x[0], "q": x[1]}).full().flatten() + + # without jacobian + lsq_sol = least_squares(objective, [2, 2], method="lm") + np.testing.assert_array_almost_equal(lsq_sol.x, [3, 3], decimal=3) + + +class TestCasadiSolverODEsWithForwardSensitivityEquations(unittest.TestCase): + def test_solve_sensitivity_scalar_var_scalar_input(self): + # Create model + model = pybamm.BaseModel() + var = pybamm.Variable("var") + p = pybamm.InputParameter("p") + model.rhs = {var: p * var} + model.initial_conditions = {var: 1} + model.variables = {"var squared": var ** 2} + + # Solve + # Make sure that passing in extra options works + solver = pybamm.CasadiSolver( + mode="fast", rtol=1e-10, atol=1e-10, solve_sensitivity_equations=True + ) + t_eval = np.linspace(0, 1, 80) + solution = solver.solve(model, t_eval, inputs={"p": 0.1}) + np.testing.assert_array_equal(solution.t, t_eval) + np.testing.assert_allclose(solution.y[0], np.exp(0.1 * solution.t)) + np.testing.assert_allclose( + solution.sensitivity["p"], + (solution.t * np.exp(0.1 * solution.t))[:, np.newaxis], + ) + np.testing.assert_allclose( + solution["var squared"].data, np.exp(0.1 * solution.t) ** 2 + ) + np.testing.assert_allclose( + solution["var squared"].sensitivity["p"], + (2 * np.exp(0.1 * solution.t) * solution.t * np.exp(0.1 * solution.t))[ + :, np.newaxis + ], + ) + + # More complicated model + # Create model + model = pybamm.BaseModel() + var = pybamm.Variable("var") + p = pybamm.InputParameter("p") + q = pybamm.InputParameter("q") + r = pybamm.InputParameter("r") + s = pybamm.InputParameter("s") + model.rhs = {var: p * q} + model.initial_conditions = {var: r} + model.variables = {"var times s": var * s} + + # Solve + # Make sure that passing in extra options works + solver = pybamm.CasadiSolver( + rtol=1e-10, atol=1e-10, solve_sensitivity_equations=True + ) + t_eval = np.linspace(0, 1, 80) + solution = solver.solve( + model, t_eval, inputs={"p": 0.1, "q": 2, "r": -1, "s": 0.5} + ) + np.testing.assert_allclose(solution.y[0], -1 + 0.2 * solution.t) + np.testing.assert_allclose( + solution.sensitivity["p"], (2 * solution.t)[:, np.newaxis], + ) + np.testing.assert_allclose( + solution.sensitivity["q"], (0.1 * solution.t)[:, np.newaxis], + ) + np.testing.assert_allclose(solution.sensitivity["r"], 1) + np.testing.assert_allclose(solution.sensitivity["s"], 0) + np.testing.assert_allclose( + solution.sensitivity["all"], + np.hstack( + [ + solution.sensitivity["p"], + solution.sensitivity["q"], + solution.sensitivity["r"], + solution.sensitivity["s"], + ] + ), + ) + np.testing.assert_allclose( + solution["var times s"].data, 0.5 * (-1 + 0.2 * solution.t) + ) + np.testing.assert_allclose( + solution["var times s"].sensitivity["p"], + 0.5 * (2 * solution.t)[:, np.newaxis], + ) + np.testing.assert_allclose( + solution["var times s"].sensitivity["q"], + 0.5 * (0.1 * solution.t)[:, np.newaxis], + ) + np.testing.assert_allclose(solution["var times s"].sensitivity["r"], 0.5) + np.testing.assert_allclose( + solution["var times s"].sensitivity["s"], + (-1 + 0.2 * solution.t)[:, np.newaxis], + ) + np.testing.assert_allclose( + solution["var times s"].sensitivity["all"], + np.hstack( + [ + solution["var times s"].sensitivity["p"], + solution["var times s"].sensitivity["q"], + solution["var times s"].sensitivity["r"], + solution["var times s"].sensitivity["s"], + ] + ), + ) + + def test_solve_sensitivity_vector_var_scalar_input(self): + var = pybamm.Variable("var", "negative electrode") + model = pybamm.BaseModel() + # Set length scales to avoid warning + model.length_scales = {"negative electrode": 1} + param = pybamm.InputParameter("param") + model.rhs = {var: -param * var} + model.initial_conditions = {var: 2} + model.variables = {"var": var} + + # create discretisation + disc = get_discretisation_for_testing() + disc.process_model(model) + n = disc.mesh["negative electrode"].npts + + # Solve - scalar input + solver = pybamm.CasadiSolver(solve_sensitivity_equations=True) + t_eval = np.linspace(0, 1) + solution = solver.solve(model, t_eval, inputs={"param": 7}) + np.testing.assert_array_almost_equal( + solution["var"].data, np.tile(2 * np.exp(-7 * t_eval), (n, 1)), decimal=4, + ) + np.testing.assert_array_almost_equal( + solution["var"].sensitivity["param"], + np.repeat(-2 * t_eval * np.exp(-7 * t_eval), n)[:, np.newaxis], + decimal=4, + ) + + # More complicated model + # Create model + model = pybamm.BaseModel() + # Set length scales to avoid warning + model.length_scales = {"negative electrode": 1} + var = pybamm.Variable("var", "negative electrode") + p = pybamm.InputParameter("p") + q = pybamm.InputParameter("q") + r = pybamm.InputParameter("r") + s = pybamm.InputParameter("s") + model.rhs = {var: p * q} + model.initial_conditions = {var: r} + model.variables = {"var times s": var * s} + + # Discretise + disc.process_model(model) + + # Solve + # Make sure that passing in extra options works + solver = pybamm.CasadiSolver( + rtol=1e-10, atol=1e-10, solve_sensitivity_equations=True + ) + t_eval = np.linspace(0, 1, 80) + solution = solver.solve( + model, t_eval, inputs={"p": 0.1, "q": 2, "r": -1, "s": 0.5} + ) + np.testing.assert_allclose(solution.y, np.tile(-1 + 0.2 * solution.t, (n, 1))) + np.testing.assert_allclose( + solution.sensitivity["p"], np.repeat(2 * solution.t, n)[:, np.newaxis], + ) + np.testing.assert_allclose( + solution.sensitivity["q"], np.repeat(0.1 * solution.t, n)[:, np.newaxis], + ) + np.testing.assert_allclose(solution.sensitivity["r"], 1) + np.testing.assert_allclose(solution.sensitivity["s"], 0) + np.testing.assert_allclose( + solution.sensitivity["all"], + np.hstack( + [ + solution.sensitivity["p"], + solution.sensitivity["q"], + solution.sensitivity["r"], + solution.sensitivity["s"], + ] + ), + ) + np.testing.assert_allclose( + solution["var times s"].data, np.tile(0.5 * (-1 + 0.2 * solution.t), (n, 1)) + ) + np.testing.assert_allclose( + solution["var times s"].sensitivity["p"], + np.repeat(0.5 * (2 * solution.t), n)[:, np.newaxis], + ) + np.testing.assert_allclose( + solution["var times s"].sensitivity["q"], + np.repeat(0.5 * (0.1 * solution.t), n)[:, np.newaxis], + ) + np.testing.assert_allclose(solution["var times s"].sensitivity["r"], 0.5) + np.testing.assert_allclose( + solution["var times s"].sensitivity["s"], + np.repeat(-1 + 0.2 * solution.t, n)[:, np.newaxis], + ) + np.testing.assert_allclose( + solution["var times s"].sensitivity["all"], + np.hstack( + [ + solution["var times s"].sensitivity["p"], + solution["var times s"].sensitivity["q"], + solution["var times s"].sensitivity["r"], + solution["var times s"].sensitivity["s"], + ] + ), + ) + + def test_solve_sensitivity_scalar_var_vector_input(self): + var = pybamm.Variable("var", "negative electrode") + model = pybamm.BaseModel() + # Set length scales to avoid warning + model.length_scales = {"negative electrode": 1} + + param = pybamm.InputParameter("param", "negative electrode") + model.rhs = {var: -param * var} + model.initial_conditions = {var: 2} + model.variables = { + "var": var, + "integral of var": pybamm.Integral(var, pybamm.standard_spatial_vars.x_n), + } + + # create discretisation + mesh = get_mesh_for_testing(xpts=5) + spatial_methods = {"macroscale": pybamm.FiniteVolume()} + disc = pybamm.Discretisation(mesh, spatial_methods) + disc.process_model(model) + n = disc.mesh["negative electrode"].npts + + # Solve - constant input + solver = pybamm.CasadiSolver( + mode="fast", rtol=1e-10, atol=1e-10, solve_sensitivity_equations=True + ) + t_eval = np.linspace(0, 1) + solution = solver.solve(model, t_eval, inputs={"param": 7 * np.ones(n)}) + l_n = mesh["negative electrode"].edges[-1] + np.testing.assert_array_almost_equal( + solution["var"].data, np.tile(2 * np.exp(-7 * t_eval), (n, 1)), decimal=4, + ) + + np.testing.assert_array_almost_equal( + solution["var"].sensitivity["param"], + np.vstack([np.eye(n) * -2 * t * np.exp(-7 * t) for t in t_eval]), + ) + np.testing.assert_array_almost_equal( + solution["integral of var"].data, 2 * np.exp(-7 * t_eval) * l_n, decimal=4, + ) + np.testing.assert_array_almost_equal( + solution["integral of var"].sensitivity["param"], + np.tile(-2 * t_eval * np.exp(-7 * t_eval) * l_n / n, (n, 1)).T, + ) + + # Solve - linspace input + p_eval = np.linspace(1, 2, n) + solution = solver.solve(model, t_eval, inputs={"param": p_eval}) + l_n = mesh["negative electrode"].edges[-1] + np.testing.assert_array_almost_equal( + solution["var"].data, 2 * np.exp(-p_eval[:, np.newaxis] * t_eval), decimal=4 + ) + np.testing.assert_array_almost_equal( + solution["var"].sensitivity["param"], + np.vstack([np.diag(-2 * t * np.exp(-p_eval * t)) for t in t_eval]), + ) + + np.testing.assert_array_almost_equal( + solution["integral of var"].data, + np.sum( + 2 + * np.exp(-p_eval[:, np.newaxis] * t_eval) + * mesh["negative electrode"].d_edges[:, np.newaxis], + axis=0, + ), + ) + np.testing.assert_array_almost_equal( + solution["integral of var"].sensitivity["param"], + np.vstack([-2 * t * np.exp(-p_eval * t) * l_n / n for t in t_eval]), + ) + + class TestCasadiSolverDAEsWithForwardSensitivityEquations(unittest.TestCase): def test_solve_sensitivity_scalar_var_scalar_input(self): # Create model @@ -992,177 +994,37 @@ def test_solve_sensitivity_scalar_var_scalar_input(self): ), ) - # def test_solve_sensitivity_vector_var_scalar_input(self): - # var = pybamm.Variable("var", "negative electrode") - # model = pybamm.BaseModel() - # # Set length scales to avoid warning - # model.length_scales = {"negative electrode": 1} - # param = pybamm.InputParameter("param") - # model.rhs = {var: -param * var} - # model.initial_conditions = {var: 2} - # model.variables = {"var": var} - - # # create discretisation - # disc = get_discretisation_for_testing() - # disc.process_model(model) - # n = disc.mesh["negative electrode"].npts - - # # Solve - scalar input - # solver = pybamm.CasadiSolver(solve_sensitivity_equations=True) - # t_eval = np.linspace(0, 1) - # solution = solver.solve(model, t_eval, inputs={"param": 7}) - # np.testing.assert_array_almost_equal( - # solution["var"].data, np.tile(2 * np.exp(-7 * t_eval), (n, 1)), decimal=4, - # ) - # np.testing.assert_array_almost_equal( - # solution["var"].sensitivity["param"], - # np.repeat(-2 * t_eval * np.exp(-7 * t_eval), n)[:, np.newaxis], - # decimal=4, - # ) - - # # More complicated model - # # Create model - # model = pybamm.BaseModel() - # # Set length scales to avoid warning - # model.length_scales = {"negative electrode": 1} - # var = pybamm.Variable("var", "negative electrode") - # p = pybamm.InputParameter("p") - # q = pybamm.InputParameter("q") - # r = pybamm.InputParameter("r") - # s = pybamm.InputParameter("s") - # model.rhs = {var: p * q} - # model.initial_conditions = {var: r} - # model.variables = {"var times s": var * s} - - # # Discretise - # disc.process_model(model) - - # # Solve - # # Make sure that passing in extra options works - # solver = pybamm.CasadiSolver( - # rtol=1e-10, atol=1e-10, solve_sensitivity_equations=True - # ) - # t_eval = np.linspace(0, 1, 80) - # solution = solver.solve( - # model, t_eval, inputs={"p": 0.1, "q": 2, "r": -1, "s": 0.5} - # ) - # np.testing.assert_allclose(solution.y, np.tile(-1 + 0.2 * solution.t, (n, 1))) - # np.testing.assert_allclose( - # solution.sensitivity["p"], np.repeat(2 * solution.t, n)[:, np.newaxis], - # ) - # np.testing.assert_allclose( - # solution.sensitivity["q"], np.repeat(0.1 * solution.t, n)[:, np.newaxis], - # ) - # np.testing.assert_allclose(solution.sensitivity["r"], 1) - # np.testing.assert_allclose(solution.sensitivity["s"], 0) - # np.testing.assert_allclose( - # solution.sensitivity["all"], - # np.hstack( - # [ - # solution.sensitivity["p"], - # solution.sensitivity["q"], - # solution.sensitivity["r"], - # solution.sensitivity["s"], - # ] - # ), - # ) - # np.testing.assert_allclose( - # solution["var times s"].data, np.tile(0.5 * (-1 + 0.2 * solution.t), (n, 1)) - # ) - # np.testing.assert_allclose( - # solution["var times s"].sensitivity["p"], - # np.repeat(0.5 * (2 * solution.t), n)[:, np.newaxis], - # ) - # np.testing.assert_allclose( - # solution["var times s"].sensitivity["q"], - # np.repeat(0.5 * (0.1 * solution.t), n)[:, np.newaxis], - # ) - # np.testing.assert_allclose(solution["var times s"].sensitivity["r"], 0.5) - # np.testing.assert_allclose( - # solution["var times s"].sensitivity["s"], - # np.repeat(-1 + 0.2 * solution.t, n)[:, np.newaxis], - # ) - # np.testing.assert_allclose( - # solution["var times s"].sensitivity["all"], - # np.hstack( - # [ - # solution["var times s"].sensitivity["p"], - # solution["var times s"].sensitivity["q"], - # solution["var times s"].sensitivity["r"], - # solution["var times s"].sensitivity["s"], - # ] - # ), - # ) - - # def test_solve_sensitivity_scalar_var_vector_input(self): - # var = pybamm.Variable("var", "negative electrode") - # model = pybamm.BaseModel() - # # Set length scales to avoid warning - # model.length_scales = {"negative electrode": 1} - - # param = pybamm.InputParameter("param", "negative electrode") - # model.rhs = {var: -param * var} - # model.initial_conditions = {var: 2} - # model.variables = { - # "var": var, - # "integral of var": pybamm.Integral(var, pybamm.standard_spatial_vars.x_n), - # } - - # # create discretisation - # mesh = get_mesh_for_testing(xpts=5) - # spatial_methods = {"macroscale": pybamm.FiniteVolume()} - # disc = pybamm.Discretisation(mesh, spatial_methods) - # disc.process_model(model) - # n = disc.mesh["negative electrode"].npts - - # # Solve - constant input - # solver = pybamm.CasadiSolver( - # mode="fast", rtol=1e-10, atol=1e-10, solve_sensitivity_equations=True - # ) - # t_eval = np.linspace(0, 1) - # solution = solver.solve(model, t_eval, inputs={"param": 7 * np.ones(n)}) - # l_n = mesh["negative electrode"].edges[-1] - # np.testing.assert_array_almost_equal( - # solution["var"].data, np.tile(2 * np.exp(-7 * t_eval), (n, 1)), decimal=4, - # ) - - # np.testing.assert_array_almost_equal( - # solution["var"].sensitivity["param"], - # np.vstack([np.eye(n) * -2 * t * np.exp(-7 * t) for t in t_eval]), - # ) - # np.testing.assert_array_almost_equal( - # solution["integral of var"].data, 2 * np.exp(-7 * t_eval) * l_n, decimal=4, - # ) - # np.testing.assert_array_almost_equal( - # solution["integral of var"].sensitivity["param"], - # np.tile(-2 * t_eval * np.exp(-7 * t_eval) * l_n / n, (n, 1)).T, - # ) - - # # Solve - linspace input - # p_eval = np.linspace(1, 2, n) - # solution = solver.solve(model, t_eval, inputs={"param": p_eval}) - # l_n = mesh["negative electrode"].edges[-1] - # np.testing.assert_array_almost_equal( - # solution["var"].data, 2 * np.exp(-p_eval[:, np.newaxis] * t_eval), decimal=4 - # ) - # np.testing.assert_array_almost_equal( - # solution["var"].sensitivity["param"], - # np.vstack([np.diag(-2 * t * np.exp(-p_eval * t)) for t in t_eval]), - # ) - - # np.testing.assert_array_almost_equal( - # solution["integral of var"].data, - # np.sum( - # 2 - # * np.exp(-p_eval[:, np.newaxis] * t_eval) - # * mesh["negative electrode"].d_edges[:, np.newaxis], - # axis=0, - # ), - # ) - # np.testing.assert_array_almost_equal( - # solution["integral of var"].sensitivity["param"], - # np.vstack([-2 * t * np.exp(-p_eval * t) * l_n / n for t in t_eval]), - # ) + def test_solve_sensitivity_vector_var_scalar_input(self): + var = pybamm.Variable("var", "negative electrode") + var2 = pybamm.Variable("var2", "negative electrode") + model = pybamm.BaseModel() + # Set length scales to avoid warning + model.length_scales = {"negative electrode": 1} + param = pybamm.InputParameter("param") + model.rhs = {var: -param * var} + model.algebraic = {var2: var2 - param} + model.initial_conditions = {var: 2, var2: param} + model.variables = {"prod": var * var2} + + # create discretisation + disc = get_discretisation_for_testing() + disc.process_model(model) + n = disc.mesh["negative electrode"].npts + + # Solve - scalar input + solver = pybamm.CasadiSolver(solve_sensitivity_equations=True) + t_eval = np.linspace(0, 1) + solution = solver.solve(model, t_eval, inputs={"param": 7}) + np.testing.assert_array_almost_equal( + solution["prod"].data, + np.tile(2 * 7 * np.exp(-7 * t_eval), (n, 1)), + decimal=4, + ) + np.testing.assert_array_almost_equal( + solution["prod"].sensitivity["param"], + np.repeat(2 * (1 - 7 * t_eval) * np.exp(-7 * t_eval), n)[:, np.newaxis], + decimal=4, + ) if __name__ == "__main__": From 0cfdf6f293ebf065c8a70876e40c73cf220986d8 Mon Sep 17 00:00:00 2001 From: Valentin Sulzer Date: Wed, 22 Jul 2020 10:59:53 -0400 Subject: [PATCH 07/73] #1100 reformatted sensitivity API --- pybamm/solvers/base_solver.py | 11 +- pybamm/solvers/casadi_algebraic_solver.py | 51 ++- pybamm/solvers/casadi_solver.py | 2 +- pybamm/solvers/processed_variable.py | 125 +++++- pybamm/solvers/scipy_solver.py | 2 +- pybamm/solvers/solution.py | 9 +- .../test_casadi_algebraic_solver.py | 388 +++++------------- tests/unit/test_solvers/test_casadi_solver.py | 22 +- 8 files changed, 296 insertions(+), 314 deletions(-) diff --git a/pybamm/solvers/base_solver.py b/pybamm/solvers/base_solver.py index d2b78b1e1c..645fc1cb4c 100644 --- a/pybamm/solvers/base_solver.py +++ b/pybamm/solvers/base_solver.py @@ -31,12 +31,15 @@ class BaseSolver(object): specified by 'root_method' (e.g. "lm", "hybr", ...) root_tol : float, optional The tolerance for the initial-condition solver (default is 1e-6). - sensitivity : bool, optional - Whether to explicitly formulate the sensitivity equations for sensitivity - to input parameters. The formulation is as per "Park, S., Kato, D., Gima, Z., + sensitivity : str, optional + Whether (and how) to calculate sensitivities when solving. Options are: + + - "explicit forward": explicitly formulate the sensitivity equations. + The formulation is as per "Park, S., Kato, D., Gima, Z., Klein, R., & Moura, S. (2018). Optimal experimental design for parameterization of an electrochemical lithium-ion battery model. Journal of The Electrochemical Society, 165(7), A1309.". See #1100 for details + - see specific solvers for other options """ def __init__( @@ -47,7 +50,7 @@ def __init__( root_method=None, root_tol=1e-6, max_steps="deprecated", - sensitivity=False, + sensitivity=None, ): self._method = method self._rtol = rtol diff --git a/pybamm/solvers/casadi_algebraic_solver.py b/pybamm/solvers/casadi_algebraic_solver.py index 798793858c..d18a2849a7 100644 --- a/pybamm/solvers/casadi_algebraic_solver.py +++ b/pybamm/solvers/casadi_algebraic_solver.py @@ -3,6 +3,7 @@ # import casadi import pybamm +import numbers import numpy as np @@ -21,10 +22,18 @@ class CasadiAlgebraicSolver(pybamm.BaseSolver): Any options to pass to the CasADi rootfinder. Please consult `CasADi documentation `_ for details. + sensitivity : str, optional + Whether (and how) to calculate sensitivities when solving. Options are: + + - None: no sensitivities + - "explicit forward": explicitly formulate the sensitivity equations. + See :class:`pybamm.BaseSolver` + - "casadi": use casadi to differentiate through the rootfinding operator + """ - def __init__(self, tol=1e-6, extra_options=None): - super().__init__() + def __init__(self, tol=1e-6, extra_options=None, sensitivity=None): + super().__init__(sensitivity=sensitivity) self.tol = tol self.name = "CasADi algebraic solver" self.algebraic_solver = True @@ -57,14 +66,24 @@ def _integrate(self, model, t_eval, inputs=None): """ # Record whether there are any symbolic inputs inputs = inputs or {} - has_symbolic_inputs = any(isinstance(v, casadi.MX) for v in inputs.values()) - symbolic_inputs = casadi.vertcat( - *[v for v in inputs.values() if isinstance(v, casadi.MX)] - ) - # Create casadi objects for the root-finder + inputs_dict = inputs inputs = casadi.vertcat(*[v for v in inputs.values()]) + if self.sensitivity == "casadi" and inputs_dict != {}: + # Create symbolic inputs for sensitivity analysis + symbolic_inputs_list = [] + for name, value in inputs_dict.items(): + if isinstance(value, numbers.Number): + symbolic_inputs_list.append(casadi.MX.sym(name)) + else: + symbolic_inputs_list.append(casadi.MX.sym(name, value.shape[0])) + symbolic_inputs = casadi.vertcat(*[p for p in symbolic_inputs_list]) + inputs_for_alg = symbolic_inputs + else: + symbolic_inputs = casadi.DM() + inputs_for_alg = inputs + y0 = model.y0 # 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 @@ -91,7 +110,7 @@ def _integrate(self, model, t_eval, inputs=None): y_sym = casadi.vertcat(y0_diff, y_alg_sym) t_and_inputs_sym = casadi.vertcat(t_sym, symbolic_inputs) - alg = model.casadi_algebraic(t_sym, y_sym, inputs) + alg = model.casadi_algebraic(t_sym, y_sym, inputs_for_alg) # Set constraints vector in the casadi format # Constrain the unknowns. 0 (default): no constraint on ui, 1: ui >= 0.0, @@ -116,8 +135,8 @@ def _integrate(self, model, t_eval, inputs=None): for idx, t in enumerate(t_eval): # Evaluate algebraic with new t and previous y0, if it's already close # enough then keep it - # We can't do this if there are symbolic inputs - if has_symbolic_inputs is False and np.all( + # We can't do this if also doing sensitivity + if self.sensitivity != "casadi" and np.all( abs(model.casadi_algebraic(t, y0, inputs).full()) < self.tol ): pybamm.logger.debug( @@ -144,9 +163,9 @@ def _integrate(self, model, t_eval, inputs=None): fun = None # If there are no symbolic inputs, check the function is below the tol - # Skip this check if there are symbolic inputs + # Skip this check if also doing sensitivity if success and ( - has_symbolic_inputs is True or np.all(casadi.fabs(fun) < self.tol) + self.sensitivity == "casadi" or np.all(casadi.fabs(fun) < self.tol) ): # update initial guess for the next iteration y0_alg = y_alg_sol @@ -173,5 +192,11 @@ def _integrate(self, model, t_eval, inputs=None): # Concatenate differential part y_diff = casadi.horzcat(*[y0_diff] * len(t_eval)) y_sol = casadi.vertcat(y_diff, y_alg) + + # If doing sensitivity, return the solution as a function of the inputs + if self.sensitivity == "casadi": + y_sol = casadi.Function("y_sol", [symbolic_inputs], [y_sol]) # Return solution object (no events, so pass None to t_event, y_event) - return pybamm.Solution(t_eval, y_sol, termination="success") + return pybamm.Solution( + t_eval, y_sol, termination="success", model=model, inputs=inputs_dict + ) diff --git a/pybamm/solvers/casadi_solver.py b/pybamm/solvers/casadi_solver.py index 663ee8ff94..ad321d8ce9 100644 --- a/pybamm/solvers/casadi_solver.py +++ b/pybamm/solvers/casadi_solver.py @@ -75,7 +75,7 @@ def __init__( dt_max=None, extra_options_setup=None, extra_options_call=None, - sensitivity=False, + sensitivity=None, ): super().__init__( "problem dependent", diff --git a/pybamm/solvers/processed_variable.py b/pybamm/solvers/processed_variable.py index 531ee77f00..2730557c82 100644 --- a/pybamm/solvers/processed_variable.py +++ b/pybamm/solvers/processed_variable.py @@ -64,6 +64,40 @@ def __init__(self, base_variable, solution, known_evals=None, warn=True): self._sensitivity = None self.solution_sensitivity = solution.sensitivity + # Special case: symbolic solution, with casadi + if isinstance(solution.y, casadi.Function): + # Evaluate solution at specific inputs value + inputs_stacked = casadi.vertcat(*solution.inputs.values()) + self.u_sol = solution.y(inputs_stacked).full() + # Convert variable to casadi + t_MX = casadi.MX.sym("t") + y_MX = casadi.MX.sym("y", self.u_sol.shape[0]) + # Make all inputs symbolic first for converting to casadi + symbolic_inputs_dict = { + name: casadi.MX.sym(name, value.shape[0]) + for name, value in solution.inputs.items() + } + + # The symbolic_inputs will be used for sensitivity + symbolic_inputs = casadi.vertcat(*symbolic_inputs_dict.values()) + try: + var_casadi = base_variable.to_casadi( + t_MX, y_MX, inputs=symbolic_inputs_dict + ) + except: + n = 1 + self.base_variable_sym = casadi.Function( + "variable", [t_MX, y_MX, symbolic_inputs], [var_casadi] + ) + # Store symbolic inputs for sensitivity + self.symbolic_inputs = symbolic_inputs + self.y_sym = solution.y(symbolic_inputs) + else: + self.u_sol = solution.y + self.base_variable_sym = None + self.symbolic_inputs = None + self.y_sym = None + # Set timescale self.timescale = solution.model.timescale.evaluate() self.t_pts = self.t_sol * self.timescale @@ -78,8 +112,8 @@ def __init__(self, base_variable, solution, known_evals=None, warn=True): # Evaluate base variable at initial time if self.known_evals: self.base_eval, self.known_evals[solution.t[0]] = base_variable.evaluate( - solution.t[0], - solution.y[:, 0], + self.t_sol[0], + self.u_sol[:, 0], inputs={name: inp[:, 0] for name, inp in solution.inputs.items()}, known_evals=self.known_evals[solution.t[0]], ) @@ -571,10 +605,20 @@ def sensitivity(self): return {} # Otherwise initialise and return sensitivity if self._sensitivity is None: - self.initialise_sensitivity() + # Check that we can compute sensitivities + if self.base_variable_sym is None and self.solution_sensitivity == {}: + raise ValueError( + "Cannot compute sensitivities. The 'sensitivity' argument of the " + "solver should be changed from 'None' to allow sensitivity " + "calculations. Check solver documentation for details." + ) + if self.base_variable_sym is None: + self.initialise_sensitivity_explicit_forward() + else: + self.initialise_sensitivity_casadi() return self._sensitivity - def initialise_sensitivity(self): + def initialise_sensitivity_explicit_forward(self): "Set up the sensitivity dictionary" inputs_stacked = casadi.vertcat(*[p for p in self.inputs.values()]) @@ -628,6 +672,79 @@ def initialise_sensitivity(self): # Save attribute self._sensitivity = sensitivity + def initialise_sensitivity_casadi(self): + def initialise_0D_symbolic(): + "Create a 0D symbolic variable" + # Evaluate the base_variable index-by-index + for idx in range(len(self.t_sol)): + t = self.t_sol[idx] + u = self.y_sym[:, idx] + next_entries = self.base_variable_sym(t, u, self.symbolic_inputs) + if idx == 0: + entries = next_entries + else: + entries = casadi.horzcat(entries, next_entries) + + return entries + + def initialise_1D_symbolic(): + "Create a 1D symbolic variable" + # Evaluate the base_variable index-by-index + for idx in range(len(self.t_sol)): + t = self.t_sol[idx] + u = self.y_sym[:, idx] + next_entries = self.base_variable_sym(t, u, self.symbolic_inputs) + if idx == 0: + entries = next_entries + else: + entries = casadi.vertcat(entries, next_entries) + + return entries + + inputs_stacked = casadi.vertcat(*self.inputs.values()) + self.base_eval = self.base_variable_sym( + self.t_sol[0], self.u_sol[:, 0], inputs_stacked + ) + if ( + isinstance(self.base_eval, numbers.Number) + or len(self.base_eval.shape) == 0 + or self.base_eval.shape[0] == 1 + ): + entries_MX = initialise_0D_symbolic() + else: + n = self.mesh.npts + base_shape = self.base_eval.shape[0] + # Try shape that could make the variable a 1D variable + if base_shape == n: + entries_MX = initialise_1D_symbolic() + else: + # Raise error for 2D variable + raise NotImplementedError( + "Shape not recognized for {} ".format(self.base_variable) + + "(note processing of 2D and 3D variables is not yet " + + "implemented)" + ) + + # Make entries a function and compute jacobian + casadi_entries_fn = casadi.Function( + "variable", [self.symbolic_inputs], [entries_MX] + ) + + sens_MX = casadi.jacobian(entries_MX, self.symbolic_inputs) + casadi_sens_fn = casadi.Function("variable", [self.symbolic_inputs], [sens_MX]) + + sens_eval = casadi_sens_fn(inputs_stacked) + sensitivity = {"all": sens_eval} + + # Add the individual sensitivity + start = 0 + for name, inp in self.inputs.items(): + end = start + inp.shape[0] + sensitivity[name] = sens_eval[:, start:end] + start = end + + self._sensitivity = sensitivity + def eval_dimension_name(name, x, r, y, z): if name == "x": diff --git a/pybamm/solvers/scipy_solver.py b/pybamm/solvers/scipy_solver.py index d96ef0aefd..eb5c7a5ba2 100644 --- a/pybamm/solvers/scipy_solver.py +++ b/pybamm/solvers/scipy_solver.py @@ -29,7 +29,7 @@ class ScipySolver(pybamm.BaseSolver): """ def __init__( - self, method="BDF", rtol=1e-6, atol=1e-6, extra_options=None, sensitivity=False, + self, method="BDF", rtol=1e-6, atol=1e-6, extra_options=None, sensitivity=None, ): super().__init__( method=method, rtol=rtol, atol=atol, sensitivity=sensitivity, diff --git a/pybamm/solvers/solution.py b/pybamm/solvers/solution.py index 49e9817385..bc97fbaec2 100644 --- a/pybamm/solvers/solution.py +++ b/pybamm/solvers/solution.py @@ -64,7 +64,13 @@ def __init__( # If the model has been provided, split up y into solution and sensitivity # Don't do this if the sensitivity equations have not been computed (i.e. if # y only has the shape or the rhs and alg solution) - if model is None or model.len_rhs_and_alg == y.shape[0]: + # Don't do this if y is symbolic (sensitivities will be calculated a different + # way) + if ( + model is None + or isinstance(y, casadi.Function) + or model.len_rhs_and_alg == y.shape[0] + ): self._y = y self.sensitivity = {} else: @@ -243,7 +249,6 @@ def update(self, variables): var = pybamm.ProcessedVariable( self.model.variables[key], self, self._known_evals ) - # Update known_evals in order to process any other variables faster for t in var.known_evals: self._known_evals[t].update(var.known_evals[t]) diff --git a/tests/unit/test_solvers/test_casadi_algebraic_solver.py b/tests/unit/test_solvers/test_casadi_algebraic_solver.py index cfd9562296..1057b4b08d 100644 --- a/tests/unit/test_solvers/test_casadi_algebraic_solver.py +++ b/tests/unit/test_solvers/test_casadi_algebraic_solver.py @@ -152,7 +152,7 @@ def test_solve_with_symbolic_input(self): disc.process_model(model) # Solve - solver = pybamm.CasadiAlgebraicSolver(sensitivity=True) + solver = pybamm.CasadiAlgebraicSolver(sensitivity="casadi") solution = solver.solve(model, [0], inputs={"param": 7}) np.testing.assert_array_equal(solution["var"].data, -7) np.testing.assert_array_equal(solution["var"].sensitivity["param"], -1) @@ -163,286 +163,112 @@ def test_solve_with_symbolic_input(self): np.testing.assert_array_equal(solution["var"].sensitivity["param"], -1) np.testing.assert_array_equal(solution["var"].sensitivity["all"], -1) - # def test_least_squares_fit(self): - # # Simple system: a single algebraic equation - # var = pybamm.Variable("var", domain="negative electrode") - # model = pybamm.BaseModel() - # p = pybamm.InputParameter("p") - # q = pybamm.InputParameter("q") - # model.algebraic = {var: (var - p)} - # model.initial_conditions = {var: 3} - # model.variables = {"objective": (var - q) ** 2 + (p - 3) ** 2} - - # # create discretisation - # disc = tests.get_discretisation_for_testing() - # disc.process_model(model) - - # # Solve - # solver = pybamm.CasadiAlgebraicSolver() - # solution = solver.solve(model, [0]) - # sol_var = solution["objective"] - - # def objective(x): - # return sol_var.value({"p": x[0], "q": x[1]}).full().flatten() - - # # without jacobian - # lsq_sol = least_squares(objective, [2, 2], method="lm") - # np.testing.assert_array_almost_equal(lsq_sol.x, [3, 3], decimal=3) - - # def jac(x): - # return sol_var.sensitivity({"p": x[0], "q": x[1]}) - - # # with jacobian - # lsq_sol = least_squares(objective, [2, 2], jac=jac, method="lm") - # np.testing.assert_array_almost_equal(lsq_sol.x, [3, 3], decimal=3) - - # def test_solve_with_symbolic_input_1D_scalar_input(self): - # var = pybamm.Variable("var", "negative electrode") - # model = pybamm.BaseModel() - # param = pybamm.InputParameter("param") - # model.algebraic = {var: var + param} - # model.initial_conditions = {var: 2} - # model.variables = {"var": var} - - # # create discretisation - # disc = tests.get_discretisation_for_testing() - # disc.process_model(model) - - # # Solve - scalar input - # solver = pybamm.CasadiAlgebraicSolver() - # solution = solver.solve(model, [0]) - # np.testing.assert_array_equal(solution["var"].value({"param": 7}), -7) - # np.testing.assert_array_equal(solution["var"].value({"param": 3}), -3) - # np.testing.assert_array_equal(solution["var"].sensitivity({"param": 3}), -1) - - # def test_solve_with_symbolic_input_1D_vector_input(self): - # var = pybamm.Variable("var", "negative electrode") - # model = pybamm.BaseModel() - # param = pybamm.InputParameter("param", "negative electrode") - # model.algebraic = {var: var + param} - # model.initial_conditions = {var: 2} - # model.variables = {"var": var} - - # # create discretisation - # disc = tests.get_discretisation_for_testing() - # disc.process_model(model) - - # # Solve - scalar input - # solver = pybamm.CasadiAlgebraicSolver() - # solution = solver.solve(model, [0]) - # n = disc.mesh["negative electrode"].npts - - # solver = pybamm.CasadiAlgebraicSolver() - # solution = solver.solve(model, [0]) - # p = np.linspace(0, 1, n)[:, np.newaxis] - # np.testing.assert_array_almost_equal( - # solution["var"].value({"param": 3 * np.ones(n)}), -3 - # ) - # np.testing.assert_array_almost_equal( - # solution["var"].value({"param": 2 * p}), -2 * p - # ) - # np.testing.assert_array_almost_equal( - # solution["var"].sensitivity({"param": 3 * np.ones(n)}), -np.eye(40) - # ) - # np.testing.assert_array_almost_equal( - # solution["var"].sensitivity({"param": p}), -np.eye(40) - # ) - - # def test_solve_with_symbolic_input_in_initial_conditions(self): - # # Simple system: a single algebraic equation - # var = pybamm.Variable("var") - # model = pybamm.BaseModel() - # model.algebraic = {var: var + 2} - # model.initial_conditions = {var: pybamm.InputParameter("param")} - # model.variables = {"var": var} - - # # create discretisation - # disc = pybamm.Discretisation() - # disc.process_model(model) - - # # Solve - # solver = pybamm.CasadiAlgebraicSolver() - # solution = solver.solve(model, [0]) - # np.testing.assert_array_equal(solution["var"].value({"param": 7}), -2) - # np.testing.assert_array_equal(solution["var"].value({"param": 3}), -2) - # np.testing.assert_array_equal(solution["var"].sensitivity({"param": 3}), 0) - - # def test_least_squares_fit_input_in_initial_conditions(self): - # # Simple system: a single algebraic equation - # var = pybamm.Variable("var", domain="negative electrode") - # model = pybamm.BaseModel() - # p = pybamm.InputParameter("p") - # q = pybamm.InputParameter("q") - # model.algebraic = {var: (var - p)} - # model.initial_conditions = {var: p} - # model.variables = {"objective": (var - q) ** 2 + (p - 3) ** 2} - - # # create discretisation - # disc = tests.get_discretisation_for_testing() - # disc.process_model(model) - - # # Solve - # solver = pybamm.CasadiAlgebraicSolver() - # solution = solver.solve(model, [0]) - # sol_var = solution["objective"] - - # def objective(x): - # return sol_var.value({"p": x[0], "q": x[1]}).full().flatten() - - # # without jacobian - # lsq_sol = least_squares(objective, [2, 2], method="lm") - # np.testing.assert_array_almost_equal(lsq_sol.x, [3, 3], decimal=3) - - -# class TestCasadiAlgebraicSolverSensitivity(unittest.TestCase): -# def test_solve_with_symbolic_input(self): -# # Simple system: a single algebraic equation -# var = pybamm.Variable("var") -# model = pybamm.BaseModel() -# model.algebraic = {var: var + pybamm.InputParameter("param")} -# model.initial_conditions = {var: 2} -# model.variables = {"var": var} - -# # create discretisation -# disc = pybamm.Discretisation() -# disc.process_model(model) - -# # Solve -# solver = pybamm.CasadiAlgebraicSolver() -# solution = solver.solve(model, [0]) -# np.testing.assert_array_equal(solution["var"].value({"param": 7}), -7) -# np.testing.assert_array_equal(solution["var"].value({"param": 3}), -3) -# np.testing.assert_array_equal(solution["var"].sensitivity({"param": 3}), -1) - -# def test_least_squares_fit(self): -# # Simple system: a single algebraic equation -# var = pybamm.Variable("var", domain="negative electrode") -# model = pybamm.BaseModel() -# p = pybamm.InputParameter("p") -# q = pybamm.InputParameter("q") -# model.algebraic = {var: (var - p)} -# model.initial_conditions = {var: 3} -# model.variables = {"objective": (var - q) ** 2 + (p - 3) ** 2} - -# # create discretisation -# disc = tests.get_discretisation_for_testing() -# disc.process_model(model) - -# # Solve -# solver = pybamm.CasadiAlgebraicSolver() -# solution = solver.solve(model, [0]) -# sol_var = solution["objective"] - -# def objective(x): -# return sol_var.value({"p": x[0], "q": x[1]}).full().flatten() - -# # without jacobian -# lsq_sol = least_squares(objective, [2, 2], method="lm") -# np.testing.assert_array_almost_equal(lsq_sol.x, [3, 3], decimal=3) - -# def jac(x): -# return sol_var.sensitivity({"p": x[0], "q": x[1]}) - -# # with jacobian -# lsq_sol = least_squares(objective, [2, 2], jac=jac, method="lm") -# np.testing.assert_array_almost_equal(lsq_sol.x, [3, 3], decimal=3) - -# def test_solve_with_symbolic_input_1D_scalar_input(self): -# var = pybamm.Variable("var", "negative electrode") -# model = pybamm.BaseModel() -# param = pybamm.InputParameter("param") -# model.algebraic = {var: var + param} -# model.initial_conditions = {var: 2} -# model.variables = {"var": var} - -# # create discretisation -# disc = tests.get_discretisation_for_testing() -# disc.process_model(model) - -# # Solve - scalar input -# solver = pybamm.CasadiAlgebraicSolver() -# solution = solver.solve(model, [0]) -# np.testing.assert_array_equal(solution["var"].value({"param": 7}), -7) -# np.testing.assert_array_equal(solution["var"].value({"param": 3}), -3) -# np.testing.assert_array_equal(solution["var"].sensitivity({"param": 3}), -1) - -# def test_solve_with_symbolic_input_1D_vector_input(self): -# var = pybamm.Variable("var", "negative electrode") -# model = pybamm.BaseModel() -# param = pybamm.InputParameter("param", "negative electrode") -# model.algebraic = {var: var + param} -# model.initial_conditions = {var: 2} -# model.variables = {"var": var} - -# # create discretisation -# disc = tests.get_discretisation_for_testing() -# disc.process_model(model) - -# # Solve - scalar input -# solver = pybamm.CasadiAlgebraicSolver() -# solution = solver.solve(model, [0]) -# n = disc.mesh["negative electrode"].npts - -# solver = pybamm.CasadiAlgebraicSolver() -# solution = solver.solve(model, [0]) -# p = np.linspace(0, 1, n)[:, np.newaxis] -# np.testing.assert_array_almost_equal( -# solution["var"].value({"param": 3 * np.ones(n)}), -3 -# ) -# np.testing.assert_array_almost_equal( -# solution["var"].value({"param": 2 * p}), -2 * p -# ) -# np.testing.assert_array_almost_equal( -# solution["var"].sensitivity({"param": 3 * np.ones(n)}), -np.eye(40) -# ) -# np.testing.assert_array_almost_equal( -# solution["var"].sensitivity({"param": p}), -np.eye(40) -# ) - -# def test_solve_with_symbolic_input_in_initial_conditions(self): -# # Simple system: a single algebraic equation -# var = pybamm.Variable("var") -# model = pybamm.BaseModel() -# model.algebraic = {var: var + 2} -# model.initial_conditions = {var: pybamm.InputParameter("param")} -# model.variables = {"var": var} - -# # create discretisation -# disc = pybamm.Discretisation() -# disc.process_model(model) - -# # Solve -# solver = pybamm.CasadiAlgebraicSolver() -# solution = solver.solve(model, [0]) -# np.testing.assert_array_equal(solution["var"].value({"param": 7}), -2) -# np.testing.assert_array_equal(solution["var"].value({"param": 3}), -2) -# np.testing.assert_array_equal(solution["var"].sensitivity({"param": 3}), 0) - -# def test_least_squares_fit_input_in_initial_conditions(self): -# # Simple system: a single algebraic equation -# var = pybamm.Variable("var", domain="negative electrode") -# model = pybamm.BaseModel() -# p = pybamm.InputParameter("p") -# q = pybamm.InputParameter("q") -# model.algebraic = {var: (var - p)} -# model.initial_conditions = {var: p} -# model.variables = {"objective": (var - q) ** 2 + (p - 3) ** 2} - -# # create discretisation -# disc = tests.get_discretisation_for_testing() -# disc.process_model(model) - -# # Solve -# solver = pybamm.CasadiAlgebraicSolver() -# solution = solver.solve(model, [0]) -# sol_var = solution["objective"] - -# def objective(x): -# return sol_var.value({"p": x[0], "q": x[1]}).full().flatten() - -# # without jacobian -# lsq_sol = least_squares(objective, [2, 2], method="lm") -# np.testing.assert_array_almost_equal(lsq_sol.x, [3, 3], decimal=3) + def test_least_squares_fit(self): + # Simple system: a single algebraic equation + var = pybamm.Variable("var", domain="negative electrode") + model = pybamm.BaseModel() + # Set length scale to avoid warning + model.length_scales = {"negative electrode": 1} + + p = pybamm.InputParameter("p") + q = pybamm.InputParameter("q") + model.algebraic = {var: (var - p)} + model.initial_conditions = {var: 3} + model.variables = {"objective": (var - q) ** 2 + (p - 3) ** 2} + + # create discretisation + disc = tests.get_discretisation_for_testing() + disc.process_model(model) + + # Solve + solver = pybamm.CasadiAlgebraicSolver(sensitivity="casadi") + + def objective(x): + solution = solver.solve(model, [0], inputs={"p": x[0], "q": x[1]}) + return solution["objective"].data.flatten() + + # without jacobian + lsq_sol = least_squares(objective, [2, 2], method="lm") + np.testing.assert_array_almost_equal(lsq_sol.x, [3, 3], decimal=3) + + def jac(x): + solution = solver.solve(model, [0], inputs={"p": x[0], "q": x[1]}) + return solution["objective"].sensitivity["all"] + + # with jacobian + lsq_sol = least_squares(objective, [2, 2], jac=jac, method="lm") + np.testing.assert_array_almost_equal(lsq_sol.x, [3, 3], decimal=3) + + def test_solve_with_symbolic_input_vector_variable_scalar_input(self): + var = pybamm.Variable("var", "negative electrode") + model = pybamm.BaseModel() + # Set length scale to avoid warning + model.length_scales = {"negative electrode": 1} + param = pybamm.InputParameter("param") + model.algebraic = {var: var + param} + model.initial_conditions = {var: 2} + model.variables = {"var": var} + + # create discretisation + disc = tests.get_discretisation_for_testing() + disc.process_model(model) + + # Solve - scalar input + solver = pybamm.CasadiAlgebraicSolver(sensitivity="casadi") + solution = solver.solve(model, [0], inputs={"param": 7}) + np.testing.assert_array_equal(solution["var"].data, -7) + solution = solver.solve(model, [0], inputs={"param": 3}) + np.testing.assert_array_equal(solution["var"].data, -3) + np.testing.assert_array_equal(solution["var"].sensitivity["param"], -1) + + def test_solve_with_symbolic_input_vector_variable_vector_input(self): + var = pybamm.Variable("var", "negative electrode") + model = pybamm.BaseModel() + # Set length scale to avoid warning + model.length_scales = {"negative electrode": 1} + param = pybamm.InputParameter("param", "negative electrode") + model.algebraic = {var: var + param} + model.initial_conditions = {var: 2} + model.variables = {"var": var} + + # create discretisation + disc = tests.get_discretisation_for_testing() + disc.process_model(model) + n = disc.mesh["negative electrode"].npts + + # Solve - vector input + solver = pybamm.CasadiAlgebraicSolver(sensitivity="casadi") + solution = solver.solve(model, [0], inputs={"param": 3 * np.ones(n)}) + + np.testing.assert_array_almost_equal(solution["var"].data, -3) + np.testing.assert_array_almost_equal( + solution["var"].sensitivity["param"], -np.eye(40) + ) + + p = np.linspace(0, 1, n)[:, np.newaxis] + solution = solver.solve(model, [0], inputs={"param": 2 * p}) + np.testing.assert_array_almost_equal(solution["var"].data, -2 * p) + np.testing.assert_array_almost_equal( + solution["var"].sensitivity["param"], -np.eye(40) + ) + + def test_solve_with_symbolic_input_in_initial_conditions(self): + # Simple system: a single algebraic equation + var = pybamm.Variable("var") + model = pybamm.BaseModel() + model.algebraic = {var: var + 2} + model.initial_conditions = {var: pybamm.InputParameter("param")} + model.variables = {"var": var} + + # create discretisation + disc = pybamm.Discretisation() + disc.process_model(model) + + # Solve + solver = pybamm.CasadiAlgebraicSolver(sensitivity="casadi") + solution = solver.solve(model, [0], inputs={"param": 7}) + np.testing.assert_array_equal(solution["var"].data, -2) + np.testing.assert_array_equal(solution["var"].sensitivity["param"], 0) if __name__ == "__main__": diff --git a/tests/unit/test_solvers/test_casadi_solver.py b/tests/unit/test_solvers/test_casadi_solver.py index 1a407cc8ed..35bd2dfeb3 100644 --- a/tests/unit/test_solvers/test_casadi_solver.py +++ b/tests/unit/test_solvers/test_casadi_solver.py @@ -622,7 +622,7 @@ def test_solve_sensitivity_scalar_var_scalar_input(self): # Solve # Make sure that passing in extra options works solver = pybamm.CasadiSolver( - mode="fast", rtol=1e-10, atol=1e-10, sensitivity=True + mode="fast", rtol=1e-10, atol=1e-10, sensitivity="explicit forward" ) t_eval = np.linspace(0, 1, 80) solution = solver.solve(model, t_eval, inputs={"p": 0.1}) @@ -656,7 +656,9 @@ def test_solve_sensitivity_scalar_var_scalar_input(self): # Solve # Make sure that passing in extra options works - solver = pybamm.CasadiSolver(rtol=1e-10, atol=1e-10, sensitivity=True) + solver = pybamm.CasadiSolver( + rtol=1e-10, atol=1e-10, sensitivity="explicit forward" + ) t_eval = np.linspace(0, 1, 80) solution = solver.solve( model, t_eval, inputs={"p": 0.1, "q": 2, "r": -1, "s": 0.5} @@ -725,7 +727,7 @@ def test_solve_sensitivity_vector_var_scalar_input(self): n = disc.mesh["negative electrode"].npts # Solve - scalar input - solver = pybamm.CasadiSolver(sensitivity=True) + solver = pybamm.CasadiSolver(sensitivity="explicit forward") t_eval = np.linspace(0, 1) solution = solver.solve(model, t_eval, inputs={"param": 7}) np.testing.assert_array_almost_equal( @@ -756,7 +758,9 @@ def test_solve_sensitivity_vector_var_scalar_input(self): # Solve # Make sure that passing in extra options works - solver = pybamm.CasadiSolver(rtol=1e-10, atol=1e-10, sensitivity=True) + solver = pybamm.CasadiSolver( + rtol=1e-10, atol=1e-10, sensitivity="explicit forward" + ) t_eval = np.linspace(0, 1, 80) solution = solver.solve( model, t_eval, inputs={"p": 0.1, "q": 2, "r": -1, "s": 0.5} @@ -832,7 +836,7 @@ def test_solve_sensitivity_scalar_var_vector_input(self): # Solve - constant input solver = pybamm.CasadiSolver( - mode="fast", rtol=1e-10, atol=1e-10, sensitivity=True + mode="fast", rtol=1e-10, atol=1e-10, sensitivity="explicit forward" ) t_eval = np.linspace(0, 1) solution = solver.solve(model, t_eval, inputs={"param": 7 * np.ones(n)}) @@ -895,7 +899,7 @@ def test_solve_sensitivity_scalar_var_scalar_input(self): # Solve # Make sure that passing in extra options works solver = pybamm.CasadiSolver( - mode="fast", rtol=1e-10, atol=1e-10, sensitivity=True + mode="fast", rtol=1e-10, atol=1e-10, sensitivity="explicit forward" ) t_eval = np.linspace(0, 1, 80) solution = solver.solve(model, t_eval, inputs={"p": 0.1}) @@ -935,7 +939,9 @@ def test_solve_sensitivity_scalar_var_scalar_input(self): # Solve # Make sure that passing in extra options works - solver = pybamm.CasadiSolver(rtol=1e-10, atol=1e-10, sensitivity=True) + solver = pybamm.CasadiSolver( + rtol=1e-10, atol=1e-10, sensitivity="explicit forward" + ) t_eval = np.linspace(0, 1, 3) solution = solver.solve( model, t_eval, inputs={"p": 0.1, "q": 2, "r": -1, "s": 0.5} @@ -1013,7 +1019,7 @@ def test_solve_sensitivity_vector_var_scalar_input(self): n = disc.mesh["negative electrode"].npts # Solve - scalar input - solver = pybamm.CasadiSolver(sensitivity=True) + solver = pybamm.CasadiSolver(sensitivity="explicit forward") t_eval = np.linspace(0, 1) solution = solver.solve(model, t_eval, inputs={"param": 7}) np.testing.assert_array_almost_equal( From 26447b36bc02253e5ae92c634ff88c241464bf16 Mon Sep 17 00:00:00 2001 From: Valentin Sulzer Date: Wed, 22 Jul 2020 11:04:16 -0400 Subject: [PATCH 08/73] #1100 remove ProcessedSymbolicVariable --- CHANGELOG.md | 3 +- docs/source/solvers/processed_variable.rst | 3 - pybamm/__init__.py | 1 - pybamm/solvers/casadi_algebraic_solver.py | 5 +- pybamm/solvers/processed_symbolic_variable.py | 219 ------------ pybamm/solvers/processed_variable.py | 9 +- pybamm/solvers/solution.py | 19 +- .../test_processed_symbolic_variable.py | 312 ------------------ 8 files changed, 12 insertions(+), 559 deletions(-) delete mode 100644 pybamm/solvers/processed_symbolic_variable.py delete mode 100644 tests/unit/test_solvers/test_processed_symbolic_variable.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 1090184dd7..bf39789e1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,8 @@ ## Breaking changes -- Renamed `quick_plot_vars` to `output_variables` in `Simulation` to be consistent with `QuickPlot`. Passing `quick_plot_vars` to `Simulation.plot()` has been deprecated and `output_variables` should be passed instead ([#1099](https://github.com/pybamm-team/PyBaMM/pull/1099)) +- Changed sensitivity API. Removed `ProcessedSymbolicVariable`, all sensitivity now handled within the solvers and `ProcessedVariable` () +- Renamed `quick_plot_vars` to `output_variables` in `Simulation` to be consistent with `QuickPlot`. Passing `quick_plot_vars` to `Simulation.plot()` has been deprecated and `output_variables` should be passed instead ([#1099](https://github.com/pybamm-team/PyBaMM/pull/1099)) # [v0.2.3](https://github.com/pybamm-team/PyBaMM/tree/v0.2.3) - 2020-07-01 diff --git a/docs/source/solvers/processed_variable.rst b/docs/source/solvers/processed_variable.rst index 4b4296061b..9f1d62f2a7 100644 --- a/docs/source/solvers/processed_variable.rst +++ b/docs/source/solvers/processed_variable.rst @@ -3,6 +3,3 @@ Post-Process Variables .. autoclass:: pybamm.ProcessedVariable :members: - -.. autoclass:: pybamm.ProcessedSymbolicVariable - :members: diff --git a/pybamm/__init__.py b/pybamm/__init__.py index e8c1721e6e..39d2c4b9ec 100644 --- a/pybamm/__init__.py +++ b/pybamm/__init__.py @@ -206,7 +206,6 @@ def version(formatted=False): # from .solvers.solution import Solution, _BaseSolution from .solvers.processed_variable import ProcessedVariable -from .solvers.processed_symbolic_variable import ProcessedSymbolicVariable from .solvers.base_solver import BaseSolver from .solvers.dummy_solver import DummySolver from .solvers.algebraic_solver import AlgebraicSolver diff --git a/pybamm/solvers/casadi_algebraic_solver.py b/pybamm/solvers/casadi_algebraic_solver.py index d18a2849a7..367f4bf0ee 100644 --- a/pybamm/solvers/casadi_algebraic_solver.py +++ b/pybamm/solvers/casadi_algebraic_solver.py @@ -59,10 +59,7 @@ def _integrate(self, model, t_eval, inputs=None): t_eval : :class:`numpy.array`, size (k,) The times at which to compute the solution inputs : dict, optional - Any input parameters to pass to the model when solving. If any input - parameters that are present in the model are missing from "inputs", then - the solution will consist of `ProcessedSymbolicVariable` objects, which must - be provided with inputs to obtain their value. + Any input parameters to pass to the model when solving. """ # Record whether there are any symbolic inputs inputs = inputs or {} diff --git a/pybamm/solvers/processed_symbolic_variable.py b/pybamm/solvers/processed_symbolic_variable.py deleted file mode 100644 index 3ff80f0fe4..0000000000 --- a/pybamm/solvers/processed_symbolic_variable.py +++ /dev/null @@ -1,219 +0,0 @@ -# -# Processed Variable class -# -import casadi -import numbers - - -class ProcessedSymbolicVariable(object): - """ - An object that can be evaluated at arbitrary (scalars or vectors) t and x, and - returns the (interpolated) value of the base variable at that t and x. - - Parameters - ---------- - base_variable : :class:`pybamm.Symbol` - A base variable with a method `evaluate(t,y)` that returns the value of that - variable. Note that this can be any kind of node in the expression tree, not - just a :class:`pybamm.Variable`. - When evaluated, returns an array of size (m,n) - solution : :class:`pybamm.Solution` - The solution object to be used to create the processed variables - """ - - def __init__(self, base_variable, solution): - # Convert variable to casadi - t_MX = casadi.MX.sym("t") - y_MX = casadi.MX.sym("y", solution.y.shape[0]) - # Make all inputs symbolic first for converting to casadi - all_inputs_as_MX_dict = {} - symbolic_inputs_dict = {} - for key, value in solution.inputs.items(): - if not isinstance(value, casadi.MX): - all_inputs_as_MX_dict[key] = casadi.MX.sym("input") - else: - all_inputs_as_MX_dict[key] = value - # Only add symbolic inputs to the "symbolic_inputs" dict - symbolic_inputs_dict[key] = value - - all_inputs_as_MX = casadi.vertcat(*[p for p in all_inputs_as_MX_dict.values()]) - all_inputs = casadi.vertcat(*[p for p in solution.inputs.values()]) - # The symbolic_inputs dictionary will be used for sensitivity - symbolic_inputs = casadi.vertcat(*[p for p in symbolic_inputs_dict.values()]) - var = base_variable.to_casadi(t_MX, y_MX, inputs=all_inputs_as_MX_dict) - - self.base_variable = casadi.Function( - "variable", [t_MX, y_MX, all_inputs_as_MX], [var] - ) - # Store some attributes - self.t_sol = solution.t - self.u_sol = solution.y - self.mesh = base_variable.mesh - self.symbolic_inputs_dict = symbolic_inputs_dict - self.symbolic_inputs_total_shape = symbolic_inputs.shape[0] - self.inputs = all_inputs - self.domain = base_variable.domain - - self.base_eval = self.base_variable(solution.t[0], solution.y[:, 0], all_inputs) - - if ( - isinstance(self.base_eval, numbers.Number) - or len(self.base_eval.shape) == 0 - or self.base_eval.shape[0] == 1 - ): - self.initialise_0D() - else: - n = self.mesh.npts - base_shape = self.base_eval.shape[0] - # Try shape that could make the variable a 1D variable - if base_shape == n: - self.initialise_1D() - else: - # Raise error for 2D variable - raise NotImplementedError( - "Shape not recognized for {} ".format(base_variable) - + "(note processing of 2D and 3D variables is not yet " - + "implemented)" - ) - - # Make entries a function and compute jacobian - entries_MX = self.entries - self.casadi_entries_fn = casadi.Function( - "variable", [symbolic_inputs], [entries_MX] - ) - - # Don't compute jacobian if the entries are a DM (not symbolic) - if isinstance(entries_MX, casadi.DM): - self.casadi_sens_fn = None - # Do compute jacobian if the entries are symbolic (functions of input) - else: - sens_MX = casadi.jacobian(entries_MX, symbolic_inputs) - self.casadi_sens_fn = casadi.Function( - "variable", [symbolic_inputs], [sens_MX] - ) - - def initialise_0D(self): - "Create a 0D variable" - # Evaluate the base_variable index-by-index - for idx in range(len(self.t_sol)): - t = self.t_sol[idx] - u = self.u_sol[:, idx] - next_entries = self.base_variable(t, u, self.inputs) - if idx == 0: - entries = next_entries - else: - entries = casadi.horzcat(entries, next_entries) - - self.entries = entries - self.dimensions = 0 - - def initialise_1D(self): - "Create a 1D variable" - # Evaluate the base_variable index-by-index - for idx in range(len(self.t_sol)): - t = self.t_sol[idx] - u = self.u_sol[:, idx] - next_entries = self.base_variable(t, u, self.inputs) - if idx == 0: - entries = next_entries - else: - entries = casadi.vertcat(entries, next_entries) - - self.entries = entries - - def value(self, inputs=None, check_inputs=True): - """ - Returns the value of the variable at the specified input values - - Parameters - ---------- - inputs : dict - The inputs at which to evaluate the variable. - - Returns - ------- - casadi.DM - A casadi matrix of size (n_x * n_t, 1), where n_x is the number of spatial - discretisation points for the variable, and n_t is the length of the time - vector - """ - if inputs is None: - return self.casadi_entries_fn(casadi.DM()) - else: - if check_inputs: - inputs = self._check_and_transform(inputs) - return self.casadi_entries_fn(inputs) - - def sensitivity(self, inputs=None, check_inputs=True): - """ - Returns the sensitivity of the variable to the symbolic inputs at the specified - input values - - Parameters - ---------- - inputs : dict - The inputs at which to evaluate the variable. - - Returns - ------- - casadi.DM - A casadi matrix of size (n_x * n_t, n_p), where n_x is the number of spatial - discretisation points for the variable, n_t is the length of the time - vector, and n_p is the number of input parameters - """ - if self.casadi_sens_fn is None: - raise ValueError( - "Variable is not symbolic, so sensitivities are not defined" - ) - if check_inputs: - inputs = self._check_and_transform(inputs) - return self.casadi_sens_fn(inputs) - - def value_and_sensitivity(self, inputs=None): - """ - Returns the value of the variable and its sensitivity to the symbolic inputs at - the specified input values - - Parameters - ---------- - inputs : dict - The inputs at which to evaluate the variable. - """ - inputs = self._check_and_transform(inputs) - # Pass check_inputs=False to avoid re-checking inputs - return ( - self.value(inputs, check_inputs=False), - self.sensitivity(inputs, check_inputs=False), - ) - - def _check_and_transform(self, inputs_dict): - "Check dictionary has the right inputs, and convert to a vector" - # Convert dict to casadi vector - if not isinstance(inputs_dict, dict): - raise TypeError("inputs should be 'dict' but are {}".format(inputs_dict)) - # Sort input dictionary keys according to the symbolic inputs dictionary - # For practical number of input parameters this should be extremely fast and - # so is ok to do at each step - try: - inputs_dict_sorted = { - k: inputs_dict[k] for k in self.symbolic_inputs_dict.keys() - } - except KeyError as e: - raise KeyError("Inconsistent input keys. '{}' not found".format(e.args[0])) - inputs = casadi.vertcat(*[p for p in inputs_dict_sorted.values()]) - if inputs.shape[0] != self.symbolic_inputs_total_shape: - # Find the variable which caused the error, for a clearer error message - for key, inp in inputs_dict_sorted.items(): - if inp.shape[0] != self.symbolic_inputs_dict[key].shape[0]: - raise ValueError( - "Wrong shape for input '{}': expected {}, actual {}".format( - key, self.symbolic_inputs_dict[key].shape[0], inp.shape[0] - ) - ) - - return inputs - - @property - def data(self): - "Same as entries, but different name" - return self.entries diff --git a/pybamm/solvers/processed_variable.py b/pybamm/solvers/processed_variable.py index 2730557c82..b5c41615b5 100644 --- a/pybamm/solvers/processed_variable.py +++ b/pybamm/solvers/processed_variable.py @@ -80,12 +80,9 @@ def __init__(self, base_variable, solution, known_evals=None, warn=True): # The symbolic_inputs will be used for sensitivity symbolic_inputs = casadi.vertcat(*symbolic_inputs_dict.values()) - try: - var_casadi = base_variable.to_casadi( - t_MX, y_MX, inputs=symbolic_inputs_dict - ) - except: - n = 1 + var_casadi = base_variable.to_casadi( + t_MX, y_MX, inputs=symbolic_inputs_dict + ) self.base_variable_sym = casadi.Function( "variable", [t_MX, y_MX, symbolic_inputs], [var_casadi] ) diff --git a/pybamm/solvers/solution.py b/pybamm/solvers/solution.py index bc97fbaec2..58b3f90b10 100644 --- a/pybamm/solvers/solution.py +++ b/pybamm/solvers/solution.py @@ -239,19 +239,12 @@ def update(self, variables): # Process for key in variables: pybamm.logger.debug("Post-processing {}".format(key)) - # If there are symbolic inputs then we need to make a - # ProcessedSymbolicVariable - if self.has_symbolic_inputs is True: - var = pybamm.ProcessedSymbolicVariable(self.model.variables[key], self) - - # Otherwise a standard ProcessedVariable is ok - else: - var = pybamm.ProcessedVariable( - self.model.variables[key], self, self._known_evals - ) - # Update known_evals in order to process any other variables faster - for t in var.known_evals: - self._known_evals[t].update(var.known_evals[t]) + var = pybamm.ProcessedVariable( + self.model.variables[key], self, self._known_evals + ) + # Update known_evals in order to process any other variables faster + for t in var.known_evals: + self._known_evals[t].update(var.known_evals[t]) # Save variable and data self._variables[key] = var diff --git a/tests/unit/test_solvers/test_processed_symbolic_variable.py b/tests/unit/test_solvers/test_processed_symbolic_variable.py deleted file mode 100644 index c179562d3b..0000000000 --- a/tests/unit/test_solvers/test_processed_symbolic_variable.py +++ /dev/null @@ -1,312 +0,0 @@ -# -# Tests for the Processed Variable class -# -import pybamm -import casadi - -import numpy as np -import unittest -import tests - - -class TestProcessedSymbolicVariable(unittest.TestCase): - def test_processed_variable_0D(self): - # without inputs - y = pybamm.StateVector(slice(0, 1)) - var = 2 * y - var.mesh = None - - t_sol = np.linspace(0, 1) - y_sol = np.array([np.linspace(0, 5)]) - solution = pybamm.Solution(t_sol, y_sol) - processed_var = pybamm.ProcessedSymbolicVariable(var, solution) - np.testing.assert_array_equal(processed_var.value(), 2 * y_sol) - - # No sensitivity as variable is not symbolic - with self.assertRaisesRegex(ValueError, "Variable is not symbolic"): - processed_var.sensitivity() - - def test_processed_variable_0D_with_inputs(self): - # with symbolic inputs - y = pybamm.StateVector(slice(0, 1)) - p = pybamm.InputParameter("p") - q = pybamm.InputParameter("q") - var = p * y + q - var.mesh = None - - t_sol = np.linspace(0, 1) - y_sol = np.array([np.linspace(0, 5)]) - solution = pybamm.Solution(t_sol, y_sol) - solution.inputs = {"p": casadi.MX.sym("p"), "q": casadi.MX.sym("q")} - processed_var = pybamm.ProcessedSymbolicVariable(var, solution) - np.testing.assert_array_equal( - processed_var.value({"p": 3, "q": 4}).full(), 3 * y_sol + 4 - ) - np.testing.assert_array_equal( - processed_var.sensitivity({"p": 3, "q": 4}).full(), - np.c_[y_sol.T, np.ones_like(y_sol).T], - ) - - # via value_and_sensitivity - val, sens = processed_var.value_and_sensitivity({"p": 3, "q": 4}) - np.testing.assert_array_equal(val.full(), 3 * y_sol + 4) - np.testing.assert_array_equal( - sens.full(), np.c_[y_sol.T, np.ones_like(y_sol).T] - ) - - # Test bad inputs - with self.assertRaisesRegex(TypeError, "inputs should be 'dict'"): - processed_var.value(1) - with self.assertRaisesRegex(KeyError, "Inconsistent input keys"): - processed_var.value({"not p": 3}) - - def test_processed_variable_0D_some_inputs(self): - # with some symbolic inputs and some non-symbolic inputs - y = pybamm.StateVector(slice(0, 1)) - p = pybamm.InputParameter("p") - q = pybamm.InputParameter("q") - var = p * y - q - var.mesh = None - - t_sol = np.linspace(0, 1) - y_sol = np.array([np.linspace(0, 5)]) - solution = pybamm.Solution(t_sol, y_sol) - solution.inputs = {"p": casadi.MX.sym("p"), "q": 2} - processed_var = pybamm.ProcessedSymbolicVariable(var, solution) - np.testing.assert_array_equal( - processed_var.value({"p": 3}).full(), 3 * y_sol - 2 - ) - np.testing.assert_array_equal( - processed_var.sensitivity({"p": 3}).full(), y_sol.T - ) - - def test_processed_variable_1D(self): - var = pybamm.Variable("var", domain=["negative electrode", "separator"]) - x = pybamm.SpatialVariable("x", domain=["negative electrode", "separator"]) - eqn = var + x - - # On nodes - disc = tests.get_discretisation_for_testing() - disc.set_variable_slices([var]) - x_sol = disc.process_symbol(x).entries[:, 0] - eqn_sol = disc.process_symbol(eqn) - - # With scalar t_sol - t_sol = [0] - y_sol = np.ones_like(x_sol)[:, np.newaxis] * 5 - sol = pybamm.Solution(t_sol, y_sol) - processed_eqn = pybamm.ProcessedSymbolicVariable(eqn_sol, sol) - np.testing.assert_array_equal( - processed_eqn.value(), y_sol + x_sol[:, np.newaxis] - ) - - # With vector t_sol - t_sol = np.linspace(0, 1) - y_sol = np.ones_like(x_sol)[:, np.newaxis] * np.linspace(0, 5) - sol = pybamm.Solution(t_sol, y_sol) - processed_eqn = pybamm.ProcessedSymbolicVariable(eqn_sol, sol) - np.testing.assert_array_equal( - processed_eqn.value(), (y_sol + x_sol[:, np.newaxis]).T.reshape(-1, 1) - ) - - def test_processed_variable_1D_with_scalar_inputs(self): - var = pybamm.Variable("var", domain=["negative electrode", "separator"]) - x = pybamm.SpatialVariable("x", domain=["negative electrode", "separator"]) - p = pybamm.InputParameter("p") - q = pybamm.InputParameter("q") - eqn = var * p + 2 * q - - # On nodes - disc = tests.get_discretisation_for_testing() - disc.set_variable_slices([var]) - x_sol = disc.process_symbol(x).entries[:, 0] - eqn_sol = disc.process_symbol(eqn) - - # Scalar t - t_sol = [0] - y_sol = np.ones_like(x_sol)[:, np.newaxis] * 5 - - sol = pybamm.Solution(t_sol, y_sol) - sol.inputs = {"p": casadi.MX.sym("p"), "q": casadi.MX.sym("q")} - processed_eqn = pybamm.ProcessedSymbolicVariable(eqn_sol, sol) - - # Test values - np.testing.assert_array_equal( - processed_eqn.value({"p": 27, "q": -42}), 27 * y_sol - 84, - ) - - # Test sensitivities - np.testing.assert_array_equal( - processed_eqn.sensitivity({"p": 27, "q": -84}), - np.c_[y_sol, 2 * np.ones_like(y_sol)], - ) - - ################################################################################ - # Vector t - t_sol = np.linspace(0, 1) - y_sol = np.ones_like(x_sol)[:, np.newaxis] * np.linspace(0, 5) - - sol = pybamm.Solution(t_sol, y_sol) - sol.inputs = {"p": casadi.MX.sym("p"), "q": casadi.MX.sym("q")} - processed_eqn = pybamm.ProcessedSymbolicVariable(eqn_sol, sol) - - # Test values - np.testing.assert_array_equal( - processed_eqn.value({"p": 27, "q": -42}), - (27 * y_sol - 84).T.reshape(-1, 1), - ) - - # Test sensitivities - np.testing.assert_array_equal( - processed_eqn.sensitivity({"p": 27, "q": -42}), - np.c_[y_sol.T.flatten(), 2 * np.ones_like(y_sol.T.flatten())], - ) - - def test_processed_variable_1D_with_vector_inputs(self): - var = pybamm.Variable("var", domain=["negative electrode", "separator"]) - x = pybamm.SpatialVariable("x", domain=["negative electrode", "separator"]) - p = pybamm.InputParameter("p", domain=["negative electrode", "separator"]) - p.set_expected_size(65) - q = pybamm.InputParameter("q") - eqn = (var * p) ** 2 + 2 * q - - # On nodes - disc = tests.get_discretisation_for_testing() - disc.set_variable_slices([var]) - x_sol = disc.process_symbol(x).entries[:, 0] - n = x_sol.size - eqn_sol = disc.process_symbol(eqn) - - # Scalar t - t_sol = [0] - y_sol = np.ones_like(x_sol)[:, np.newaxis] * 5 - sol = pybamm.Solution(t_sol, y_sol) - sol.inputs = {"p": casadi.MX.sym("p", n), "q": casadi.MX.sym("q")} - processed_eqn = pybamm.ProcessedSymbolicVariable(eqn_sol, sol) - - # Test values - constant p - np.testing.assert_array_equal( - processed_eqn.value({"p": 27 * np.ones(n), "q": -42}), - (27 * y_sol) ** 2 - 84, - ) - # Test values - varying p - p = np.linspace(0, 1, n) - np.testing.assert_array_equal( - processed_eqn.value({"p": p, "q": 3}), (p[:, np.newaxis] * y_sol) ** 2 + 6, - ) - - # Test sensitivities - constant p - np.testing.assert_array_equal( - processed_eqn.sensitivity({"p": 2 * np.ones(n), "q": -84}), - np.c_[100 * np.eye(y_sol.size), 2 * np.ones(n)], - ) - # Test sensitivities - varying p - # d/dy((py)**2) = (2*p*y) * y - np.testing.assert_array_equal( - processed_eqn.sensitivity({"p": p, "q": -84}), - np.c_[ - np.diag((2 * p[:, np.newaxis] * y_sol ** 2).flatten()), 2 * np.ones(n) - ], - ) - - # Bad shape - with self.assertRaisesRegex( - ValueError, "Wrong shape for input 'p': expected 65, actual 5" - ): - processed_eqn.value({"p": casadi.MX.sym("p", 5), "q": 1}) - - def test_1D_different_domains(self): - # Negative electrode domain - var = pybamm.Variable("var", domain=["negative electrode"]) - x = pybamm.SpatialVariable("x", domain=["negative electrode"]) - - disc = tests.get_discretisation_for_testing() - disc.set_variable_slices([var]) - x_sol = disc.process_symbol(x).entries[:, 0] - var_sol = disc.process_symbol(var) - - t_sol = [0] - y_sol = np.ones_like(x_sol)[:, np.newaxis] * 5 - sol = pybamm.Solution(t_sol, y_sol) - pybamm.ProcessedSymbolicVariable(var_sol, sol) - - # Particle domain - var = pybamm.Variable("var", domain=["negative particle"]) - r = pybamm.SpatialVariable("r", domain=["negative particle"]) - - disc = tests.get_discretisation_for_testing() - disc.set_variable_slices([var]) - r_sol = disc.process_symbol(r).entries[:, 0] - var_sol = disc.process_symbol(var) - - t_sol = [0] - y_sol = np.ones_like(r_sol)[:, np.newaxis] * 5 - sol = pybamm.Solution(t_sol, y_sol) - pybamm.ProcessedSymbolicVariable(var_sol, sol) - - # Current collector domain - var = pybamm.Variable("var", domain=["current collector"]) - z = pybamm.SpatialVariable("z", domain=["current collector"]) - - disc = tests.get_1p1d_discretisation_for_testing() - disc.set_variable_slices([var]) - z_sol = disc.process_symbol(z).entries[:, 0] - var_sol = disc.process_symbol(var) - - t_sol = [0] - y_sol = np.ones_like(z_sol)[:, np.newaxis] * 5 - sol = pybamm.Solution(t_sol, y_sol) - pybamm.ProcessedSymbolicVariable(var_sol, sol) - - # Other domain - var = pybamm.Variable("var", domain=["line"]) - x = pybamm.SpatialVariable("x", domain=["line"]) - - geometry = pybamm.Geometry( - {"line": {x: {"min": pybamm.Scalar(0), "max": pybamm.Scalar(1)}}} - ) - submesh_types = {"line": pybamm.MeshGenerator(pybamm.Uniform1DSubMesh)} - var_pts = {x: 10} - mesh = pybamm.Mesh(geometry, submesh_types, var_pts) - disc = pybamm.Discretisation(mesh, {"line": pybamm.FiniteVolume()}) - disc.set_variable_slices([var]) - x_sol = disc.process_symbol(x).entries[:, 0] - var_sol = disc.process_symbol(var) - - t_sol = [0] - y_sol = np.ones_like(x_sol)[:, np.newaxis] * 5 - sol = pybamm.Solution(t_sol, y_sol) - pybamm.ProcessedSymbolicVariable(var_sol, sol) - - # 2D fails - var = pybamm.Variable( - "var", - domain=["negative particle"], - auxiliary_domains={"secondary": "negative electrode"}, - ) - r = pybamm.SpatialVariable( - "r", - domain=["negative particle"], - auxiliary_domains={"secondary": "negative electrode"}, - ) - - disc = tests.get_p2d_discretisation_for_testing() - disc.set_variable_slices([var]) - r_sol = disc.process_symbol(r).entries[:, 0] - var_sol = disc.process_symbol(var) - - t_sol = [0] - y_sol = np.ones_like(r_sol)[:, np.newaxis] * 5 - sol = pybamm.Solution(t_sol, y_sol) - with self.assertRaisesRegex(NotImplementedError, "Shape not recognized"): - pybamm.ProcessedSymbolicVariable(var_sol, sol) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() From b8875155570f17088dfa2caa8758098f3df63da1 Mon Sep 17 00:00:00 2001 From: Valentin Sulzer Date: Wed, 22 Jul 2020 14:02:49 -0400 Subject: [PATCH 09/73] #1100 working on casadi solver --- pybamm/solvers/casadi_algebraic_solver.py | 104 +- pybamm/solvers/casadi_solver.py | 61 +- pybamm/solvers/processed_variable.py | 6 +- tests/unit/test_solvers/test_casadi_solver.py | 1704 ++++++++--------- 4 files changed, 876 insertions(+), 999 deletions(-) diff --git a/pybamm/solvers/casadi_algebraic_solver.py b/pybamm/solvers/casadi_algebraic_solver.py index 367f4bf0ee..5e48946211 100644 --- a/pybamm/solvers/casadi_algebraic_solver.py +++ b/pybamm/solvers/casadi_algebraic_solver.py @@ -40,6 +40,9 @@ def __init__(self, tol=1e-6, extra_options=None, sensitivity=None): self.extra_options = extra_options or {} pybamm.citations.register("Andersson2019") + self.rootfinders = {} + self.y_sols = {} + @property def tol(self): return self._tol @@ -62,24 +65,12 @@ def _integrate(self, model, t_eval, inputs=None): Any input parameters to pass to the model when solving. """ # Record whether there are any symbolic inputs - inputs = inputs or {} + inputs_dict = inputs or {} # Create casadi objects for the root-finder - inputs_dict = inputs inputs = casadi.vertcat(*[v for v in inputs.values()]) - if self.sensitivity == "casadi" and inputs_dict != {}: - # Create symbolic inputs for sensitivity analysis - symbolic_inputs_list = [] - for name, value in inputs_dict.items(): - if isinstance(value, numbers.Number): - symbolic_inputs_list.append(casadi.MX.sym(name)) - else: - symbolic_inputs_list.append(casadi.MX.sym(name, value.shape[0])) - symbolic_inputs = casadi.vertcat(*[p for p in symbolic_inputs_list]) - inputs_for_alg = symbolic_inputs - else: - symbolic_inputs = casadi.DM() - inputs_for_alg = inputs + # Create symbolic inputs + symbolic_inputs = casadi.MX.sym("inputs", inputs.shape[0]) y0 = model.y0 # The casadi algebraic solver can read rhs equations, but leaves them unchanged @@ -101,34 +92,50 @@ def _integrate(self, model, t_eval, inputs=None): y_alg = None - # Set up - t_sym = casadi.MX.sym("t") - y_alg_sym = casadi.MX.sym("y_alg", y0_alg.shape[0]) - y_sym = casadi.vertcat(y0_diff, y_alg_sym) - - t_and_inputs_sym = casadi.vertcat(t_sym, symbolic_inputs) - alg = model.casadi_algebraic(t_sym, y_sym, inputs_for_alg) - - # Set constraints vector in the casadi format - # Constrain the unknowns. 0 (default): no constraint on ui, 1: ui >= 0.0, - # -1: ui <= 0.0, 2: ui > 0.0, -2: ui < 0.0. - constraints = np.zeros_like(model.bounds[0], dtype=int) - # If the lower bound is positive then the variable must always be positive - constraints[model.bounds[0] >= 0] = 1 - # If the upper bound is negative then the variable must always be negative - constraints[model.bounds[1] <= 0] = -1 - - # Set up rootfinder - roots = casadi.rootfinder( - "roots", - "newton", - dict(x=y_alg_sym, p=t_and_inputs_sym, g=alg), - { - **self.extra_options, - "abstol": self.tol, - "constraints": list(constraints[len_rhs:]), - }, - ) + if model in self.rootfinders: + if self.sensitivity == "casadi": + # Reuse (symbolic) solution with new inputs + y_sol = self.y_sols[model] + return pybamm.Solution( + t_eval, + y_sol, + termination="success", + model=model, + inputs=inputs_dict, + ) + roots = self.rootfinders[model] + else: + # Set up + t_sym = casadi.MX.sym("t") + y_alg_sym = casadi.MX.sym("y_alg", y0_alg.shape[0]) + y_sym = casadi.vertcat(y0_diff, y_alg_sym) + + t_and_inputs_sym = casadi.vertcat(t_sym, symbolic_inputs) + alg = model.casadi_algebraic(t_sym, y_sym, symbolic_inputs) + + # Set constraints vector in the casadi format + # Constrain the unknowns. 0 (default): no constraint on ui, 1: ui >= 0.0, + # -1: ui <= 0.0, 2: ui > 0.0, -2: ui < 0.0. + constraints = np.zeros_like(model.bounds[0], dtype=int) + # If the lower bound is positive then the variable must always be positive + constraints[model.bounds[0] >= 0] = 1 + # If the upper bound is negative then the variable must always be negative + constraints[model.bounds[1] <= 0] = -1 + + # Set up rootfinder + roots = casadi.rootfinder( + "roots", + "newton", + dict(x=y_alg_sym, p=t_and_inputs_sym, g=alg), + { + **self.extra_options, + "abstol": self.tol, + "constraints": list(constraints[len_rhs:]), + }, + ) + + self.rootfinders[model] = roots + for idx, t in enumerate(t_eval): # Evaluate algebraic with new t and previous y0, if it's already close # enough then keep it @@ -145,10 +152,15 @@ def _integrate(self, model, t_eval, inputs=None): y_alg = casadi.horzcat(y_alg, y0_alg) # Otherwise calculate new y_sol else: - t_eval_inputs_sym = casadi.vertcat(t, symbolic_inputs) + # If doing sensitivity with casadi, evaluate with symbolic inputs + # Otherwise, evaluate with actual inputs + if self.sensitivity == "casadi": + t_eval_and_inputs = casadi.vertcat(t, symbolic_inputs) + else: + t_eval_and_inputs = casadi.vertcat(t, inputs) # Solve try: - y_alg_sol = roots(y0_alg, t_eval_inputs_sym) + y_alg_sol = roots(y0_alg, t_eval_and_inputs) success = True message = None # Check final output @@ -193,6 +205,8 @@ def _integrate(self, model, t_eval, inputs=None): # If doing sensitivity, return the solution as a function of the inputs if self.sensitivity == "casadi": y_sol = casadi.Function("y_sol", [symbolic_inputs], [y_sol]) + # Save the solution, can just reuse and change the inputs + self.y_sols[model] = y_sol # Return solution object (no events, so pass None to t_event, y_event) return pybamm.Solution( t_eval, y_sol, termination="success", model=model, inputs=inputs_dict diff --git a/pybamm/solvers/casadi_solver.py b/pybamm/solvers/casadi_solver.py index ad321d8ce9..70b7c0e7b2 100644 --- a/pybamm/solvers/casadi_solver.py +++ b/pybamm/solvers/casadi_solver.py @@ -59,9 +59,12 @@ class CasadiSolver(pybamm.BaseSolver): Please consult `CasADi documentation `_ for details. sensitivity : bool, optional - Whether to explicitly formulate and solve the forward sensitivity equations. - See :class:`pybamm.BaseSolver` + Whether (and how) to calculate sensitivities when solving. Options are: + - None: no sensitivities + - "explicit forward": explicitly formulate the sensitivity equations. + See :class:`pybamm.BaseSolver` + - "casadi": use casadi to differentiate through the integrator """ def __init__( @@ -104,6 +107,7 @@ def __init__( # Initialize self.integrators = {} self.integrator_specs = {} + self.y_sols = {} pybamm.citations.register("Andersson2019") @@ -122,24 +126,29 @@ def _integrate(self, model, t_eval, inputs=None): """ # Record whether there are any symbolic inputs inputs_dict = inputs or {} - has_symbolic_inputs = any( - isinstance(v, casadi.MX) for v in inputs_dict.values() - ) # convert inputs to casadi format inputs = casadi.vertcat(*[x for x in inputs_dict.values()]) - if has_symbolic_inputs: - # Create integrator without grid to avoid having to create several times - self.create_integrator(model, inputs) - solution = self._run_integrator(model, model.y0, inputs_dict, t_eval) + if self.sensitivity == "casadi" and inputs_dict != {}: + # If the solution has already been created, we can reuse it + if model in self.y_sols: + y_sol = self.y_sols[model] + solution = pybamm.Solution( + t_eval, y_sol, model=model, inputs=inputs_dict + ) + else: + # Create integrator without grid, which will be called repeatedly + # This is necessary for casadi to compute sensitivities + self.create_integrator(model, inputs_dict) + solution = self._run_integrator(model, model.y0, inputs_dict, t_eval) solution.termination = "final time" return solution elif self.mode == "fast" or not model.events: if not model.events: pybamm.logger.info("No events found, running fast mode") # Create an integrator with the grid (we just need to do this once) - self.create_integrator(model, inputs, t_eval) + self.create_integrator(model, inputs_dict, t_eval) solution = self._run_integrator(model, model.y0, inputs_dict, t_eval) solution.termination = "final time" return solution @@ -161,7 +170,7 @@ def _integrate(self, model, t_eval, inputs=None): # in "safe without grid" mode, # create integrator once, without grid, # to avoid having to create several times - self.create_integrator(model, inputs) + self.create_integrator(model, inputs_dict) # Initialize solution solution = pybamm.Solution( np.array([t]), y0[:, np.newaxis], model=model, inputs=inputs_dict @@ -314,12 +323,15 @@ def event_fun(t): y0 = solution.y[:, -1] return solution - def create_integrator(self, model, inputs, t_eval=None): + def create_integrator(self, model, inputs_dict, t_eval=None): """ Method to create a casadi integrator object. If t_eval is provided, the integrator uses t_eval to make the grid. Otherwise, the integrator has grid [0,1]. """ + # convert inputs to casadi format + inputs = casadi.vertcat(*[x for x in inputs_dict.values()]) + # Use grid if t_eval is given use_grid = not (t_eval is None) # Only set up problem once @@ -400,6 +412,13 @@ def create_integrator(self, model, inputs, t_eval=None): def _run_integrator(self, model, y0, inputs_dict, t_eval): inputs = casadi.vertcat(*[x for x in inputs_dict.values()]) + symbolic_inputs = casadi.MX.sym("inputs", inputs.shape[0]) + # If doing sensitivity with casadi, evaluate with symbolic inputs + # Otherwise, evaluate with actual inputs + if self.sensitivity == "casadi": + inputs_eval = symbolic_inputs + else: + inputs_eval = inputs integrator, use_grid = self.integrators[model] # Split up initial conditions into differential and algebraic # Check y0 to see if it includes sensitivities @@ -415,10 +434,9 @@ def _run_integrator(self, model, y0, inputs_dict, t_eval): if use_grid is True: # Call the integrator once, with the grid sol = integrator( - x0=y0_diff, z0=y0_alg, p=inputs, **self.extra_options_call + x0=y0_diff, z0=y0_alg, p=inputs_eval, **self.extra_options_call ) y_sol = np.concatenate([sol["xf"].full(), sol["zf"].full()]) - return pybamm.Solution(t_eval, y_sol, model=model, inputs=inputs_dict) else: # Repeated calls to the integrator x = y0_diff @@ -428,7 +446,7 @@ def _run_integrator(self, model, y0, inputs_dict, t_eval): for i in range(len(t_eval) - 1): t_min = t_eval[i] t_max = t_eval[i + 1] - inputs_with_tlims = casadi.vertcat(inputs, t_min, t_max) + inputs_with_tlims = casadi.vertcat(inputs_eval, t_min, t_max) sol = integrator( x0=x, z0=z, p=inputs_with_tlims, **self.extra_options_call ) @@ -438,14 +456,15 @@ def _run_integrator(self, model, y0, inputs_dict, t_eval): if not z.is_empty(): y_alg = casadi.horzcat(y_alg, z) if z.is_empty(): - return pybamm.Solution( - t_eval, y_diff, model=model, inputs=inputs_dict - ) + y_sol = y_diff else: y_sol = casadi.vertcat(y_diff, y_alg) - return pybamm.Solution( - t_eval, y_sol, model=model, inputs=inputs_dict - ) + # If doing sensitivity, return the solution as a function of the inputs + if self.sensitivity == "casadi": + y_sol = casadi.Function("y_sol", [symbolic_inputs], [y_sol]) + # Save the solution, can just reuse and change the inputs + self.y_sols[model] = y_sol + return pybamm.Solution(t_eval, y_sol, model=model, inputs=inputs_dict) except RuntimeError as e: # If it doesn't work raise error raise pybamm.SolverError(e.args[0]) diff --git a/pybamm/solvers/processed_variable.py b/pybamm/solvers/processed_variable.py index b5c41615b5..0c373c0b8d 100644 --- a/pybamm/solvers/processed_variable.py +++ b/pybamm/solvers/processed_variable.py @@ -722,11 +722,7 @@ def initialise_1D_symbolic(): + "implemented)" ) - # Make entries a function and compute jacobian - casadi_entries_fn = casadi.Function( - "variable", [self.symbolic_inputs], [entries_MX] - ) - + # Compute jacobian sens_MX = casadi.jacobian(entries_MX, self.symbolic_inputs) casadi_sens_fn = casadi.Function("variable", [self.symbolic_inputs], [sens_MX]) diff --git a/tests/unit/test_solvers/test_casadi_solver.py b/tests/unit/test_solvers/test_casadi_solver.py index 35bd2dfeb3..aab9a346e9 100644 --- a/tests/unit/test_solvers/test_casadi_solver.py +++ b/tests/unit/test_solvers/test_casadi_solver.py @@ -9,408 +9,408 @@ from scipy.optimize import least_squares -class TestCasadiSolver(unittest.TestCase): - def test_bad_mode(self): - with self.assertRaisesRegex(ValueError, "invalid mode"): - pybamm.CasadiSolver(mode="bad mode") - - def test_model_solver(self): - # Create model - model = pybamm.BaseModel() - var = pybamm.Variable("var") - model.rhs = {var: 0.1 * var} - model.initial_conditions = {var: 1} - # No need to set parameters; can use base discretisation (no spatial operators) - - # create discretisation - disc = pybamm.Discretisation() - model_disc = disc.process_model(model, inplace=False) - # Solve - solver = pybamm.CasadiSolver(mode="fast", rtol=1e-8, atol=1e-8) - t_eval = np.linspace(0, 1, 100) - solution = solver.solve(model_disc, t_eval) - np.testing.assert_array_equal(solution.t, t_eval) - np.testing.assert_array_almost_equal( - solution.y[0], np.exp(0.1 * solution.t), decimal=5 - ) - - # Safe mode (enforce events that won't be triggered) - model.events = [pybamm.Event("an event", var + 1)] - disc.process_model(model) - solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) - solution = solver.solve(model, t_eval) - np.testing.assert_array_equal(solution.t, t_eval) - np.testing.assert_array_almost_equal( - solution.y[0], np.exp(0.1 * solution.t), decimal=5 - ) - - # Safe mode, without grid (enforce events that won't be triggered) - solver = pybamm.CasadiSolver(mode="safe without grid", rtol=1e-8, atol=1e-8) - solution = solver.solve(model, t_eval) - np.testing.assert_array_equal(solution.t, t_eval) - np.testing.assert_array_almost_equal( - solution.y[0], np.exp(0.1 * solution.t), decimal=5 - ) - - def test_model_solver_python(self): - # Create model - pybamm.set_logging_level("ERROR") - model = pybamm.BaseModel() - model.convert_to_format = "python" - var = pybamm.Variable("var") - model.rhs = {var: 0.1 * var} - model.initial_conditions = {var: 1} - # No need to set parameters; can use base discretisation (no spatial operators) - - # create discretisation - disc = pybamm.Discretisation() - disc.process_model(model) - # Solve - solver = pybamm.CasadiSolver(mode="fast", rtol=1e-8, atol=1e-8) - t_eval = np.linspace(0, 1, 100) - solution = solver.solve(model, t_eval) - np.testing.assert_array_equal(solution.t, t_eval) - np.testing.assert_array_almost_equal( - solution.y[0], np.exp(0.1 * solution.t), decimal=5 - ) - pybamm.set_logging_level("WARNING") - - def test_model_solver_failure(self): - # Create model - model = pybamm.BaseModel() - var = pybamm.Variable("var") - model.rhs = {var: -pybamm.sqrt(var)} - model.initial_conditions = {var: 1} - # add events so that safe mode is used (won't be triggered) - model.events = [pybamm.Event("10", var - 10)] - # No need to set parameters; can use base discretisation (no spatial operators) - - # create discretisation - disc = pybamm.Discretisation() - model_disc = disc.process_model(model, inplace=False) - - solver = pybamm.CasadiSolver(extra_options_call={"regularity_check": False}) - # Solve with failure at t=2 - t_eval = np.linspace(0, 20, 100) - with self.assertRaises(pybamm.SolverError): - solver.solve(model_disc, t_eval) - # Solve with failure at t=0 - model.initial_conditions = {var: 0} - model_disc = disc.process_model(model, inplace=False) - t_eval = np.linspace(0, 20, 100) - with self.assertRaises(pybamm.SolverError): - solver.solve(model_disc, t_eval) - - def test_model_solver_events(self): - # Create model - model = pybamm.BaseModel() - whole_cell = ["negative electrode", "separator", "positive electrode"] - var1 = pybamm.Variable("var1", domain=whole_cell) - var2 = pybamm.Variable("var2", domain=whole_cell) - model.rhs = {var1: 0.1 * var1} - model.algebraic = {var2: 2 * var1 - var2} - model.initial_conditions = {var1: 1, var2: 2} - model.events = [ - pybamm.Event("var1 = 1.5", pybamm.min(var1 - 1.5)), - pybamm.Event("var2 = 2.5", pybamm.min(var2 - 2.5)), - ] - disc = get_discretisation_for_testing() - disc.process_model(model) - - # Solve using "safe" mode - solver = pybamm.CasadiSolver(mode="safe", rtol=1e-8, atol=1e-8) - t_eval = np.linspace(0, 5, 100) - solution = solver.solve(model, t_eval) - np.testing.assert_array_less(solution.y[0], 1.5) - np.testing.assert_array_less(solution.y[-1], 2.5 + 1e-10) - np.testing.assert_array_almost_equal( - solution.y[0], np.exp(0.1 * solution.t), decimal=5 - ) - np.testing.assert_array_almost_equal( - solution.y[-1], 2 * np.exp(0.1 * solution.t), decimal=5 - ) - - # Solve using "safe" mode with debug off - pybamm.settings.debug_mode = False - solver = pybamm.CasadiSolver(mode="safe", rtol=1e-8, atol=1e-8, dt_max=1) - t_eval = np.linspace(0, 5, 100) - solution = solver.solve(model, t_eval) - np.testing.assert_array_less(solution.y[0], 1.5) - np.testing.assert_array_less(solution.y[-1], 2.5 + 1e-10) - # test the last entry is exactly 2.5 - np.testing.assert_array_almost_equal(solution.y[-1, -1], 2.5, decimal=2) - np.testing.assert_array_almost_equal( - solution.y[0], np.exp(0.1 * solution.t), decimal=5 - ) - np.testing.assert_array_almost_equal( - solution.y[-1], 2 * np.exp(0.1 * solution.t), decimal=5 - ) - pybamm.settings.debug_mode = True - - # Try dt_max=0 to enforce using all timesteps - solver = pybamm.CasadiSolver(dt_max=0, rtol=1e-8, atol=1e-8) - t_eval = np.linspace(0, 5, 100) - solution = solver.solve(model, t_eval) - np.testing.assert_array_less(solution.y[0], 1.5) - np.testing.assert_array_less(solution.y[-1], 2.5) - np.testing.assert_array_almost_equal( - solution.y[0], np.exp(0.1 * solution.t), decimal=5 - ) - np.testing.assert_array_almost_equal( - solution.y[-1], 2 * np.exp(0.1 * solution.t), decimal=5 - ) - - # Test when an event returns nan - model = pybamm.BaseModel() - var = pybamm.Variable("var") - model.rhs = {var: 0.1 * var} - model.initial_conditions = {var: 1} - model.events = [ - pybamm.Event("event", var - 1.02), - pybamm.Event("sqrt event", pybamm.sqrt(1.0199 - var)), - ] - disc = pybamm.Discretisation() - disc.process_model(model) - solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) - solution = solver.solve(model, t_eval) - np.testing.assert_array_less(solution.y[0], 1.02 + 1e-10) - np.testing.assert_array_almost_equal(solution.y[0, -1], 1.02, decimal=2) - - def test_model_step(self): - # Create model - model = pybamm.BaseModel() - domain = ["negative electrode", "separator", "positive electrode"] - var = pybamm.Variable("var", domain=domain) - model.rhs = {var: 0.1 * var} - model.initial_conditions = {var: 1} - # No need to set parameters; can use base discretisation (no spatial operators) - - # create discretisation - mesh = get_mesh_for_testing() - spatial_methods = {"macroscale": pybamm.FiniteVolume()} - disc = pybamm.Discretisation(mesh, spatial_methods) - disc.process_model(model) - - solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) - - # Step once - dt = 1 - step_sol = solver.step(None, model, dt) - np.testing.assert_array_equal(step_sol.t, [0, dt]) - np.testing.assert_array_almost_equal(step_sol.y[0], np.exp(0.1 * step_sol.t)) - - # Step again (return 5 points) - step_sol_2 = solver.step(step_sol, model, dt, npts=5) - np.testing.assert_array_equal( - step_sol_2.t, np.concatenate([np.array([0]), np.linspace(dt, 2 * dt, 5)]) - ) - np.testing.assert_array_almost_equal( - step_sol_2.y[0], np.exp(0.1 * step_sol_2.t) - ) - - # Check steps give same solution as solve - t_eval = step_sol.t - solution = solver.solve(model, t_eval) - np.testing.assert_array_almost_equal(solution.y[0], step_sol.y[0]) - - def test_model_step_with_input(self): - # Create model - model = pybamm.BaseModel() - var = pybamm.Variable("var") - a = pybamm.InputParameter("a") - model.rhs = {var: a * var} - model.initial_conditions = {var: 1} - model.variables = {"a": a} - # No need to set parameters; can use base discretisation (no spatial operators) - - # create discretisation - disc = pybamm.Discretisation() - disc.process_model(model) - - solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) - - # Step with an input - dt = 0.1 - step_sol = solver.step(None, model, dt, npts=5, inputs={"a": 0.1}) - np.testing.assert_array_equal(step_sol.t, np.linspace(0, dt, 5)) - np.testing.assert_allclose(step_sol.y[0], np.exp(0.1 * step_sol.t)) - - # Step again with different inputs - step_sol_2 = solver.step(step_sol, model, dt, npts=5, inputs={"a": -1}) - np.testing.assert_array_equal(step_sol_2.t, np.linspace(0, 2 * dt, 9)) - np.testing.assert_array_equal( - step_sol_2["a"].entries, np.array([0.1, 0.1, 0.1, 0.1, 0.1, -1, -1, -1, -1]) - ) - np.testing.assert_allclose( - step_sol_2.y[0], - np.concatenate( - [ - np.exp(0.1 * step_sol.t[:5]), - np.exp(0.1 * step_sol.t[4]) * np.exp(-(step_sol.t[5:] - dt)), - ] - ), - ) - - def test_model_step_events(self): - # Create model - model = pybamm.BaseModel() - var1 = pybamm.Variable("var1") - var2 = pybamm.Variable("var2") - model.rhs = {var1: 0.1 * var1} - model.algebraic = {var2: 2 * var1 - var2} - model.initial_conditions = {var1: 1, var2: 2} - model.events = [ - pybamm.Event("var1 = 1.5", pybamm.min(var1 - 1.5)), - pybamm.Event("var2 = 2.5", pybamm.min(var2 - 2.5)), - ] - disc = pybamm.Discretisation() - disc.process_model(model) - - # Solve - step_solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) - dt = 0.05 - time = 0 - end_time = 5 - step_solution = None - while time < end_time: - step_solution = step_solver.step(step_solution, model, dt=dt, npts=10) - time += dt - np.testing.assert_array_less(step_solution.y[0], 1.5) - np.testing.assert_array_less(step_solution.y[-1], 2.5001) - np.testing.assert_array_almost_equal( - step_solution.y[0], np.exp(0.1 * step_solution.t), decimal=5 - ) - np.testing.assert_array_almost_equal( - step_solution.y[-1], 2 * np.exp(0.1 * step_solution.t), decimal=4 - ) - - def test_model_solver_with_inputs(self): - # Create model - model = pybamm.BaseModel() - domain = ["negative electrode", "separator", "positive electrode"] - var = pybamm.Variable("var", domain=domain) - model.rhs = {var: -pybamm.InputParameter("rate") * var} - model.initial_conditions = {var: 1} - model.events = [pybamm.Event("var=0.5", pybamm.min(var - 0.5))] - # No need to set parameters; can use base discretisation (no spatial - # operators) - - # create discretisation - mesh = get_mesh_for_testing() - spatial_methods = {"macroscale": pybamm.FiniteVolume()} - disc = pybamm.Discretisation(mesh, spatial_methods) - disc.process_model(model) - # Solve - solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) - t_eval = np.linspace(0, 10, 100) - solution = solver.solve(model, t_eval, inputs={"rate": 0.1}) - self.assertLess(len(solution.t), len(t_eval)) - np.testing.assert_allclose(solution.y[0], np.exp(-0.1 * solution.t), rtol=1e-04) - - def test_model_solver_dae_inputs_in_initial_conditions(self): - # Create model - model = pybamm.BaseModel() - var1 = pybamm.Variable("var1") - var2 = pybamm.Variable("var2") - model.rhs = {var1: pybamm.InputParameter("rate") * var1} - model.algebraic = {var2: var1 - var2} - model.initial_conditions = { - var1: pybamm.InputParameter("ic 1"), - var2: pybamm.InputParameter("ic 2"), - } - - # Solve - solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) - t_eval = np.linspace(0, 5, 100) - solution = solver.solve( - model, t_eval, inputs={"rate": -1, "ic 1": 0.1, "ic 2": 2} - ) - np.testing.assert_array_almost_equal( - solution.y[0], 0.1 * np.exp(-solution.t), decimal=5 - ) - np.testing.assert_array_almost_equal( - solution.y[-1], 0.1 * np.exp(-solution.t), decimal=5 - ) - - # Solve again with different initial conditions - solution = solver.solve( - model, t_eval, inputs={"rate": -0.1, "ic 1": 1, "ic 2": 3} - ) - np.testing.assert_array_almost_equal( - solution.y[0], 1 * np.exp(-0.1 * solution.t), decimal=5 - ) - np.testing.assert_array_almost_equal( - solution.y[-1], 1 * np.exp(-0.1 * solution.t), decimal=5 - ) - - def test_model_solver_with_external(self): - # Create model - model = pybamm.BaseModel() - domain = ["negative electrode", "separator", "positive electrode"] - var1 = pybamm.Variable("var1", domain=domain) - var2 = pybamm.Variable("var2", domain=domain) - model.rhs = {var1: -var2} - model.initial_conditions = {var1: 1} - model.external_variables = [var2] - model.variables = {"var1": var1, "var2": var2} - # No need to set parameters; can use base discretisation (no spatial - # operators) - - # create discretisation - mesh = get_mesh_for_testing() - spatial_methods = {"macroscale": pybamm.FiniteVolume()} - disc = pybamm.Discretisation(mesh, spatial_methods) - disc.process_model(model) - # Solve - solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) - t_eval = np.linspace(0, 10, 100) - solution = solver.solve(model, t_eval, external_variables={"var2": 0.5}) - np.testing.assert_allclose(solution.y[0], 1 - 0.5 * solution.t, rtol=1e-06) - - def test_model_solver_with_non_identity_mass(self): - model = pybamm.BaseModel() - var1 = pybamm.Variable("var1", domain="negative electrode") - var2 = pybamm.Variable("var2", domain="negative electrode") - model.rhs = {var1: var1} - model.algebraic = {var2: 2 * var1 - var2} - model.initial_conditions = {var1: 1, var2: 2} - disc = get_discretisation_for_testing() - disc.process_model(model) - - # FV discretisation has identity mass. Manually set the mass matrix to - # be a diag of 10s here for testing. Note that the algebraic part is all - # zeros - mass_matrix = 10 * model.mass_matrix.entries - model.mass_matrix = pybamm.Matrix(mass_matrix) - - # Note that mass_matrix_inv is just the inverse of the ode block of the - # mass matrix - mass_matrix_inv = 0.1 * eye(int(mass_matrix.shape[0] / 2)) - model.mass_matrix_inv = pybamm.Matrix(mass_matrix_inv) - - # Solve - solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) - t_eval = np.linspace(0, 1, 100) - solution = solver.solve(model, t_eval) - np.testing.assert_array_equal(solution.t, t_eval) - np.testing.assert_allclose(solution.y[0], np.exp(0.1 * solution.t)) - np.testing.assert_allclose(solution.y[-1], 2 * np.exp(0.1 * solution.t)) - - def test_dae_solver_algebraic_model(self): - model = pybamm.BaseModel() - var = pybamm.Variable("var") - model.algebraic = {var: var + 1} - model.initial_conditions = {var: 0} - - disc = pybamm.Discretisation() - disc.process_model(model) - - solver = pybamm.CasadiSolver() - t_eval = np.linspace(0, 1) - with self.assertRaisesRegex( - pybamm.SolverError, "Cannot use CasadiSolver to solve algebraic model" - ): - solver.solve(model, t_eval) +# class TestCasadiSolver(unittest.TestCase): +# def test_bad_mode(self): +# with self.assertRaisesRegex(ValueError, "invalid mode"): +# pybamm.CasadiSolver(mode="bad mode") + +# def test_model_solver(self): +# # Create model +# model = pybamm.BaseModel() +# var = pybamm.Variable("var") +# model.rhs = {var: 0.1 * var} +# model.initial_conditions = {var: 1} +# # No need to set parameters; can use base discretisation (no spatial operators) + +# # create discretisation +# disc = pybamm.Discretisation() +# model_disc = disc.process_model(model, inplace=False) +# # Solve +# solver = pybamm.CasadiSolver(mode="fast", rtol=1e-8, atol=1e-8) +# t_eval = np.linspace(0, 1, 100) +# solution = solver.solve(model_disc, t_eval) +# np.testing.assert_array_equal(solution.t, t_eval) +# np.testing.assert_array_almost_equal( +# solution.y[0], np.exp(0.1 * solution.t), decimal=5 +# ) + +# # Safe mode (enforce events that won't be triggered) +# model.events = [pybamm.Event("an event", var + 1)] +# disc.process_model(model) +# solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) +# solution = solver.solve(model, t_eval) +# np.testing.assert_array_equal(solution.t, t_eval) +# np.testing.assert_array_almost_equal( +# solution.y[0], np.exp(0.1 * solution.t), decimal=5 +# ) + +# # Safe mode, without grid (enforce events that won't be triggered) +# solver = pybamm.CasadiSolver(mode="safe without grid", rtol=1e-8, atol=1e-8) +# solution = solver.solve(model, t_eval) +# np.testing.assert_array_equal(solution.t, t_eval) +# np.testing.assert_array_almost_equal( +# solution.y[0], np.exp(0.1 * solution.t), decimal=5 +# ) + +# def test_model_solver_python(self): +# # Create model +# pybamm.set_logging_level("ERROR") +# model = pybamm.BaseModel() +# model.convert_to_format = "python" +# var = pybamm.Variable("var") +# model.rhs = {var: 0.1 * var} +# model.initial_conditions = {var: 1} +# # No need to set parameters; can use base discretisation (no spatial operators) + +# # create discretisation +# disc = pybamm.Discretisation() +# disc.process_model(model) +# # Solve +# solver = pybamm.CasadiSolver(mode="fast", rtol=1e-8, atol=1e-8) +# t_eval = np.linspace(0, 1, 100) +# solution = solver.solve(model, t_eval) +# np.testing.assert_array_equal(solution.t, t_eval) +# np.testing.assert_array_almost_equal( +# solution.y[0], np.exp(0.1 * solution.t), decimal=5 +# ) +# pybamm.set_logging_level("WARNING") + +# def test_model_solver_failure(self): +# # Create model +# model = pybamm.BaseModel() +# var = pybamm.Variable("var") +# model.rhs = {var: -pybamm.sqrt(var)} +# model.initial_conditions = {var: 1} +# # add events so that safe mode is used (won't be triggered) +# model.events = [pybamm.Event("10", var - 10)] +# # No need to set parameters; can use base discretisation (no spatial operators) + +# # create discretisation +# disc = pybamm.Discretisation() +# model_disc = disc.process_model(model, inplace=False) + +# solver = pybamm.CasadiSolver(extra_options_call={"regularity_check": False}) +# # Solve with failure at t=2 +# t_eval = np.linspace(0, 20, 100) +# with self.assertRaises(pybamm.SolverError): +# solver.solve(model_disc, t_eval) +# # Solve with failure at t=0 +# model.initial_conditions = {var: 0} +# model_disc = disc.process_model(model, inplace=False) +# t_eval = np.linspace(0, 20, 100) +# with self.assertRaises(pybamm.SolverError): +# solver.solve(model_disc, t_eval) + +# def test_model_solver_events(self): +# # Create model +# model = pybamm.BaseModel() +# whole_cell = ["negative electrode", "separator", "positive electrode"] +# var1 = pybamm.Variable("var1", domain=whole_cell) +# var2 = pybamm.Variable("var2", domain=whole_cell) +# model.rhs = {var1: 0.1 * var1} +# model.algebraic = {var2: 2 * var1 - var2} +# model.initial_conditions = {var1: 1, var2: 2} +# model.events = [ +# pybamm.Event("var1 = 1.5", pybamm.min(var1 - 1.5)), +# pybamm.Event("var2 = 2.5", pybamm.min(var2 - 2.5)), +# ] +# disc = get_discretisation_for_testing() +# disc.process_model(model) + +# # Solve using "safe" mode +# solver = pybamm.CasadiSolver(mode="safe", rtol=1e-8, atol=1e-8) +# t_eval = np.linspace(0, 5, 100) +# solution = solver.solve(model, t_eval) +# np.testing.assert_array_less(solution.y[0], 1.5) +# np.testing.assert_array_less(solution.y[-1], 2.5 + 1e-10) +# np.testing.assert_array_almost_equal( +# solution.y[0], np.exp(0.1 * solution.t), decimal=5 +# ) +# np.testing.assert_array_almost_equal( +# solution.y[-1], 2 * np.exp(0.1 * solution.t), decimal=5 +# ) + +# # Solve using "safe" mode with debug off +# pybamm.settings.debug_mode = False +# solver = pybamm.CasadiSolver(mode="safe", rtol=1e-8, atol=1e-8, dt_max=1) +# t_eval = np.linspace(0, 5, 100) +# solution = solver.solve(model, t_eval) +# np.testing.assert_array_less(solution.y[0], 1.5) +# np.testing.assert_array_less(solution.y[-1], 2.5 + 1e-10) +# # test the last entry is exactly 2.5 +# np.testing.assert_array_almost_equal(solution.y[-1, -1], 2.5, decimal=2) +# np.testing.assert_array_almost_equal( +# solution.y[0], np.exp(0.1 * solution.t), decimal=5 +# ) +# np.testing.assert_array_almost_equal( +# solution.y[-1], 2 * np.exp(0.1 * solution.t), decimal=5 +# ) +# pybamm.settings.debug_mode = True + +# # Try dt_max=0 to enforce using all timesteps +# solver = pybamm.CasadiSolver(dt_max=0, rtol=1e-8, atol=1e-8) +# t_eval = np.linspace(0, 5, 100) +# solution = solver.solve(model, t_eval) +# np.testing.assert_array_less(solution.y[0], 1.5) +# np.testing.assert_array_less(solution.y[-1], 2.5) +# np.testing.assert_array_almost_equal( +# solution.y[0], np.exp(0.1 * solution.t), decimal=5 +# ) +# np.testing.assert_array_almost_equal( +# solution.y[-1], 2 * np.exp(0.1 * solution.t), decimal=5 +# ) + +# # Test when an event returns nan +# model = pybamm.BaseModel() +# var = pybamm.Variable("var") +# model.rhs = {var: 0.1 * var} +# model.initial_conditions = {var: 1} +# model.events = [ +# pybamm.Event("event", var - 1.02), +# pybamm.Event("sqrt event", pybamm.sqrt(1.0199 - var)), +# ] +# disc = pybamm.Discretisation() +# disc.process_model(model) +# solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) +# solution = solver.solve(model, t_eval) +# np.testing.assert_array_less(solution.y[0], 1.02 + 1e-10) +# np.testing.assert_array_almost_equal(solution.y[0, -1], 1.02, decimal=2) + +# def test_model_step(self): +# # Create model +# model = pybamm.BaseModel() +# domain = ["negative electrode", "separator", "positive electrode"] +# var = pybamm.Variable("var", domain=domain) +# model.rhs = {var: 0.1 * var} +# model.initial_conditions = {var: 1} +# # No need to set parameters; can use base discretisation (no spatial operators) + +# # create discretisation +# mesh = get_mesh_for_testing() +# spatial_methods = {"macroscale": pybamm.FiniteVolume()} +# disc = pybamm.Discretisation(mesh, spatial_methods) +# disc.process_model(model) + +# solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) + +# # Step once +# dt = 1 +# step_sol = solver.step(None, model, dt) +# np.testing.assert_array_equal(step_sol.t, [0, dt]) +# np.testing.assert_array_almost_equal(step_sol.y[0], np.exp(0.1 * step_sol.t)) + +# # Step again (return 5 points) +# step_sol_2 = solver.step(step_sol, model, dt, npts=5) +# np.testing.assert_array_equal( +# step_sol_2.t, np.concatenate([np.array([0]), np.linspace(dt, 2 * dt, 5)]) +# ) +# np.testing.assert_array_almost_equal( +# step_sol_2.y[0], np.exp(0.1 * step_sol_2.t) +# ) + +# # Check steps give same solution as solve +# t_eval = step_sol.t +# solution = solver.solve(model, t_eval) +# np.testing.assert_array_almost_equal(solution.y[0], step_sol.y[0]) + +# def test_model_step_with_input(self): +# # Create model +# model = pybamm.BaseModel() +# var = pybamm.Variable("var") +# a = pybamm.InputParameter("a") +# model.rhs = {var: a * var} +# model.initial_conditions = {var: 1} +# model.variables = {"a": a} +# # No need to set parameters; can use base discretisation (no spatial operators) + +# # create discretisation +# disc = pybamm.Discretisation() +# disc.process_model(model) + +# solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) + +# # Step with an input +# dt = 0.1 +# step_sol = solver.step(None, model, dt, npts=5, inputs={"a": 0.1}) +# np.testing.assert_array_equal(step_sol.t, np.linspace(0, dt, 5)) +# np.testing.assert_allclose(step_sol.y[0], np.exp(0.1 * step_sol.t)) + +# # Step again with different inputs +# step_sol_2 = solver.step(step_sol, model, dt, npts=5, inputs={"a": -1}) +# np.testing.assert_array_equal(step_sol_2.t, np.linspace(0, 2 * dt, 9)) +# np.testing.assert_array_equal( +# step_sol_2["a"].entries, np.array([0.1, 0.1, 0.1, 0.1, 0.1, -1, -1, -1, -1]) +# ) +# np.testing.assert_allclose( +# step_sol_2.y[0], +# np.concatenate( +# [ +# np.exp(0.1 * step_sol.t[:5]), +# np.exp(0.1 * step_sol.t[4]) * np.exp(-(step_sol.t[5:] - dt)), +# ] +# ), +# ) + +# def test_model_step_events(self): +# # Create model +# model = pybamm.BaseModel() +# var1 = pybamm.Variable("var1") +# var2 = pybamm.Variable("var2") +# model.rhs = {var1: 0.1 * var1} +# model.algebraic = {var2: 2 * var1 - var2} +# model.initial_conditions = {var1: 1, var2: 2} +# model.events = [ +# pybamm.Event("var1 = 1.5", pybamm.min(var1 - 1.5)), +# pybamm.Event("var2 = 2.5", pybamm.min(var2 - 2.5)), +# ] +# disc = pybamm.Discretisation() +# disc.process_model(model) + +# # Solve +# step_solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) +# dt = 0.05 +# time = 0 +# end_time = 5 +# step_solution = None +# while time < end_time: +# step_solution = step_solver.step(step_solution, model, dt=dt, npts=10) +# time += dt +# np.testing.assert_array_less(step_solution.y[0], 1.5) +# np.testing.assert_array_less(step_solution.y[-1], 2.5001) +# np.testing.assert_array_almost_equal( +# step_solution.y[0], np.exp(0.1 * step_solution.t), decimal=5 +# ) +# np.testing.assert_array_almost_equal( +# step_solution.y[-1], 2 * np.exp(0.1 * step_solution.t), decimal=4 +# ) + +# def test_model_solver_with_inputs(self): +# # Create model +# model = pybamm.BaseModel() +# domain = ["negative electrode", "separator", "positive electrode"] +# var = pybamm.Variable("var", domain=domain) +# model.rhs = {var: -pybamm.InputParameter("rate") * var} +# model.initial_conditions = {var: 1} +# model.events = [pybamm.Event("var=0.5", pybamm.min(var - 0.5))] +# # No need to set parameters; can use base discretisation (no spatial +# # operators) + +# # create discretisation +# mesh = get_mesh_for_testing() +# spatial_methods = {"macroscale": pybamm.FiniteVolume()} +# disc = pybamm.Discretisation(mesh, spatial_methods) +# disc.process_model(model) +# # Solve +# solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) +# t_eval = np.linspace(0, 10, 100) +# solution = solver.solve(model, t_eval, inputs={"rate": 0.1}) +# self.assertLess(len(solution.t), len(t_eval)) +# np.testing.assert_allclose(solution.y[0], np.exp(-0.1 * solution.t), rtol=1e-04) + +# def test_model_solver_dae_inputs_in_initial_conditions(self): +# # Create model +# model = pybamm.BaseModel() +# var1 = pybamm.Variable("var1") +# var2 = pybamm.Variable("var2") +# model.rhs = {var1: pybamm.InputParameter("rate") * var1} +# model.algebraic = {var2: var1 - var2} +# model.initial_conditions = { +# var1: pybamm.InputParameter("ic 1"), +# var2: pybamm.InputParameter("ic 2"), +# } + +# # Solve +# solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) +# t_eval = np.linspace(0, 5, 100) +# solution = solver.solve( +# model, t_eval, inputs={"rate": -1, "ic 1": 0.1, "ic 2": 2} +# ) +# np.testing.assert_array_almost_equal( +# solution.y[0], 0.1 * np.exp(-solution.t), decimal=5 +# ) +# np.testing.assert_array_almost_equal( +# solution.y[-1], 0.1 * np.exp(-solution.t), decimal=5 +# ) + +# # Solve again with different initial conditions +# solution = solver.solve( +# model, t_eval, inputs={"rate": -0.1, "ic 1": 1, "ic 2": 3} +# ) +# np.testing.assert_array_almost_equal( +# solution.y[0], 1 * np.exp(-0.1 * solution.t), decimal=5 +# ) +# np.testing.assert_array_almost_equal( +# solution.y[-1], 1 * np.exp(-0.1 * solution.t), decimal=5 +# ) + +# def test_model_solver_with_external(self): +# # Create model +# model = pybamm.BaseModel() +# domain = ["negative electrode", "separator", "positive electrode"] +# var1 = pybamm.Variable("var1", domain=domain) +# var2 = pybamm.Variable("var2", domain=domain) +# model.rhs = {var1: -var2} +# model.initial_conditions = {var1: 1} +# model.external_variables = [var2] +# model.variables = {"var1": var1, "var2": var2} +# # No need to set parameters; can use base discretisation (no spatial +# # operators) + +# # create discretisation +# mesh = get_mesh_for_testing() +# spatial_methods = {"macroscale": pybamm.FiniteVolume()} +# disc = pybamm.Discretisation(mesh, spatial_methods) +# disc.process_model(model) +# # Solve +# solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) +# t_eval = np.linspace(0, 10, 100) +# solution = solver.solve(model, t_eval, external_variables={"var2": 0.5}) +# np.testing.assert_allclose(solution.y[0], 1 - 0.5 * solution.t, rtol=1e-06) + +# def test_model_solver_with_non_identity_mass(self): +# model = pybamm.BaseModel() +# var1 = pybamm.Variable("var1", domain="negative electrode") +# var2 = pybamm.Variable("var2", domain="negative electrode") +# model.rhs = {var1: var1} +# model.algebraic = {var2: 2 * var1 - var2} +# model.initial_conditions = {var1: 1, var2: 2} +# disc = get_discretisation_for_testing() +# disc.process_model(model) + +# # FV discretisation has identity mass. Manually set the mass matrix to +# # be a diag of 10s here for testing. Note that the algebraic part is all +# # zeros +# mass_matrix = 10 * model.mass_matrix.entries +# model.mass_matrix = pybamm.Matrix(mass_matrix) + +# # Note that mass_matrix_inv is just the inverse of the ode block of the +# # mass matrix +# mass_matrix_inv = 0.1 * eye(int(mass_matrix.shape[0] / 2)) +# model.mass_matrix_inv = pybamm.Matrix(mass_matrix_inv) + +# # Solve +# solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) +# t_eval = np.linspace(0, 1, 100) +# solution = solver.solve(model, t_eval) +# np.testing.assert_array_equal(solution.t, t_eval) +# np.testing.assert_allclose(solution.y[0], np.exp(0.1 * solution.t)) +# np.testing.assert_allclose(solution.y[-1], 2 * np.exp(0.1 * solution.t)) + +# def test_dae_solver_algebraic_model(self): +# model = pybamm.BaseModel() +# var = pybamm.Variable("var") +# model.algebraic = {var: var + 1} +# model.initial_conditions = {var: 0} + +# disc = pybamm.Discretisation() +# disc.process_model(model) + +# solver = pybamm.CasadiSolver() +# t_eval = np.linspace(0, 1) +# with self.assertRaisesRegex( +# pybamm.SolverError, "Cannot use CasadiSolver to solve algebraic model" +# ): +# solver.solve(model, t_eval) class TestCasadiSolverSensitivity(unittest.TestCase): @@ -427,57 +427,61 @@ def test_solve_with_symbolic_input(self): disc.process_model(model) # Solve - solver = pybamm.CasadiSolver() + solver = pybamm.CasadiSolver(sensitivity="casadi") t_eval = np.linspace(0, 1) - solution = solver.solve(model, t_eval) - np.testing.assert_array_almost_equal( - solution["var"].value({"param": 7}).full().flatten(), 2 + 7 * t_eval - ) + solution = solver.solve(model, t_eval, inputs={"param": 7}) + np.testing.assert_array_almost_equal(solution["var"].data, 2 + 7 * t_eval) np.testing.assert_array_almost_equal( - solution["var"].value({"param": -3}).full().flatten(), 2 - 3 * t_eval + solution["var"].sensitivity["param"], t_eval[:, np.newaxis] ) + + solution = solver.solve(model, t_eval, inputs={"param": -3}) + np.testing.assert_array_almost_equal(solution["var"].data, 2 - 3 * t_eval) np.testing.assert_array_almost_equal( - solution["var"].sensitivity({"param": 3}).full().flatten(), t_eval + solution["var"].sensitivity["param"], t_eval[:, np.newaxis] ) - def test_least_squares_fit(self): - # Simple system: a single algebraic equation - var1 = pybamm.Variable("var1", domain="negative electrode") - var2 = pybamm.Variable("var2", domain="negative electrode") - model = pybamm.BaseModel() - p = pybamm.InputParameter("p") - q = pybamm.InputParameter("q") - model.rhs = {var1: -var1} - model.algebraic = {var2: (var2 - p)} - model.initial_conditions = {var1: 1, var2: 3} - model.variables = {"objective": (var2 - q) ** 2 + (p - 3) ** 2} + # def test_least_squares_fit(self): + # # Simple system: a single algebraic equation + # var1 = pybamm.Variable("var1", domain="negative electrode") + # var2 = pybamm.Variable("var2", domain="negative electrode") + # model = pybamm.BaseModel() + # p = pybamm.InputParameter("p") + # q = pybamm.InputParameter("q") + # model.rhs = {var1: -var1} + # model.algebraic = {var2: (var2 - p)} + # model.initial_conditions = {var1: 1, var2: 3} + # model.variables = {"objective": (var2 - q) ** 2 + (p - 3) ** 2} - # create discretisation - disc = get_discretisation_for_testing() - disc.process_model(model) + # # create discretisation + # disc = get_discretisation_for_testing() + # disc.process_model(model) - # Solve - solver = pybamm.CasadiSolver() - solution = solver.solve(model, np.linspace(0, 1)) - sol_var = solution["objective"] + # # Solve + # solver = pybamm.CasadiSolver() + # solution = solver.solve(model, np.linspace(0, 1)) + # sol_var = solution["objective"] - def objective(x): - return sol_var.value({"p": x[0], "q": x[1]}).full().flatten() + # def objective(x): + # return sol_var.value({"p": x[0], "q": x[1]}).full().flatten() - # without jacobian - lsq_sol = least_squares(objective, [2, 2], method="lm") - np.testing.assert_array_almost_equal(lsq_sol.x, [3, 3], decimal=3) + # # without jacobian + # lsq_sol = least_squares(objective, [2, 2], method="lm") + # np.testing.assert_array_almost_equal(lsq_sol.x, [3, 3], decimal=3) - def jac(x): - return sol_var.sensitivity({"p": x[0], "q": x[1]}) + # def jac(x): + # return sol_var.sensitivity({"p": x[0], "q": x[1]}) - # with jacobian - lsq_sol = least_squares(objective, [2, 2], jac=jac, method="lm") - np.testing.assert_array_almost_equal(lsq_sol.x, [3, 3], decimal=3) + # # with jacobian + # lsq_sol = least_squares(objective, [2, 2], jac=jac, method="lm") + # np.testing.assert_array_almost_equal(lsq_sol.x, [3, 3], decimal=3) - def test_solve_with_symbolic_input_1D_scalar_input(self): + def test_solve_with_symbolic_input_vector_output_scalar_input(self): var = pybamm.Variable("var", "negative electrode") model = pybamm.BaseModel() + # Set length scale to avoid warning + model.length_scales = {"negative electrode": 1} + param = pybamm.InputParameter("param") model.rhs = {var: -param * var} model.initial_conditions = {var: 2} @@ -486,32 +490,34 @@ def test_solve_with_symbolic_input_1D_scalar_input(self): # create discretisation disc = get_discretisation_for_testing() disc.process_model(model) + n = disc.mesh["negative electrode"].npts # Solve - scalar input - solver = pybamm.CasadiSolver() + solver = pybamm.CasadiSolver(sensitivity="casadi") t_eval = np.linspace(0, 1) - solution = solver.solve(model, t_eval) + solution = solver.solve(model, t_eval, inputs={"param": 7}) np.testing.assert_array_almost_equal( - solution["var"].value({"param": 7}), - np.repeat(2 * np.exp(-7 * t_eval), 40)[:, np.newaxis], - decimal=4, + solution["var"].data, np.tile(2 * np.exp(-7 * t_eval), (n, 1)), decimal=4, ) + + solution = solver.solve(model, t_eval, inputs={"param": 3}) np.testing.assert_array_almost_equal( - solution["var"].value({"param": 3}), - np.repeat(2 * np.exp(-3 * t_eval), 40)[:, np.newaxis], - decimal=4, + solution["var"].data, np.tile(2 * np.exp(-3 * t_eval), (n, 1)), decimal=4, ) np.testing.assert_array_almost_equal( - solution["var"].sensitivity({"param": 3}), + solution["var"].sensitivity["param"], np.repeat( -2 * t_eval * np.exp(-3 * t_eval), disc.mesh["negative electrode"].npts )[:, np.newaxis], decimal=4, ) - def test_solve_with_symbolic_input_1D_vector_input(self): + def test_solve_with_symbolic_input_vector_output_vector_input(self): var = pybamm.Variable("var", "negative electrode") model = pybamm.BaseModel() + # Set length scale to avoid warning + model.length_scales = {"negative electrode": 1} + param = pybamm.InputParameter("param", "negative electrode") model.rhs = {var: -param * var} model.initial_conditions = {var: 2} @@ -520,37 +526,31 @@ def test_solve_with_symbolic_input_1D_vector_input(self): # create discretisation disc = get_discretisation_for_testing() disc.process_model(model) - - # Solve - scalar input - solver = pybamm.CasadiSolver() - solution = solver.solve(model, np.linspace(0, 1)) n = disc.mesh["negative electrode"].npts - solver = pybamm.CasadiSolver() + solver = pybamm.CasadiSolver(sensitivity="casadi") t_eval = np.linspace(0, 1) - solution = solver.solve(model, t_eval) - p = np.linspace(0, 1, n)[:, np.newaxis] + solution = solver.solve(model, t_eval, inputs={"param": 3 * np.ones(n)}) np.testing.assert_array_almost_equal( - solution["var"].value({"param": 3 * np.ones(n)}), - np.repeat(2 * np.exp(-3 * t_eval), 40)[:, np.newaxis], - decimal=4, + solution["var"].data, np.tile(2 * np.exp(-3 * t_eval), (n, 1)), decimal=4, ) np.testing.assert_array_almost_equal( - solution["var"].value({"param": 2 * p}), - 2 * np.exp(-2 * p * t_eval).T.reshape(-1, 1), + solution["var"].sensitivity["param"], + np.kron(-2 * t_eval * np.exp(-3 * t_eval), np.eye(n)).T, decimal=4, ) + + p = np.linspace(0, 1, n)[:, np.newaxis] + solution = solver.solve(model, t_eval, inputs={"param": 2 * p}) np.testing.assert_array_almost_equal( - solution["var"].sensitivity({"param": 3 * np.ones(n)}), - np.kron(-2 * t_eval * np.exp(-3 * t_eval), np.eye(40)).T, - decimal=4, + solution["var"].data, 2 * np.exp(-2 * p * t_eval), decimal=4, ) - sens = solution["var"].sensitivity({"param": p}).full() + sens = solution["var"].sensitivity["param"] for idx in range(len(t_eval)): np.testing.assert_array_almost_equal( sens[40 * idx : 40 * (idx + 1), :], - -2 * t_eval[idx] * np.exp(-p * t_eval[idx]) * np.eye(40), + -2 * t_eval[idx] * np.exp(-2 * p * t_eval[idx]) * np.eye(40), decimal=4, ) @@ -567,471 +567,319 @@ def test_solve_with_symbolic_input_in_initial_conditions(self): disc.process_model(model) # Solve - solver = pybamm.CasadiSolver(atol=1e-10, rtol=1e-10) - t_eval = np.linspace(0, 1) - solution = solver.solve(model, t_eval) - np.testing.assert_array_almost_equal( - solution["var"].value({"param": 7}), 7 * np.exp(-t_eval)[np.newaxis, :] - ) - np.testing.assert_array_almost_equal( - solution["var"].value({"param": 3}), 3 * np.exp(-t_eval)[np.newaxis, :] - ) - np.testing.assert_array_almost_equal( - solution["var"].sensitivity({"param": 3}), np.exp(-t_eval)[:, np.newaxis] - ) - - def test_least_squares_fit_input_in_initial_conditions(self): - # Simple system: a single algebraic equation - var1 = pybamm.Variable("var1", domain="negative electrode") - var2 = pybamm.Variable("var2", domain="negative electrode") - model = pybamm.BaseModel() - p = pybamm.InputParameter("p") - q = pybamm.InputParameter("q") - model.rhs = {var1: -var1} - model.algebraic = {var2: (var2 - p)} - model.initial_conditions = {var1: 1, var2: p} - model.variables = {"objective": (var2 - q) ** 2 + (p - 3) ** 2} - - # create discretisation - disc = get_discretisation_for_testing() - disc.process_model(model) - - # Solve - solver = pybamm.CasadiSolver() - solution = solver.solve(model, np.linspace(0, 1)) - sol_var = solution["objective"] - - def objective(x): - return sol_var.value({"p": x[0], "q": x[1]}).full().flatten() - - # without jacobian - lsq_sol = least_squares(objective, [2, 2], method="lm") - np.testing.assert_array_almost_equal(lsq_sol.x, [3, 3], decimal=3) - - -class TestCasadiSolverODEsWithForwardSensitivityEquations(unittest.TestCase): - def test_solve_sensitivity_scalar_var_scalar_input(self): - # Create model - model = pybamm.BaseModel() - var = pybamm.Variable("var") - p = pybamm.InputParameter("p") - model.rhs = {var: p * var} - model.initial_conditions = {var: 1} - model.variables = {"var squared": var ** 2} - - # Solve - # Make sure that passing in extra options works - solver = pybamm.CasadiSolver( - mode="fast", rtol=1e-10, atol=1e-10, sensitivity="explicit forward" - ) - t_eval = np.linspace(0, 1, 80) - solution = solver.solve(model, t_eval, inputs={"p": 0.1}) - np.testing.assert_array_equal(solution.t, t_eval) - np.testing.assert_allclose(solution.y[0], np.exp(0.1 * solution.t)) - np.testing.assert_allclose( - solution.sensitivity["p"], - (solution.t * np.exp(0.1 * solution.t))[:, np.newaxis], - ) - np.testing.assert_allclose( - solution["var squared"].data, np.exp(0.1 * solution.t) ** 2 - ) - np.testing.assert_allclose( - solution["var squared"].sensitivity["p"], - (2 * np.exp(0.1 * solution.t) * solution.t * np.exp(0.1 * solution.t))[ - :, np.newaxis - ], - ) - - # More complicated model - # Create model - model = pybamm.BaseModel() - var = pybamm.Variable("var") - p = pybamm.InputParameter("p") - q = pybamm.InputParameter("q") - r = pybamm.InputParameter("r") - s = pybamm.InputParameter("s") - model.rhs = {var: p * q} - model.initial_conditions = {var: r} - model.variables = {"var times s": var * s} - - # Solve - # Make sure that passing in extra options works - solver = pybamm.CasadiSolver( - rtol=1e-10, atol=1e-10, sensitivity="explicit forward" - ) - t_eval = np.linspace(0, 1, 80) - solution = solver.solve( - model, t_eval, inputs={"p": 0.1, "q": 2, "r": -1, "s": 0.5} - ) - np.testing.assert_allclose(solution.y[0], -1 + 0.2 * solution.t) - np.testing.assert_allclose( - solution.sensitivity["p"], (2 * solution.t)[:, np.newaxis], - ) - np.testing.assert_allclose( - solution.sensitivity["q"], (0.1 * solution.t)[:, np.newaxis], - ) - np.testing.assert_allclose(solution.sensitivity["r"], 1) - np.testing.assert_allclose(solution.sensitivity["s"], 0) - np.testing.assert_allclose( - solution.sensitivity["all"], - np.hstack( - [ - solution.sensitivity["p"], - solution.sensitivity["q"], - solution.sensitivity["r"], - solution.sensitivity["s"], - ] - ), - ) - np.testing.assert_allclose( - solution["var times s"].data, 0.5 * (-1 + 0.2 * solution.t) - ) - np.testing.assert_allclose( - solution["var times s"].sensitivity["p"], - 0.5 * (2 * solution.t)[:, np.newaxis], - ) - np.testing.assert_allclose( - solution["var times s"].sensitivity["q"], - 0.5 * (0.1 * solution.t)[:, np.newaxis], - ) - np.testing.assert_allclose(solution["var times s"].sensitivity["r"], 0.5) - np.testing.assert_allclose( - solution["var times s"].sensitivity["s"], - (-1 + 0.2 * solution.t)[:, np.newaxis], - ) - np.testing.assert_allclose( - solution["var times s"].sensitivity["all"], - np.hstack( - [ - solution["var times s"].sensitivity["p"], - solution["var times s"].sensitivity["q"], - solution["var times s"].sensitivity["r"], - solution["var times s"].sensitivity["s"], - ] - ), - ) - - def test_solve_sensitivity_vector_var_scalar_input(self): - var = pybamm.Variable("var", "negative electrode") - model = pybamm.BaseModel() - # Set length scales to avoid warning - model.length_scales = {"negative electrode": 1} - param = pybamm.InputParameter("param") - model.rhs = {var: -param * var} - model.initial_conditions = {var: 2} - model.variables = {"var": var} - - # create discretisation - disc = get_discretisation_for_testing() - disc.process_model(model) - n = disc.mesh["negative electrode"].npts - - # Solve - scalar input - solver = pybamm.CasadiSolver(sensitivity="explicit forward") - t_eval = np.linspace(0, 1) - solution = solver.solve(model, t_eval, inputs={"param": 7}) - np.testing.assert_array_almost_equal( - solution["var"].data, np.tile(2 * np.exp(-7 * t_eval), (n, 1)), decimal=4, - ) - np.testing.assert_array_almost_equal( - solution["var"].sensitivity["param"], - np.repeat(-2 * t_eval * np.exp(-7 * t_eval), n)[:, np.newaxis], - decimal=4, - ) - - # More complicated model - # Create model - model = pybamm.BaseModel() - # Set length scales to avoid warning - model.length_scales = {"negative electrode": 1} - var = pybamm.Variable("var", "negative electrode") - p = pybamm.InputParameter("p") - q = pybamm.InputParameter("q") - r = pybamm.InputParameter("r") - s = pybamm.InputParameter("s") - model.rhs = {var: p * q} - model.initial_conditions = {var: r} - model.variables = {"var times s": var * s} - - # Discretise - disc.process_model(model) - - # Solve - # Make sure that passing in extra options works - solver = pybamm.CasadiSolver( - rtol=1e-10, atol=1e-10, sensitivity="explicit forward" - ) - t_eval = np.linspace(0, 1, 80) - solution = solver.solve( - model, t_eval, inputs={"p": 0.1, "q": 2, "r": -1, "s": 0.5} - ) - np.testing.assert_allclose(solution.y, np.tile(-1 + 0.2 * solution.t, (n, 1))) - np.testing.assert_allclose( - solution.sensitivity["p"], np.repeat(2 * solution.t, n)[:, np.newaxis], - ) - np.testing.assert_allclose( - solution.sensitivity["q"], np.repeat(0.1 * solution.t, n)[:, np.newaxis], - ) - np.testing.assert_allclose(solution.sensitivity["r"], 1) - np.testing.assert_allclose(solution.sensitivity["s"], 0) - np.testing.assert_allclose( - solution.sensitivity["all"], - np.hstack( - [ - solution.sensitivity["p"], - solution.sensitivity["q"], - solution.sensitivity["r"], - solution.sensitivity["s"], - ] - ), - ) - np.testing.assert_allclose( - solution["var times s"].data, np.tile(0.5 * (-1 + 0.2 * solution.t), (n, 1)) - ) - np.testing.assert_allclose( - solution["var times s"].sensitivity["p"], - np.repeat(0.5 * (2 * solution.t), n)[:, np.newaxis], - ) - np.testing.assert_allclose( - solution["var times s"].sensitivity["q"], - np.repeat(0.5 * (0.1 * solution.t), n)[:, np.newaxis], - ) - np.testing.assert_allclose(solution["var times s"].sensitivity["r"], 0.5) - np.testing.assert_allclose( - solution["var times s"].sensitivity["s"], - np.repeat(-1 + 0.2 * solution.t, n)[:, np.newaxis], - ) - np.testing.assert_allclose( - solution["var times s"].sensitivity["all"], - np.hstack( - [ - solution["var times s"].sensitivity["p"], - solution["var times s"].sensitivity["q"], - solution["var times s"].sensitivity["r"], - solution["var times s"].sensitivity["s"], - ] - ), - ) - - def test_solve_sensitivity_scalar_var_vector_input(self): - var = pybamm.Variable("var", "negative electrode") - model = pybamm.BaseModel() - # Set length scales to avoid warning - model.length_scales = {"negative electrode": 1} - - param = pybamm.InputParameter("param", "negative electrode") - model.rhs = {var: -param * var} - model.initial_conditions = {var: 2} - model.variables = { - "var": var, - "integral of var": pybamm.Integral(var, pybamm.standard_spatial_vars.x_n), - } - - # create discretisation - mesh = get_mesh_for_testing(xpts=5) - spatial_methods = {"macroscale": pybamm.FiniteVolume()} - disc = pybamm.Discretisation(mesh, spatial_methods) - disc.process_model(model) - n = disc.mesh["negative electrode"].npts - - # Solve - constant input - solver = pybamm.CasadiSolver( - mode="fast", rtol=1e-10, atol=1e-10, sensitivity="explicit forward" - ) - t_eval = np.linspace(0, 1) - solution = solver.solve(model, t_eval, inputs={"param": 7 * np.ones(n)}) - l_n = mesh["negative electrode"].edges[-1] - np.testing.assert_array_almost_equal( - solution["var"].data, np.tile(2 * np.exp(-7 * t_eval), (n, 1)), decimal=4, - ) - - np.testing.assert_array_almost_equal( - solution["var"].sensitivity["param"], - np.vstack([np.eye(n) * -2 * t * np.exp(-7 * t) for t in t_eval]), - ) - np.testing.assert_array_almost_equal( - solution["integral of var"].data, 2 * np.exp(-7 * t_eval) * l_n, decimal=4, - ) - np.testing.assert_array_almost_equal( - solution["integral of var"].sensitivity["param"], - np.tile(-2 * t_eval * np.exp(-7 * t_eval) * l_n / n, (n, 1)).T, - ) - - # Solve - linspace input - p_eval = np.linspace(1, 2, n) - solution = solver.solve(model, t_eval, inputs={"param": p_eval}) - l_n = mesh["negative electrode"].edges[-1] - np.testing.assert_array_almost_equal( - solution["var"].data, 2 * np.exp(-p_eval[:, np.newaxis] * t_eval), decimal=4 - ) - np.testing.assert_array_almost_equal( - solution["var"].sensitivity["param"], - np.vstack([np.diag(-2 * t * np.exp(-p_eval * t)) for t in t_eval]), - ) - - np.testing.assert_array_almost_equal( - solution["integral of var"].data, - np.sum( - 2 - * np.exp(-p_eval[:, np.newaxis] * t_eval) - * mesh["negative electrode"].d_edges[:, np.newaxis], - axis=0, - ), - ) - np.testing.assert_array_almost_equal( - solution["integral of var"].sensitivity["param"], - np.vstack([-2 * t * np.exp(-p_eval * t) * l_n / n for t in t_eval]), - ) - - -class TestCasadiSolverDAEsWithForwardSensitivityEquations(unittest.TestCase): - def test_solve_sensitivity_scalar_var_scalar_input(self): - # Create model - model = pybamm.BaseModel() - var = pybamm.Variable("var") - var2 = pybamm.Variable("var2") - p = pybamm.InputParameter("p") - model.rhs = {var: p * var} - model.algebraic = {var2: var2 - p} - model.initial_conditions = {var: 1, var2: p} - model.variables = {"prod": var * var2} - - # Solve - # Make sure that passing in extra options works - solver = pybamm.CasadiSolver( - mode="fast", rtol=1e-10, atol=1e-10, sensitivity="explicit forward" - ) - t_eval = np.linspace(0, 1, 80) - solution = solver.solve(model, t_eval, inputs={"p": 0.1}) - np.testing.assert_array_equal(solution.t, t_eval) - np.testing.assert_allclose(solution.y[0], np.exp(0.1 * solution.t)) - np.testing.assert_allclose(solution.y[1], 0.1) - np.testing.assert_allclose( - solution.sensitivity["p"], - np.hstack( - [ - (solution.t * np.exp(0.1 * solution.t))[:, np.newaxis], - np.ones((len(t_eval), 1)), - ] - ).reshape(2 * len(t_eval), 1), - ) - np.testing.assert_allclose( - solution["prod"].data, 0.1 * np.exp(0.1 * solution.t) - ) - np.testing.assert_allclose( - solution["prod"].sensitivity["p"], - ((1 + 0.1 * solution.t) * np.exp(0.1 * solution.t))[:, np.newaxis], - ) - - # More complicated model - # Create model - model = pybamm.BaseModel() - var = pybamm.Variable("var") - var2 = pybamm.Variable("var2") - p = pybamm.InputParameter("p") - q = pybamm.InputParameter("q") - r = pybamm.InputParameter("r") - s = pybamm.InputParameter("s") - model.rhs = {var: p} - model.algebraic = {var2: var2 - q} - model.initial_conditions = {var: r, var2: q} - model.variables = {"var prod times s": var * var2 * s} - - # Solve - # Make sure that passing in extra options works - solver = pybamm.CasadiSolver( - rtol=1e-10, atol=1e-10, sensitivity="explicit forward" - ) - t_eval = np.linspace(0, 1, 3) - solution = solver.solve( - model, t_eval, inputs={"p": 0.1, "q": 2, "r": -1, "s": 0.5} - ) - np.testing.assert_allclose(solution.y[0], -1 + 0.1 * solution.t) - np.testing.assert_allclose(solution.y[1], 2) - n_t = len(t_eval) - zeros = np.zeros((n_t, 1)) - ones = np.ones((n_t, 1)) - np.testing.assert_allclose( - solution.sensitivity["p"], - np.hstack([solution.t[:, np.newaxis], zeros]).reshape(2 * n_t, 1), - ) - np.testing.assert_allclose( - solution.sensitivity["q"], np.hstack([zeros, ones]).reshape(2 * n_t, 1), - ) - np.testing.assert_allclose( - solution.sensitivity["r"], np.hstack([ones, zeros]).reshape(2 * n_t, 1) - ) - np.testing.assert_allclose(solution.sensitivity["s"], 0) - np.testing.assert_allclose( - solution.sensitivity["all"], - np.hstack( - [ - solution.sensitivity["p"], - solution.sensitivity["q"], - solution.sensitivity["r"], - solution.sensitivity["s"], - ] - ), - ) - np.testing.assert_allclose( - solution["var prod times s"].data, 0.5 * 2 * (-1 + 0.1 * solution.t) - ) - np.testing.assert_allclose( - solution["var prod times s"].sensitivity["p"], - 0.5 * (2 * solution.t)[:, np.newaxis], - ) - np.testing.assert_allclose( - solution["var prod times s"].sensitivity["q"], - 0.5 * (-1 + 0.1 * solution.t)[:, np.newaxis], - ) - np.testing.assert_allclose(solution["var prod times s"].sensitivity["r"], 1) - np.testing.assert_allclose( - solution["var prod times s"].sensitivity["s"], - 2 * (-1 + 0.1 * solution.t)[:, np.newaxis], - ) - np.testing.assert_allclose( - solution["var prod times s"].sensitivity["all"], - np.hstack( - [ - solution["var prod times s"].sensitivity["p"], - solution["var prod times s"].sensitivity["q"], - solution["var prod times s"].sensitivity["r"], - solution["var prod times s"].sensitivity["s"], - ] - ), - ) - - def test_solve_sensitivity_vector_var_scalar_input(self): - var = pybamm.Variable("var", "negative electrode") - var2 = pybamm.Variable("var2", "negative electrode") - model = pybamm.BaseModel() - # Set length scales to avoid warning - model.length_scales = {"negative electrode": 1} - param = pybamm.InputParameter("param") - model.rhs = {var: -param * var} - model.algebraic = {var2: var2 - param} - model.initial_conditions = {var: 2, var2: param} - model.variables = {"prod": var * var2} - - # create discretisation - disc = get_discretisation_for_testing() - disc.process_model(model) - n = disc.mesh["negative electrode"].npts - - # Solve - scalar input - solver = pybamm.CasadiSolver(sensitivity="explicit forward") + solver = pybamm.CasadiSolver(sensitivity="casadi", atol=1e-10, rtol=1e-10) t_eval = np.linspace(0, 1) solution = solver.solve(model, t_eval, inputs={"param": 7}) - np.testing.assert_array_almost_equal( - solution["prod"].data, - np.tile(2 * 7 * np.exp(-7 * t_eval), (n, 1)), - decimal=4, - ) - np.testing.assert_array_almost_equal( - solution["prod"].sensitivity["param"], - np.repeat(2 * (1 - 7 * t_eval) * np.exp(-7 * t_eval), n)[:, np.newaxis], - decimal=4, - ) + np.testing.assert_array_almost_equal(solution["var"].data, 7 * np.exp(-t_eval)) + + solution = solver.solve(model, t_eval, inputs={"param": 3}) + np.testing.assert_array_almost_equal(solution["var"].data, 3 * np.exp(-t_eval)) + np.testing.assert_array_almost_equal( + solution["var"].sensitivity["param"], np.exp(-t_eval)[:, np.newaxis] + ) + + # def test_least_squares_fit_input_in_initial_conditions(self): + # # Simple system: a single algebraic equation + # var1 = pybamm.Variable("var1", domain="negative electrode") + # var2 = pybamm.Variable("var2", domain="negative electrode") + # model = pybamm.BaseModel() + # p = pybamm.InputParameter("p") + # q = pybamm.InputParameter("q") + # model.rhs = {var1: -var1} + # model.algebraic = {var2: (var2 - p)} + # model.initial_conditions = {var1: 1, var2: p} + # model.variables = {"objective": (var2 - q) ** 2 + (p - 3) ** 2} + + # # create discretisation + # disc = get_discretisation_for_testing() + # disc.process_model(model) + + # # Solve + # solver = pybamm.CasadiSolver() + # solution = solver.solve(model, np.linspace(0, 1)) + # sol_var = solution["objective"] + + # def objective(x): + # return sol_var.value({"p": x[0], "q": x[1]}).full().flatten() + + # # without jacobian + # lsq_sol = least_squares(objective, [2, 2], method="lm") + # np.testing.assert_array_almost_equal(lsq_sol.x, [3, 3], decimal=3) + + +# class TestCasadiSolverODEsWithForwardSensitivityEquations(unittest.TestCase): +# def test_solve_sensitivity_scalar_var_scalar_input(self): +# # Create model +# model = pybamm.BaseModel() +# var = pybamm.Variable("var") +# p = pybamm.InputParameter("p") +# model.rhs = {var: p * var} +# model.initial_conditions = {var: 1} +# model.variables = {"var squared": var ** 2} + +# # Solve +# # Make sure that passing in extra options works +# solver = pybamm.CasadiSolver( +# mode="fast", rtol=1e-10, atol=1e-10, sensitivity="explicit forward" +# ) +# t_eval = np.linspace(0, 1, 80) +# solution = solver.solve(model, t_eval, inputs={"p": 0.1}) +# np.testing.assert_array_equal(solution.t, t_eval) +# np.testing.assert_allclose(solution.y[0], np.exp(0.1 * solution.t)) +# np.testing.assert_allclose( +# solution.sensitivity["p"], +# (solution.t * np.exp(0.1 * solution.t))[:, np.newaxis], +# ) +# np.testing.assert_allclose( +# solution["var squared"].data, np.exp(0.1 * solution.t) ** 2 +# ) +# np.testing.assert_allclose( +# solution["var squared"].sensitivity["p"], +# (2 * np.exp(0.1 * solution.t) * solution.t * np.exp(0.1 * solution.t))[ +# :, np.newaxis +# ], +# ) + +# # More complicated model +# # Create model +# model = pybamm.BaseModel() +# var = pybamm.Variable("var") +# p = pybamm.InputParameter("p") +# q = pybamm.InputParameter("q") +# r = pybamm.InputParameter("r") +# s = pybamm.InputParameter("s") +# model.rhs = {var: p * q} +# model.initial_conditions = {var: r} +# model.variables = {"var times s": var * s} + +# # Solve +# # Make sure that passing in extra options works +# solver = pybamm.CasadiSolver( +# rtol=1e-10, atol=1e-10, sensitivity="explicit forward" +# ) +# t_eval = np.linspace(0, 1, 80) +# solution = solver.solve( +# model, t_eval, inputs={"p": 0.1, "q": 2, "r": -1, "s": 0.5} +# ) +# np.testing.assert_allclose(solution.y[0], -1 + 0.2 * solution.t) +# np.testing.assert_allclose( +# solution.sensitivity["p"], (2 * solution.t)[:, np.newaxis], +# ) +# np.testing.assert_allclose( +# solution.sensitivity["q"], (0.1 * solution.t)[:, np.newaxis], +# ) +# np.testing.assert_allclose(solution.sensitivity["r"], 1) +# np.testing.assert_allclose(solution.sensitivity["s"], 0) +# np.testing.assert_allclose( +# solution.sensitivity["all"], +# np.hstack( +# [ +# solution.sensitivity["p"], +# solution.sensitivity["q"], +# solution.sensitivity["r"], +# solution.sensitivity["s"], +# ] +# ), +# ) +# np.testing.assert_allclose( +# solution["var times s"].data, 0.5 * (-1 + 0.2 * solution.t) +# ) +# np.testing.assert_allclose( +# solution["var times s"].sensitivity["p"], +# 0.5 * (2 * solution.t)[:, np.newaxis], +# ) +# np.testing.assert_allclose( +# solution["var times s"].sensitivity["q"], +# 0.5 * (0.1 * solution.t)[:, np.newaxis], +# ) +# np.testing.assert_allclose(solution["var times s"].sensitivity["r"], 0.5) +# np.testing.assert_allclose( +# solution["var times s"].sensitivity["s"], +# (-1 + 0.2 * solution.t)[:, np.newaxis], +# ) +# np.testing.assert_allclose( +# solution["var times s"].sensitivity["all"], +# np.hstack( +# [ +# solution["var times s"].sensitivity["p"], +# solution["var times s"].sensitivity["q"], +# solution["var times s"].sensitivity["r"], +# solution["var times s"].sensitivity["s"], +# ] +# ), +# ) + +# def test_solve_sensitivity_vector_var_scalar_input(self): +# var = pybamm.Variable("var", "negative electrode") +# model = pybamm.BaseModel() +# # Set length scales to avoid warning +# model.length_scales = {"negative electrode": 1} +# param = pybamm.InputParameter("param") +# model.rhs = {var: -param * var} +# model.initial_conditions = {var: 2} +# model.variables = {"var": var} + +# # create discretisation +# disc = get_discretisation_for_testing() +# disc.process_model(model) +# n = disc.mesh["negative electrode"].npts + +# # Solve - scalar input +# solver = pybamm.CasadiSolver(sensitivity="explicit forward") +# t_eval = np.linspace(0, 1) +# solution = solver.solve(model, t_eval, inputs={"param": 7}) +# np.testing.assert_array_almost_equal( +# solution["var"].data, np.tile(2 * np.exp(-7 * t_eval), (n, 1)), decimal=4, +# ) +# np.testing.assert_array_almost_equal( +# solution["var"].sensitivity["param"], +# np.repeat(-2 * t_eval * np.exp(-7 * t_eval), n)[:, np.newaxis], +# decimal=4, +# ) + +# # More complicated model +# # Create model +# model = pybamm.BaseModel() +# # Set length scales to avoid warning +# model.length_scales = {"negative electrode": 1} +# var = pybamm.Variable("var", "negative electrode") +# p = pybamm.InputParameter("p") +# q = pybamm.InputParameter("q") +# r = pybamm.InputParameter("r") +# s = pybamm.InputParameter("s") +# model.rhs = {var: p * q} +# model.initial_conditions = {var: r} +# model.variables = {"var times s": var * s} + +# # Discretise +# disc.process_model(model) + +# # Solve +# # Make sure that passing in extra options works +# solver = pybamm.CasadiSolver( +# rtol=1e-10, atol=1e-10, sensitivity="explicit forward" +# ) +# t_eval = np.linspace(0, 1, 80) +# solution = solver.solve( +# model, t_eval, inputs={"p": 0.1, "q": 2, "r": -1, "s": 0.5} +# ) +# np.testing.assert_allclose(solution.y, np.tile(-1 + 0.2 * solution.t, (n, 1))) +# np.testing.assert_allclose( +# solution.sensitivity["p"], np.repeat(2 * solution.t, n)[:, np.newaxis], +# ) +# np.testing.assert_allclose( +# solution.sensitivity["q"], np.repeat(0.1 * solution.t, n)[:, np.newaxis], +# ) +# np.testing.assert_allclose(solution.sensitivity["r"], 1) +# np.testing.assert_allclose(solution.sensitivity["s"], 0) +# np.testing.assert_allclose( +# solution.sensitivity["all"], +# np.hstack( +# [ +# solution.sensitivity["p"], +# solution.sensitivity["q"], +# solution.sensitivity["r"], +# solution.sensitivity["s"], +# ] +# ), +# ) +# np.testing.assert_allclose( +# solution["var times s"].data, np.tile(0.5 * (-1 + 0.2 * solution.t), (n, 1)) +# ) +# np.testing.assert_allclose( +# solution["var times s"].sensitivity["p"], +# np.repeat(0.5 * (2 * solution.t), n)[:, np.newaxis], +# ) +# np.testing.assert_allclose( +# solution["var times s"].sensitivity["q"], +# np.repeat(0.5 * (0.1 * solution.t), n)[:, np.newaxis], +# ) +# np.testing.assert_allclose(solution["var times s"].sensitivity["r"], 0.5) +# np.testing.assert_allclose( +# solution["var times s"].sensitivity["s"], +# np.repeat(-1 + 0.2 * solution.t, n)[:, np.newaxis], +# ) +# np.testing.assert_allclose( +# solution["var times s"].sensitivity["all"], +# np.hstack( +# [ +# solution["var times s"].sensitivity["p"], +# solution["var times s"].sensitivity["q"], +# solution["var times s"].sensitivity["r"], +# solution["var times s"].sensitivity["s"], +# ] +# ), +# ) + +# def test_solve_sensitivity_scalar_var_vector_input(self): +# var = pybamm.Variable("var", "negative electrode") +# model = pybamm.BaseModel() +# # Set length scales to avoid warning +# model.length_scales = {"negative electrode": 1} + +# param = pybamm.InputParameter("param", "negative electrode") +# model.rhs = {var: -param * var} +# model.initial_conditions = {var: 2} +# model.variables = { +# "var": var, +# "integral of var": pybamm.Integral(var, pybamm.standard_spatial_vars.x_n), +# } + +# # create discretisation +# mesh = get_mesh_for_testing(xpts=5) +# spatial_methods = {"macroscale": pybamm.FiniteVolume()} +# disc = pybamm.Discretisation(mesh, spatial_methods) +# disc.process_model(model) +# n = disc.mesh["negative electrode"].npts + +# # Solve - constant input +# solver = pybamm.CasadiSolver( +# mode="fast", rtol=1e-10, atol=1e-10, sensitivity="explicit forward" +# ) +# t_eval = np.linspace(0, 1) +# solution = solver.solve(model, t_eval, inputs={"param": 7 * np.ones(n)}) +# l_n = mesh["negative electrode"].edges[-1] +# np.testing.assert_array_almost_equal( +# solution["var"].data, np.tile(2 * np.exp(-7 * t_eval), (n, 1)), decimal=4, +# ) + +# np.testing.assert_array_almost_equal( +# solution["var"].sensitivity["param"], +# np.vstack([np.eye(n) * -2 * t * np.exp(-7 * t) for t in t_eval]), +# ) +# np.testing.assert_array_almost_equal( +# solution["integral of var"].data, 2 * np.exp(-7 * t_eval) * l_n, decimal=4, +# ) +# np.testing.assert_array_almost_equal( +# solution["integral of var"].sensitivity["param"], +# np.tile(-2 * t_eval * np.exp(-7 * t_eval) * l_n / n, (n, 1)).T, +# ) + +# # Solve - linspace input +# p_eval = np.linspace(1, 2, n) +# solution = solver.solve(model, t_eval, inputs={"param": p_eval}) +# l_n = mesh["negative electrode"].edges[-1] +# np.testing.assert_array_almost_equal( +# solution["var"].data, 2 * np.exp(-p_eval[:, np.newaxis] * t_eval), decimal=4 +# ) +# np.testing.assert_array_almost_equal( +# solution["var"].sensitivity["param"], +# np.vstack([np.diag(-2 * t * np.exp(-p_eval * t)) for t in t_eval]), +# ) + +# np.testing.assert_array_almost_equal( +# solution["integral of var"].data, +# np.sum( +# 2 +# * np.exp(-p_eval[:, np.newaxis] * t_eval) +# * mesh["negative electrode"].d_edges[:, np.newaxis], +# axis=0, +# ), +# ) +# np.testing.assert_array_almost_equal( +# solution["integral of var"].sensitivity["param"], +# np.vstack([-2 * t * np.exp(-p_eval * t) * l_n / n for t in t_eval]), +# ) if __name__ == "__main__": From c79f0caa6bfb15a6ad47f9e55cb51caa6f3d46bf Mon Sep 17 00:00:00 2001 From: Valentin Sulzer Date: Thu, 23 Jul 2020 13:29:41 -0400 Subject: [PATCH 10/73] #1100 working on casadi solver sensitivities --- pybamm/solvers/base_solver.py | 18 +- pybamm/solvers/casadi_algebraic_solver.py | 13 +- pybamm/solvers/casadi_solver.py | 16 +- tests/unit/test_solvers/test_casadi_solver.py | 732 +++++++++--------- 4 files changed, 402 insertions(+), 377 deletions(-) diff --git a/pybamm/solvers/base_solver.py b/pybamm/solvers/base_solver.py index 645fc1cb4c..a2aa4b55d5 100644 --- a/pybamm/solvers/base_solver.py +++ b/pybamm/solvers/base_solver.py @@ -510,20 +510,27 @@ def _set_initial_conditions(self, model, inputs, update_rhs): Whether to update the rhs. True for 'solve', False for 'step'. """ + # Make inputs symbolic if calculating sensitivities with casadi + if self.sensitivity == "casadi": + symbolic_inputs = casadi.MX.sym( + "inputs", casadi.vertcat(*inputs.values()).shape[0] + ) + else: + symbolic_inputs = inputs if self.algebraic_solver is True: # Don't update model.y0 return None elif len(model.algebraic) == 0: if update_rhs is True: # Recalculate initial conditions for the rhs equations - model.y0 = model.init_eval(inputs) + y0 = model.init_eval(symbolic_inputs) else: # Don't update model.y0 return None else: if update_rhs is True: # Recalculate initial conditions for the rhs equations - y0_from_inputs = model.init_eval(inputs) + y0_from_inputs = model.init_eval(symbolic_inputs) # Reuse old solution for algebraic equations y0_from_model = model.y0 len_rhs = model.len_rhs @@ -534,7 +541,12 @@ def _set_initial_conditions(self, model, inputs, update_rhs): model.y0 = casadi.vertcat( y0_from_inputs[:len_rhs], y0_from_model[len_rhs:] ) - model.y0 = self.calculate_consistent_state(model, 0, inputs) + y0 = self.calculate_consistent_state(model, 0, inputs) + # Make y0 a function of inputs if doing symbolic with casadi + if self.sensitivity == "casadi": + model.y0 = casadi.Function("y0", [symbolic_inputs], [y0]) + else: + model.y0 = y0 def calculate_consistent_state(self, model, time=0, inputs=None): """ diff --git a/pybamm/solvers/casadi_algebraic_solver.py b/pybamm/solvers/casadi_algebraic_solver.py index 5e48946211..07bd36e6cc 100644 --- a/pybamm/solvers/casadi_algebraic_solver.py +++ b/pybamm/solvers/casadi_algebraic_solver.py @@ -107,10 +107,11 @@ def _integrate(self, model, t_eval, inputs=None): else: # Set up t_sym = casadi.MX.sym("t") + y0_diff_sym = casadi.MX.sym("y0_diff", y0_diff.shape[0]) y_alg_sym = casadi.MX.sym("y_alg", y0_alg.shape[0]) - y_sym = casadi.vertcat(y0_diff, y_alg_sym) + y_sym = casadi.vertcat(y0_diff_sym, y_alg_sym) - t_and_inputs_sym = casadi.vertcat(t_sym, symbolic_inputs) + t_y0diff_inputs_sym = casadi.vertcat(t_sym, y0_diff_sym, symbolic_inputs) alg = model.casadi_algebraic(t_sym, y_sym, symbolic_inputs) # Set constraints vector in the casadi format @@ -126,7 +127,7 @@ def _integrate(self, model, t_eval, inputs=None): roots = casadi.rootfinder( "roots", "newton", - dict(x=y_alg_sym, p=t_and_inputs_sym, g=alg), + dict(x=y_alg_sym, p=t_y0diff_inputs_sym, g=alg), { **self.extra_options, "abstol": self.tol, @@ -155,12 +156,12 @@ def _integrate(self, model, t_eval, inputs=None): # If doing sensitivity with casadi, evaluate with symbolic inputs # Otherwise, evaluate with actual inputs if self.sensitivity == "casadi": - t_eval_and_inputs = casadi.vertcat(t, symbolic_inputs) + t_y0_diff_inputs = casadi.vertcat(t, y0_diff, symbolic_inputs) else: - t_eval_and_inputs = casadi.vertcat(t, inputs) + t_y0_diff_inputs = casadi.vertcat(t, y0_diff, inputs) # Solve try: - y_alg_sol = roots(y0_alg, t_eval_and_inputs) + y_alg_sol = roots(y0_alg, t_y0_diff_inputs) success = True message = None # Check final output diff --git a/pybamm/solvers/casadi_solver.py b/pybamm/solvers/casadi_solver.py index 70b7c0e7b2..c202549bc8 100644 --- a/pybamm/solvers/casadi_solver.py +++ b/pybamm/solvers/casadi_solver.py @@ -208,7 +208,7 @@ def _integrate(self, model, t_eval, inputs=None): if self.mode == "safe": # update integrator with the grid - self.create_integrator(model, inputs, t_window) + self.create_integrator(model, inputs_dict, t_window) # Try to solve with the current global step, if it fails then # halve the step size and try again. try: @@ -347,7 +347,6 @@ def create_integrator(self, model, inputs_dict, t_eval=None): self.integrators[model] = (integrator, use_grid) return integrator else: - y0 = model.y0 rhs = model.casadi_rhs algebraic = model.casadi_algebraic @@ -370,6 +369,12 @@ def create_integrator(self, model, inputs_dict, t_eval=None): # set up and solve t = casadi.MX.sym("t") p = casadi.MX.sym("p", inputs.shape[0]) + # If the initial conditions depend on inputs, evaluate the function + if isinstance(model.y0, casadi.Function): + y0 = model.y0(p) + else: + y0 = model.y0 + y_diff = casadi.MX.sym("y_diff", rhs(0, y0, p).shape[0]) if use_grid is False: @@ -420,6 +425,13 @@ def _run_integrator(self, model, y0, inputs_dict, t_eval): else: inputs_eval = inputs integrator, use_grid = self.integrators[model] + + # If the initial conditions depend on inputs, evaluate the function + if isinstance(y0, casadi.Function): + y0 = y0(symbolic_inputs) + else: + y0 = y0 + # Split up initial conditions into differential and algebraic # Check y0 to see if it includes sensitivities if model.len_rhs_and_alg == y0.shape[0]: diff --git a/tests/unit/test_solvers/test_casadi_solver.py b/tests/unit/test_solvers/test_casadi_solver.py index aab9a346e9..e948a788aa 100644 --- a/tests/unit/test_solvers/test_casadi_solver.py +++ b/tests/unit/test_solvers/test_casadi_solver.py @@ -9,408 +9,408 @@ from scipy.optimize import least_squares -# class TestCasadiSolver(unittest.TestCase): -# def test_bad_mode(self): -# with self.assertRaisesRegex(ValueError, "invalid mode"): -# pybamm.CasadiSolver(mode="bad mode") +class TestCasadiSolver(unittest.TestCase): + def test_bad_mode(self): + with self.assertRaisesRegex(ValueError, "invalid mode"): + pybamm.CasadiSolver(mode="bad mode") -# def test_model_solver(self): -# # Create model -# model = pybamm.BaseModel() -# var = pybamm.Variable("var") -# model.rhs = {var: 0.1 * var} -# model.initial_conditions = {var: 1} -# # No need to set parameters; can use base discretisation (no spatial operators) - -# # create discretisation -# disc = pybamm.Discretisation() -# model_disc = disc.process_model(model, inplace=False) -# # Solve -# solver = pybamm.CasadiSolver(mode="fast", rtol=1e-8, atol=1e-8) -# t_eval = np.linspace(0, 1, 100) -# solution = solver.solve(model_disc, t_eval) -# np.testing.assert_array_equal(solution.t, t_eval) -# np.testing.assert_array_almost_equal( -# solution.y[0], np.exp(0.1 * solution.t), decimal=5 -# ) - -# # Safe mode (enforce events that won't be triggered) -# model.events = [pybamm.Event("an event", var + 1)] -# disc.process_model(model) -# solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) -# solution = solver.solve(model, t_eval) -# np.testing.assert_array_equal(solution.t, t_eval) -# np.testing.assert_array_almost_equal( -# solution.y[0], np.exp(0.1 * solution.t), decimal=5 -# ) + def test_model_solver(self): + # Create model + model = pybamm.BaseModel() + var = pybamm.Variable("var") + model.rhs = {var: 0.1 * var} + model.initial_conditions = {var: 1} + # No need to set parameters; can use base discretisation (no spatial operators) -# # Safe mode, without grid (enforce events that won't be triggered) -# solver = pybamm.CasadiSolver(mode="safe without grid", rtol=1e-8, atol=1e-8) -# solution = solver.solve(model, t_eval) -# np.testing.assert_array_equal(solution.t, t_eval) -# np.testing.assert_array_almost_equal( -# solution.y[0], np.exp(0.1 * solution.t), decimal=5 -# ) + # create discretisation + disc = pybamm.Discretisation() + model_disc = disc.process_model(model, inplace=False) + # Solve + solver = pybamm.CasadiSolver(mode="fast", rtol=1e-8, atol=1e-8) + t_eval = np.linspace(0, 1, 100) + solution = solver.solve(model_disc, t_eval) + np.testing.assert_array_equal(solution.t, t_eval) + np.testing.assert_array_almost_equal( + solution.y[0], np.exp(0.1 * solution.t), decimal=5 + ) -# def test_model_solver_python(self): -# # Create model -# pybamm.set_logging_level("ERROR") -# model = pybamm.BaseModel() -# model.convert_to_format = "python" -# var = pybamm.Variable("var") -# model.rhs = {var: 0.1 * var} -# model.initial_conditions = {var: 1} -# # No need to set parameters; can use base discretisation (no spatial operators) + # Safe mode (enforce events that won't be triggered) + model.events = [pybamm.Event("an event", var + 1)] + disc.process_model(model) + solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) + solution = solver.solve(model, t_eval) + np.testing.assert_array_equal(solution.t, t_eval) + np.testing.assert_array_almost_equal( + solution.y[0], np.exp(0.1 * solution.t), decimal=5 + ) -# # create discretisation -# disc = pybamm.Discretisation() -# disc.process_model(model) -# # Solve -# solver = pybamm.CasadiSolver(mode="fast", rtol=1e-8, atol=1e-8) -# t_eval = np.linspace(0, 1, 100) -# solution = solver.solve(model, t_eval) -# np.testing.assert_array_equal(solution.t, t_eval) -# np.testing.assert_array_almost_equal( -# solution.y[0], np.exp(0.1 * solution.t), decimal=5 -# ) -# pybamm.set_logging_level("WARNING") + # Safe mode, without grid (enforce events that won't be triggered) + solver = pybamm.CasadiSolver(mode="safe without grid", rtol=1e-8, atol=1e-8) + solution = solver.solve(model, t_eval) + np.testing.assert_array_equal(solution.t, t_eval) + np.testing.assert_array_almost_equal( + solution.y[0], np.exp(0.1 * solution.t), decimal=5 + ) -# def test_model_solver_failure(self): -# # Create model -# model = pybamm.BaseModel() -# var = pybamm.Variable("var") -# model.rhs = {var: -pybamm.sqrt(var)} -# model.initial_conditions = {var: 1} -# # add events so that safe mode is used (won't be triggered) -# model.events = [pybamm.Event("10", var - 10)] -# # No need to set parameters; can use base discretisation (no spatial operators) + def test_model_solver_python(self): + # Create model + pybamm.set_logging_level("ERROR") + model = pybamm.BaseModel() + model.convert_to_format = "python" + var = pybamm.Variable("var") + model.rhs = {var: 0.1 * var} + model.initial_conditions = {var: 1} + # No need to set parameters; can use base discretisation (no spatial operators) -# # create discretisation -# disc = pybamm.Discretisation() -# model_disc = disc.process_model(model, inplace=False) - -# solver = pybamm.CasadiSolver(extra_options_call={"regularity_check": False}) -# # Solve with failure at t=2 -# t_eval = np.linspace(0, 20, 100) -# with self.assertRaises(pybamm.SolverError): -# solver.solve(model_disc, t_eval) -# # Solve with failure at t=0 -# model.initial_conditions = {var: 0} -# model_disc = disc.process_model(model, inplace=False) -# t_eval = np.linspace(0, 20, 100) -# with self.assertRaises(pybamm.SolverError): -# solver.solve(model_disc, t_eval) - -# def test_model_solver_events(self): -# # Create model -# model = pybamm.BaseModel() -# whole_cell = ["negative electrode", "separator", "positive electrode"] -# var1 = pybamm.Variable("var1", domain=whole_cell) -# var2 = pybamm.Variable("var2", domain=whole_cell) -# model.rhs = {var1: 0.1 * var1} -# model.algebraic = {var2: 2 * var1 - var2} -# model.initial_conditions = {var1: 1, var2: 2} -# model.events = [ -# pybamm.Event("var1 = 1.5", pybamm.min(var1 - 1.5)), -# pybamm.Event("var2 = 2.5", pybamm.min(var2 - 2.5)), -# ] -# disc = get_discretisation_for_testing() -# disc.process_model(model) + # create discretisation + disc = pybamm.Discretisation() + disc.process_model(model) + # Solve + solver = pybamm.CasadiSolver(mode="fast", rtol=1e-8, atol=1e-8) + t_eval = np.linspace(0, 1, 100) + solution = solver.solve(model, t_eval) + np.testing.assert_array_equal(solution.t, t_eval) + np.testing.assert_array_almost_equal( + solution.y[0], np.exp(0.1 * solution.t), decimal=5 + ) + pybamm.set_logging_level("WARNING") -# # Solve using "safe" mode -# solver = pybamm.CasadiSolver(mode="safe", rtol=1e-8, atol=1e-8) -# t_eval = np.linspace(0, 5, 100) -# solution = solver.solve(model, t_eval) -# np.testing.assert_array_less(solution.y[0], 1.5) -# np.testing.assert_array_less(solution.y[-1], 2.5 + 1e-10) -# np.testing.assert_array_almost_equal( -# solution.y[0], np.exp(0.1 * solution.t), decimal=5 -# ) -# np.testing.assert_array_almost_equal( -# solution.y[-1], 2 * np.exp(0.1 * solution.t), decimal=5 -# ) - -# # Solve using "safe" mode with debug off -# pybamm.settings.debug_mode = False -# solver = pybamm.CasadiSolver(mode="safe", rtol=1e-8, atol=1e-8, dt_max=1) -# t_eval = np.linspace(0, 5, 100) -# solution = solver.solve(model, t_eval) -# np.testing.assert_array_less(solution.y[0], 1.5) -# np.testing.assert_array_less(solution.y[-1], 2.5 + 1e-10) -# # test the last entry is exactly 2.5 -# np.testing.assert_array_almost_equal(solution.y[-1, -1], 2.5, decimal=2) -# np.testing.assert_array_almost_equal( -# solution.y[0], np.exp(0.1 * solution.t), decimal=5 -# ) -# np.testing.assert_array_almost_equal( -# solution.y[-1], 2 * np.exp(0.1 * solution.t), decimal=5 -# ) -# pybamm.settings.debug_mode = True + def test_model_solver_failure(self): + # Create model + model = pybamm.BaseModel() + var = pybamm.Variable("var") + model.rhs = {var: -pybamm.sqrt(var)} + model.initial_conditions = {var: 1} + # add events so that safe mode is used (won't be triggered) + model.events = [pybamm.Event("10", var - 10)] + # No need to set parameters; can use base discretisation (no spatial operators) -# # Try dt_max=0 to enforce using all timesteps -# solver = pybamm.CasadiSolver(dt_max=0, rtol=1e-8, atol=1e-8) -# t_eval = np.linspace(0, 5, 100) -# solution = solver.solve(model, t_eval) -# np.testing.assert_array_less(solution.y[0], 1.5) -# np.testing.assert_array_less(solution.y[-1], 2.5) -# np.testing.assert_array_almost_equal( -# solution.y[0], np.exp(0.1 * solution.t), decimal=5 -# ) -# np.testing.assert_array_almost_equal( -# solution.y[-1], 2 * np.exp(0.1 * solution.t), decimal=5 -# ) + # create discretisation + disc = pybamm.Discretisation() + model_disc = disc.process_model(model, inplace=False) + + solver = pybamm.CasadiSolver(extra_options_call={"regularity_check": False}) + # Solve with failure at t=2 + t_eval = np.linspace(0, 20, 100) + with self.assertRaises(pybamm.SolverError): + solver.solve(model_disc, t_eval) + # Solve with failure at t=0 + model.initial_conditions = {var: 0} + model_disc = disc.process_model(model, inplace=False) + t_eval = np.linspace(0, 20, 100) + with self.assertRaises(pybamm.SolverError): + solver.solve(model_disc, t_eval) + + def test_model_solver_events(self): + # Create model + model = pybamm.BaseModel() + whole_cell = ["negative electrode", "separator", "positive electrode"] + var1 = pybamm.Variable("var1", domain=whole_cell) + var2 = pybamm.Variable("var2", domain=whole_cell) + model.rhs = {var1: 0.1 * var1} + model.algebraic = {var2: 2 * var1 - var2} + model.initial_conditions = {var1: 1, var2: 2} + model.events = [ + pybamm.Event("var1 = 1.5", pybamm.min(var1 - 1.5)), + pybamm.Event("var2 = 2.5", pybamm.min(var2 - 2.5)), + ] + disc = get_discretisation_for_testing() + disc.process_model(model) -# # Test when an event returns nan -# model = pybamm.BaseModel() -# var = pybamm.Variable("var") -# model.rhs = {var: 0.1 * var} -# model.initial_conditions = {var: 1} -# model.events = [ -# pybamm.Event("event", var - 1.02), -# pybamm.Event("sqrt event", pybamm.sqrt(1.0199 - var)), -# ] -# disc = pybamm.Discretisation() -# disc.process_model(model) -# solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) -# solution = solver.solve(model, t_eval) -# np.testing.assert_array_less(solution.y[0], 1.02 + 1e-10) -# np.testing.assert_array_almost_equal(solution.y[0, -1], 1.02, decimal=2) + # Solve using "safe" mode + solver = pybamm.CasadiSolver(mode="safe", rtol=1e-8, atol=1e-8) + t_eval = np.linspace(0, 5, 100) + solution = solver.solve(model, t_eval) + np.testing.assert_array_less(solution.y[0], 1.5) + np.testing.assert_array_less(solution.y[-1], 2.5 + 1e-10) + np.testing.assert_array_almost_equal( + solution.y[0], np.exp(0.1 * solution.t), decimal=5 + ) + np.testing.assert_array_almost_equal( + solution.y[-1], 2 * np.exp(0.1 * solution.t), decimal=5 + ) -# def test_model_step(self): -# # Create model -# model = pybamm.BaseModel() -# domain = ["negative electrode", "separator", "positive electrode"] -# var = pybamm.Variable("var", domain=domain) -# model.rhs = {var: 0.1 * var} -# model.initial_conditions = {var: 1} -# # No need to set parameters; can use base discretisation (no spatial operators) + # Solve using "safe" mode with debug off + pybamm.settings.debug_mode = False + solver = pybamm.CasadiSolver(mode="safe", rtol=1e-8, atol=1e-8, dt_max=1) + t_eval = np.linspace(0, 5, 100) + solution = solver.solve(model, t_eval) + np.testing.assert_array_less(solution.y[0], 1.5) + np.testing.assert_array_less(solution.y[-1], 2.5 + 1e-10) + # test the last entry is exactly 2.5 + np.testing.assert_array_almost_equal(solution.y[-1, -1], 2.5, decimal=2) + np.testing.assert_array_almost_equal( + solution.y[0], np.exp(0.1 * solution.t), decimal=5 + ) + np.testing.assert_array_almost_equal( + solution.y[-1], 2 * np.exp(0.1 * solution.t), decimal=5 + ) + pybamm.settings.debug_mode = True + + # Try dt_max=0 to enforce using all timesteps + solver = pybamm.CasadiSolver(dt_max=0, rtol=1e-8, atol=1e-8) + t_eval = np.linspace(0, 5, 100) + solution = solver.solve(model, t_eval) + np.testing.assert_array_less(solution.y[0], 1.5) + np.testing.assert_array_less(solution.y[-1], 2.5) + np.testing.assert_array_almost_equal( + solution.y[0], np.exp(0.1 * solution.t), decimal=5 + ) + np.testing.assert_array_almost_equal( + solution.y[-1], 2 * np.exp(0.1 * solution.t), decimal=5 + ) -# # create discretisation -# mesh = get_mesh_for_testing() -# spatial_methods = {"macroscale": pybamm.FiniteVolume()} -# disc = pybamm.Discretisation(mesh, spatial_methods) -# disc.process_model(model) + # Test when an event returns nan + model = pybamm.BaseModel() + var = pybamm.Variable("var") + model.rhs = {var: 0.1 * var} + model.initial_conditions = {var: 1} + model.events = [ + pybamm.Event("event", var - 1.02), + pybamm.Event("sqrt event", pybamm.sqrt(1.0199 - var)), + ] + disc = pybamm.Discretisation() + disc.process_model(model) + solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) + solution = solver.solve(model, t_eval) + np.testing.assert_array_less(solution.y[0], 1.02 + 1e-10) + np.testing.assert_array_almost_equal(solution.y[0, -1], 1.02, decimal=2) -# solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) + def test_model_step(self): + # Create model + model = pybamm.BaseModel() + domain = ["negative electrode", "separator", "positive electrode"] + var = pybamm.Variable("var", domain=domain) + model.rhs = {var: 0.1 * var} + model.initial_conditions = {var: 1} + # No need to set parameters; can use base discretisation (no spatial operators) -# # Step once -# dt = 1 -# step_sol = solver.step(None, model, dt) -# np.testing.assert_array_equal(step_sol.t, [0, dt]) -# np.testing.assert_array_almost_equal(step_sol.y[0], np.exp(0.1 * step_sol.t)) + # create discretisation + mesh = get_mesh_for_testing() + spatial_methods = {"macroscale": pybamm.FiniteVolume()} + disc = pybamm.Discretisation(mesh, spatial_methods) + disc.process_model(model) -# # Step again (return 5 points) -# step_sol_2 = solver.step(step_sol, model, dt, npts=5) -# np.testing.assert_array_equal( -# step_sol_2.t, np.concatenate([np.array([0]), np.linspace(dt, 2 * dt, 5)]) -# ) -# np.testing.assert_array_almost_equal( -# step_sol_2.y[0], np.exp(0.1 * step_sol_2.t) -# ) + solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) -# # Check steps give same solution as solve -# t_eval = step_sol.t -# solution = solver.solve(model, t_eval) -# np.testing.assert_array_almost_equal(solution.y[0], step_sol.y[0]) + # Step once + dt = 1 + step_sol = solver.step(None, model, dt) + np.testing.assert_array_equal(step_sol.t, [0, dt]) + np.testing.assert_array_almost_equal(step_sol.y[0], np.exp(0.1 * step_sol.t)) -# def test_model_step_with_input(self): -# # Create model -# model = pybamm.BaseModel() -# var = pybamm.Variable("var") -# a = pybamm.InputParameter("a") -# model.rhs = {var: a * var} -# model.initial_conditions = {var: 1} -# model.variables = {"a": a} -# # No need to set parameters; can use base discretisation (no spatial operators) + # Step again (return 5 points) + step_sol_2 = solver.step(step_sol, model, dt, npts=5) + np.testing.assert_array_equal( + step_sol_2.t, np.concatenate([np.array([0]), np.linspace(dt, 2 * dt, 5)]) + ) + np.testing.assert_array_almost_equal( + step_sol_2.y[0], np.exp(0.1 * step_sol_2.t) + ) -# # create discretisation -# disc = pybamm.Discretisation() -# disc.process_model(model) + # Check steps give same solution as solve + t_eval = step_sol.t + solution = solver.solve(model, t_eval) + np.testing.assert_array_almost_equal(solution.y[0], step_sol.y[0]) -# solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) + def test_model_step_with_input(self): + # Create model + model = pybamm.BaseModel() + var = pybamm.Variable("var") + a = pybamm.InputParameter("a") + model.rhs = {var: a * var} + model.initial_conditions = {var: 1} + model.variables = {"a": a} + # No need to set parameters; can use base discretisation (no spatial operators) -# # Step with an input -# dt = 0.1 -# step_sol = solver.step(None, model, dt, npts=5, inputs={"a": 0.1}) -# np.testing.assert_array_equal(step_sol.t, np.linspace(0, dt, 5)) -# np.testing.assert_allclose(step_sol.y[0], np.exp(0.1 * step_sol.t)) + # create discretisation + disc = pybamm.Discretisation() + disc.process_model(model) -# # Step again with different inputs -# step_sol_2 = solver.step(step_sol, model, dt, npts=5, inputs={"a": -1}) -# np.testing.assert_array_equal(step_sol_2.t, np.linspace(0, 2 * dt, 9)) -# np.testing.assert_array_equal( -# step_sol_2["a"].entries, np.array([0.1, 0.1, 0.1, 0.1, 0.1, -1, -1, -1, -1]) -# ) -# np.testing.assert_allclose( -# step_sol_2.y[0], -# np.concatenate( -# [ -# np.exp(0.1 * step_sol.t[:5]), -# np.exp(0.1 * step_sol.t[4]) * np.exp(-(step_sol.t[5:] - dt)), -# ] -# ), -# ) + solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) -# def test_model_step_events(self): -# # Create model -# model = pybamm.BaseModel() -# var1 = pybamm.Variable("var1") -# var2 = pybamm.Variable("var2") -# model.rhs = {var1: 0.1 * var1} -# model.algebraic = {var2: 2 * var1 - var2} -# model.initial_conditions = {var1: 1, var2: 2} -# model.events = [ -# pybamm.Event("var1 = 1.5", pybamm.min(var1 - 1.5)), -# pybamm.Event("var2 = 2.5", pybamm.min(var2 - 2.5)), -# ] -# disc = pybamm.Discretisation() -# disc.process_model(model) + # Step with an input + dt = 0.1 + step_sol = solver.step(None, model, dt, npts=5, inputs={"a": 0.1}) + np.testing.assert_array_equal(step_sol.t, np.linspace(0, dt, 5)) + np.testing.assert_allclose(step_sol.y[0], np.exp(0.1 * step_sol.t)) -# # Solve -# step_solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) -# dt = 0.05 -# time = 0 -# end_time = 5 -# step_solution = None -# while time < end_time: -# step_solution = step_solver.step(step_solution, model, dt=dt, npts=10) -# time += dt -# np.testing.assert_array_less(step_solution.y[0], 1.5) -# np.testing.assert_array_less(step_solution.y[-1], 2.5001) -# np.testing.assert_array_almost_equal( -# step_solution.y[0], np.exp(0.1 * step_solution.t), decimal=5 -# ) -# np.testing.assert_array_almost_equal( -# step_solution.y[-1], 2 * np.exp(0.1 * step_solution.t), decimal=4 -# ) + # Step again with different inputs + step_sol_2 = solver.step(step_sol, model, dt, npts=5, inputs={"a": -1}) + np.testing.assert_array_equal(step_sol_2.t, np.linspace(0, 2 * dt, 9)) + np.testing.assert_array_equal( + step_sol_2["a"].entries, np.array([0.1, 0.1, 0.1, 0.1, 0.1, -1, -1, -1, -1]) + ) + np.testing.assert_allclose( + step_sol_2.y[0], + np.concatenate( + [ + np.exp(0.1 * step_sol.t[:5]), + np.exp(0.1 * step_sol.t[4]) * np.exp(-(step_sol.t[5:] - dt)), + ] + ), + ) -# def test_model_solver_with_inputs(self): -# # Create model -# model = pybamm.BaseModel() -# domain = ["negative electrode", "separator", "positive electrode"] -# var = pybamm.Variable("var", domain=domain) -# model.rhs = {var: -pybamm.InputParameter("rate") * var} -# model.initial_conditions = {var: 1} -# model.events = [pybamm.Event("var=0.5", pybamm.min(var - 0.5))] -# # No need to set parameters; can use base discretisation (no spatial -# # operators) + def test_model_step_events(self): + # Create model + model = pybamm.BaseModel() + var1 = pybamm.Variable("var1") + var2 = pybamm.Variable("var2") + model.rhs = {var1: 0.1 * var1} + model.algebraic = {var2: 2 * var1 - var2} + model.initial_conditions = {var1: 1, var2: 2} + model.events = [ + pybamm.Event("var1 = 1.5", pybamm.min(var1 - 1.5)), + pybamm.Event("var2 = 2.5", pybamm.min(var2 - 2.5)), + ] + disc = pybamm.Discretisation() + disc.process_model(model) -# # create discretisation -# mesh = get_mesh_for_testing() -# spatial_methods = {"macroscale": pybamm.FiniteVolume()} -# disc = pybamm.Discretisation(mesh, spatial_methods) -# disc.process_model(model) -# # Solve -# solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) -# t_eval = np.linspace(0, 10, 100) -# solution = solver.solve(model, t_eval, inputs={"rate": 0.1}) -# self.assertLess(len(solution.t), len(t_eval)) -# np.testing.assert_allclose(solution.y[0], np.exp(-0.1 * solution.t), rtol=1e-04) + # Solve + step_solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) + dt = 0.05 + time = 0 + end_time = 5 + step_solution = None + while time < end_time: + step_solution = step_solver.step(step_solution, model, dt=dt, npts=10) + time += dt + np.testing.assert_array_less(step_solution.y[0], 1.5) + np.testing.assert_array_less(step_solution.y[-1], 2.5001) + np.testing.assert_array_almost_equal( + step_solution.y[0], np.exp(0.1 * step_solution.t), decimal=5 + ) + np.testing.assert_array_almost_equal( + step_solution.y[-1], 2 * np.exp(0.1 * step_solution.t), decimal=4 + ) -# def test_model_solver_dae_inputs_in_initial_conditions(self): -# # Create model -# model = pybamm.BaseModel() -# var1 = pybamm.Variable("var1") -# var2 = pybamm.Variable("var2") -# model.rhs = {var1: pybamm.InputParameter("rate") * var1} -# model.algebraic = {var2: var1 - var2} -# model.initial_conditions = { -# var1: pybamm.InputParameter("ic 1"), -# var2: pybamm.InputParameter("ic 2"), -# } + def test_model_solver_with_inputs(self): + # Create model + model = pybamm.BaseModel() + domain = ["negative electrode", "separator", "positive electrode"] + var = pybamm.Variable("var", domain=domain) + model.rhs = {var: -pybamm.InputParameter("rate") * var} + model.initial_conditions = {var: 1} + model.events = [pybamm.Event("var=0.5", pybamm.min(var - 0.5))] + # No need to set parameters; can use base discretisation (no spatial + # operators) -# # Solve -# solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) -# t_eval = np.linspace(0, 5, 100) -# solution = solver.solve( -# model, t_eval, inputs={"rate": -1, "ic 1": 0.1, "ic 2": 2} -# ) -# np.testing.assert_array_almost_equal( -# solution.y[0], 0.1 * np.exp(-solution.t), decimal=5 -# ) -# np.testing.assert_array_almost_equal( -# solution.y[-1], 0.1 * np.exp(-solution.t), decimal=5 -# ) + # create discretisation + mesh = get_mesh_for_testing() + spatial_methods = {"macroscale": pybamm.FiniteVolume()} + disc = pybamm.Discretisation(mesh, spatial_methods) + disc.process_model(model) + # Solve + solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) + t_eval = np.linspace(0, 10, 100) + solution = solver.solve(model, t_eval, inputs={"rate": 0.1}) + self.assertLess(len(solution.t), len(t_eval)) + np.testing.assert_allclose(solution.y[0], np.exp(-0.1 * solution.t), rtol=1e-04) + + def test_model_solver_dae_inputs_in_initial_conditions(self): + # Create model + model = pybamm.BaseModel() + var1 = pybamm.Variable("var1") + var2 = pybamm.Variable("var2") + model.rhs = {var1: pybamm.InputParameter("rate") * var1} + model.algebraic = {var2: var1 - var2} + model.initial_conditions = { + var1: pybamm.InputParameter("ic 1"), + var2: pybamm.InputParameter("ic 2"), + } -# # Solve again with different initial conditions -# solution = solver.solve( -# model, t_eval, inputs={"rate": -0.1, "ic 1": 1, "ic 2": 3} -# ) -# np.testing.assert_array_almost_equal( -# solution.y[0], 1 * np.exp(-0.1 * solution.t), decimal=5 -# ) -# np.testing.assert_array_almost_equal( -# solution.y[-1], 1 * np.exp(-0.1 * solution.t), decimal=5 -# ) + # Solve + solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) + t_eval = np.linspace(0, 5, 100) + solution = solver.solve( + model, t_eval, inputs={"rate": -1, "ic 1": 0.1, "ic 2": 2} + ) + np.testing.assert_array_almost_equal( + solution.y[0], 0.1 * np.exp(-solution.t), decimal=5 + ) + np.testing.assert_array_almost_equal( + solution.y[-1], 0.1 * np.exp(-solution.t), decimal=5 + ) -# def test_model_solver_with_external(self): -# # Create model -# model = pybamm.BaseModel() -# domain = ["negative electrode", "separator", "positive electrode"] -# var1 = pybamm.Variable("var1", domain=domain) -# var2 = pybamm.Variable("var2", domain=domain) -# model.rhs = {var1: -var2} -# model.initial_conditions = {var1: 1} -# model.external_variables = [var2] -# model.variables = {"var1": var1, "var2": var2} -# # No need to set parameters; can use base discretisation (no spatial -# # operators) + # Solve again with different initial conditions + solution = solver.solve( + model, t_eval, inputs={"rate": -0.1, "ic 1": 1, "ic 2": 3} + ) + np.testing.assert_array_almost_equal( + solution.y[0], 1 * np.exp(-0.1 * solution.t), decimal=5 + ) + np.testing.assert_array_almost_equal( + solution.y[-1], 1 * np.exp(-0.1 * solution.t), decimal=5 + ) -# # create discretisation -# mesh = get_mesh_for_testing() -# spatial_methods = {"macroscale": pybamm.FiniteVolume()} -# disc = pybamm.Discretisation(mesh, spatial_methods) -# disc.process_model(model) -# # Solve -# solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) -# t_eval = np.linspace(0, 10, 100) -# solution = solver.solve(model, t_eval, external_variables={"var2": 0.5}) -# np.testing.assert_allclose(solution.y[0], 1 - 0.5 * solution.t, rtol=1e-06) + def test_model_solver_with_external(self): + # Create model + model = pybamm.BaseModel() + domain = ["negative electrode", "separator", "positive electrode"] + var1 = pybamm.Variable("var1", domain=domain) + var2 = pybamm.Variable("var2", domain=domain) + model.rhs = {var1: -var2} + model.initial_conditions = {var1: 1} + model.external_variables = [var2] + model.variables = {"var1": var1, "var2": var2} + # No need to set parameters; can use base discretisation (no spatial + # operators) -# def test_model_solver_with_non_identity_mass(self): -# model = pybamm.BaseModel() -# var1 = pybamm.Variable("var1", domain="negative electrode") -# var2 = pybamm.Variable("var2", domain="negative electrode") -# model.rhs = {var1: var1} -# model.algebraic = {var2: 2 * var1 - var2} -# model.initial_conditions = {var1: 1, var2: 2} -# disc = get_discretisation_for_testing() -# disc.process_model(model) + # create discretisation + mesh = get_mesh_for_testing() + spatial_methods = {"macroscale": pybamm.FiniteVolume()} + disc = pybamm.Discretisation(mesh, spatial_methods) + disc.process_model(model) + # Solve + solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) + t_eval = np.linspace(0, 10, 100) + solution = solver.solve(model, t_eval, external_variables={"var2": 0.5}) + np.testing.assert_allclose(solution.y[0], 1 - 0.5 * solution.t, rtol=1e-06) -# # FV discretisation has identity mass. Manually set the mass matrix to -# # be a diag of 10s here for testing. Note that the algebraic part is all -# # zeros -# mass_matrix = 10 * model.mass_matrix.entries -# model.mass_matrix = pybamm.Matrix(mass_matrix) + def test_model_solver_with_non_identity_mass(self): + model = pybamm.BaseModel() + var1 = pybamm.Variable("var1", domain="negative electrode") + var2 = pybamm.Variable("var2", domain="negative electrode") + model.rhs = {var1: var1} + model.algebraic = {var2: 2 * var1 - var2} + model.initial_conditions = {var1: 1, var2: 2} + disc = get_discretisation_for_testing() + disc.process_model(model) -# # Note that mass_matrix_inv is just the inverse of the ode block of the -# # mass matrix -# mass_matrix_inv = 0.1 * eye(int(mass_matrix.shape[0] / 2)) -# model.mass_matrix_inv = pybamm.Matrix(mass_matrix_inv) + # FV discretisation has identity mass. Manually set the mass matrix to + # be a diag of 10s here for testing. Note that the algebraic part is all + # zeros + mass_matrix = 10 * model.mass_matrix.entries + model.mass_matrix = pybamm.Matrix(mass_matrix) -# # Solve -# solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) -# t_eval = np.linspace(0, 1, 100) -# solution = solver.solve(model, t_eval) -# np.testing.assert_array_equal(solution.t, t_eval) -# np.testing.assert_allclose(solution.y[0], np.exp(0.1 * solution.t)) -# np.testing.assert_allclose(solution.y[-1], 2 * np.exp(0.1 * solution.t)) + # Note that mass_matrix_inv is just the inverse of the ode block of the + # mass matrix + mass_matrix_inv = 0.1 * eye(int(mass_matrix.shape[0] / 2)) + model.mass_matrix_inv = pybamm.Matrix(mass_matrix_inv) -# def test_dae_solver_algebraic_model(self): -# model = pybamm.BaseModel() -# var = pybamm.Variable("var") -# model.algebraic = {var: var + 1} -# model.initial_conditions = {var: 0} + # Solve + solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) + t_eval = np.linspace(0, 1, 100) + solution = solver.solve(model, t_eval) + np.testing.assert_array_equal(solution.t, t_eval) + np.testing.assert_allclose(solution.y[0], np.exp(0.1 * solution.t)) + np.testing.assert_allclose(solution.y[-1], 2 * np.exp(0.1 * solution.t)) + + def test_dae_solver_algebraic_model(self): + model = pybamm.BaseModel() + var = pybamm.Variable("var") + model.algebraic = {var: var + 1} + model.initial_conditions = {var: 0} -# disc = pybamm.Discretisation() -# disc.process_model(model) + disc = pybamm.Discretisation() + disc.process_model(model) -# solver = pybamm.CasadiSolver() -# t_eval = np.linspace(0, 1) -# with self.assertRaisesRegex( -# pybamm.SolverError, "Cannot use CasadiSolver to solve algebraic model" -# ): -# solver.solve(model, t_eval) + solver = pybamm.CasadiSolver() + t_eval = np.linspace(0, 1) + with self.assertRaisesRegex( + pybamm.SolverError, "Cannot use CasadiSolver to solve algebraic model" + ): + solver.solve(model, t_eval) class TestCasadiSolverSensitivity(unittest.TestCase): From 067c5043db51c3da33e6aa7a1ad5454a3981be71 Mon Sep 17 00:00:00 2001 From: Valentin Sulzer Date: Thu, 23 Jul 2020 14:27:52 -0400 Subject: [PATCH 11/73] #1100 explicit foward sensitivity working --- examples/notebooks/DFN-sensitivity.ipynb | 54 +- pybamm/solvers/base_solver.py | 15 +- tests/unit/test_solvers/test_casadi_solver.py | 546 +++++++++--------- 3 files changed, 328 insertions(+), 287 deletions(-) diff --git a/examples/notebooks/DFN-sensitivity.ipynb b/examples/notebooks/DFN-sensitivity.ipynb index 45c92bac85..47fb8e597d 100644 --- a/examples/notebooks/DFN-sensitivity.ipynb +++ b/examples/notebooks/DFN-sensitivity.ipynb @@ -42,7 +42,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -64,7 +64,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ @@ -85,7 +85,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ @@ -106,27 +106,27 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 6, "metadata": {}, "outputs": [], "source": [ - "solver = pybamm.CasadiSolver(mode=\"fast\", sensitivity=True)\n", + "solver = pybamm.CasadiSolver(mode=\"fast\", sensitivity=\"casadi\")\n", "sim = pybamm.Simulation(model, parameter_values=param, solver=solver)\n", "solution = sim.solve(t_eval=np.linspace(0,3600), inputs={\"Dsn\": 1, \"Dsp\": 1})" ] }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 7, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "1.7811190900000042" + "0.015949444000000312" ] }, - "execution_count": 20, + "execution_count": 7, "metadata": {}, "output_type": "execute_result" } @@ -142,6 +142,44 @@ "Since we have not specified the parameter values when solving, the resulting solution contains _symbolic_ variables, such as the voltage" ] }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 3.73 s, sys: 194 ms, total: 3.92 s\n", + "Wall time: 3.91 s\n" + ] + }, + { + "data": { + "text/plain": [ + "{'all': DM(sparse: 1500-by-100, 73500 nnz\n", + " (30, 0) -> -8.80308e-05\n", + " (31, 0) -> -9.35556e-05\n", + " (32, 0) -> -0.000107887\n", + " ...\n", + " (1497, 98) -> 0.0170617\n", + " (1498, 98) -> 0.021474\n", + " (1499, 98) -> 0.0260439),\n", + " 'Dsn': DM([00, 00, 00, ..., 0.0170617, 0.021474, 0.0260439]),\n", + " 'Dsp': DM([00, 00, 00, ..., 00, 00, 00])}" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%time\n", + "solution[\"X-averaged negative particle concentration\"].sensitivity" + ] + }, { "cell_type": "code", "execution_count": 17, diff --git a/pybamm/solvers/base_solver.py b/pybamm/solvers/base_solver.py index a2aa4b55d5..840ac7fe39 100644 --- a/pybamm/solvers/base_solver.py +++ b/pybamm/solvers/base_solver.py @@ -204,7 +204,10 @@ def set_up(self, model, inputs=None): model.convert_to_format = "casadi" # Only allow solving sensitivity equations with the casadi format for now - if self.sensitivity is True and model.convert_to_format != "casadi": + if ( + self.sensitivity is "explicit forward" + and model.convert_to_format != "casadi" + ): raise NotImplementedError( "model should be converted to casadi format in order to solve " "sensitivity equations" @@ -231,7 +234,7 @@ def set_up(self, model, inputs=None): p_casadi[name] = casadi.MX.sym(name, value.shape[0]) p_casadi_stacked = casadi.vertcat(*[p for p in p_casadi.values()]) # sensitivity vectors - if self.sensitivity is True: + if self.sensitivity == "explicit forward": S_x = casadi.MX.sym("S_x", model.len_rhs * p_casadi_stacked.shape[0]) S_z = casadi.MX.sym("S_z", model.len_alg * p_casadi_stacked.shape[0]) y_and_S = casadi.vertcat(y_diff, S_x, y_alg, S_z) @@ -286,7 +289,7 @@ def report(string): report(f"Converting {name} to CasADi") func = func.to_casadi(t_casadi, y_casadi, inputs=p_casadi) # Add sensitivity vectors to the rhs and algebraic equations - if self.sensitivity is True: + if self.sensitivity == "explicit forward": if name == "rhs" and model.len_rhs > 0: report("Creating sensitivity equations for rhs using CasADi") df_dx = casadi.jacobian(func, y_diff) @@ -408,7 +411,7 @@ def report(string): )[0] init_eval = InitialConditions(initial_conditions, model) - if self.sensitivity is True: + if self.sensitivity == "explicit forward": init_eval.y_dummy = np.zeros( ( model.len_rhs_and_alg * (np.vstack(list(inputs.values())).size + 1), @@ -456,7 +459,7 @@ def report(string): ): # can use DAE solver to solve model with algebraic equations only if len(model.rhs) > 0: - if self.sensitivity is True: + if self.sensitivity == "explicit forward": # Copy mass matrix blocks diagonally single_mass_matrix_inv = model.mass_matrix_inv.entries.toarray() n_inputs = p_casadi_stacked.shape[0] @@ -946,7 +949,7 @@ def _set_up_ext_and_inputs(self, model, external_variables, inputs): name = input_param.name if name not in inputs: # Don't allow symbolic inputs if using `sensitivity` - if self.sensitivity is True: + if self.sensitivity == "explicit forward": raise pybamm.SolverError( "Cannot have symbolic inputs if explicitly solving forward" "sensitivity equations" diff --git a/tests/unit/test_solvers/test_casadi_solver.py b/tests/unit/test_solvers/test_casadi_solver.py index e948a788aa..3035c82263 100644 --- a/tests/unit/test_solvers/test_casadi_solver.py +++ b/tests/unit/test_solvers/test_casadi_solver.py @@ -607,279 +607,279 @@ def test_solve_with_symbolic_input_in_initial_conditions(self): # np.testing.assert_array_almost_equal(lsq_sol.x, [3, 3], decimal=3) -# class TestCasadiSolverODEsWithForwardSensitivityEquations(unittest.TestCase): -# def test_solve_sensitivity_scalar_var_scalar_input(self): -# # Create model -# model = pybamm.BaseModel() -# var = pybamm.Variable("var") -# p = pybamm.InputParameter("p") -# model.rhs = {var: p * var} -# model.initial_conditions = {var: 1} -# model.variables = {"var squared": var ** 2} - -# # Solve -# # Make sure that passing in extra options works -# solver = pybamm.CasadiSolver( -# mode="fast", rtol=1e-10, atol=1e-10, sensitivity="explicit forward" -# ) -# t_eval = np.linspace(0, 1, 80) -# solution = solver.solve(model, t_eval, inputs={"p": 0.1}) -# np.testing.assert_array_equal(solution.t, t_eval) -# np.testing.assert_allclose(solution.y[0], np.exp(0.1 * solution.t)) -# np.testing.assert_allclose( -# solution.sensitivity["p"], -# (solution.t * np.exp(0.1 * solution.t))[:, np.newaxis], -# ) -# np.testing.assert_allclose( -# solution["var squared"].data, np.exp(0.1 * solution.t) ** 2 -# ) -# np.testing.assert_allclose( -# solution["var squared"].sensitivity["p"], -# (2 * np.exp(0.1 * solution.t) * solution.t * np.exp(0.1 * solution.t))[ -# :, np.newaxis -# ], -# ) - -# # More complicated model -# # Create model -# model = pybamm.BaseModel() -# var = pybamm.Variable("var") -# p = pybamm.InputParameter("p") -# q = pybamm.InputParameter("q") -# r = pybamm.InputParameter("r") -# s = pybamm.InputParameter("s") -# model.rhs = {var: p * q} -# model.initial_conditions = {var: r} -# model.variables = {"var times s": var * s} - -# # Solve -# # Make sure that passing in extra options works -# solver = pybamm.CasadiSolver( -# rtol=1e-10, atol=1e-10, sensitivity="explicit forward" -# ) -# t_eval = np.linspace(0, 1, 80) -# solution = solver.solve( -# model, t_eval, inputs={"p": 0.1, "q": 2, "r": -1, "s": 0.5} -# ) -# np.testing.assert_allclose(solution.y[0], -1 + 0.2 * solution.t) -# np.testing.assert_allclose( -# solution.sensitivity["p"], (2 * solution.t)[:, np.newaxis], -# ) -# np.testing.assert_allclose( -# solution.sensitivity["q"], (0.1 * solution.t)[:, np.newaxis], -# ) -# np.testing.assert_allclose(solution.sensitivity["r"], 1) -# np.testing.assert_allclose(solution.sensitivity["s"], 0) -# np.testing.assert_allclose( -# solution.sensitivity["all"], -# np.hstack( -# [ -# solution.sensitivity["p"], -# solution.sensitivity["q"], -# solution.sensitivity["r"], -# solution.sensitivity["s"], -# ] -# ), -# ) -# np.testing.assert_allclose( -# solution["var times s"].data, 0.5 * (-1 + 0.2 * solution.t) -# ) -# np.testing.assert_allclose( -# solution["var times s"].sensitivity["p"], -# 0.5 * (2 * solution.t)[:, np.newaxis], -# ) -# np.testing.assert_allclose( -# solution["var times s"].sensitivity["q"], -# 0.5 * (0.1 * solution.t)[:, np.newaxis], -# ) -# np.testing.assert_allclose(solution["var times s"].sensitivity["r"], 0.5) -# np.testing.assert_allclose( -# solution["var times s"].sensitivity["s"], -# (-1 + 0.2 * solution.t)[:, np.newaxis], -# ) -# np.testing.assert_allclose( -# solution["var times s"].sensitivity["all"], -# np.hstack( -# [ -# solution["var times s"].sensitivity["p"], -# solution["var times s"].sensitivity["q"], -# solution["var times s"].sensitivity["r"], -# solution["var times s"].sensitivity["s"], -# ] -# ), -# ) - -# def test_solve_sensitivity_vector_var_scalar_input(self): -# var = pybamm.Variable("var", "negative electrode") -# model = pybamm.BaseModel() -# # Set length scales to avoid warning -# model.length_scales = {"negative electrode": 1} -# param = pybamm.InputParameter("param") -# model.rhs = {var: -param * var} -# model.initial_conditions = {var: 2} -# model.variables = {"var": var} - -# # create discretisation -# disc = get_discretisation_for_testing() -# disc.process_model(model) -# n = disc.mesh["negative electrode"].npts - -# # Solve - scalar input -# solver = pybamm.CasadiSolver(sensitivity="explicit forward") -# t_eval = np.linspace(0, 1) -# solution = solver.solve(model, t_eval, inputs={"param": 7}) -# np.testing.assert_array_almost_equal( -# solution["var"].data, np.tile(2 * np.exp(-7 * t_eval), (n, 1)), decimal=4, -# ) -# np.testing.assert_array_almost_equal( -# solution["var"].sensitivity["param"], -# np.repeat(-2 * t_eval * np.exp(-7 * t_eval), n)[:, np.newaxis], -# decimal=4, -# ) - -# # More complicated model -# # Create model -# model = pybamm.BaseModel() -# # Set length scales to avoid warning -# model.length_scales = {"negative electrode": 1} -# var = pybamm.Variable("var", "negative electrode") -# p = pybamm.InputParameter("p") -# q = pybamm.InputParameter("q") -# r = pybamm.InputParameter("r") -# s = pybamm.InputParameter("s") -# model.rhs = {var: p * q} -# model.initial_conditions = {var: r} -# model.variables = {"var times s": var * s} - -# # Discretise -# disc.process_model(model) - -# # Solve -# # Make sure that passing in extra options works -# solver = pybamm.CasadiSolver( -# rtol=1e-10, atol=1e-10, sensitivity="explicit forward" -# ) -# t_eval = np.linspace(0, 1, 80) -# solution = solver.solve( -# model, t_eval, inputs={"p": 0.1, "q": 2, "r": -1, "s": 0.5} -# ) -# np.testing.assert_allclose(solution.y, np.tile(-1 + 0.2 * solution.t, (n, 1))) -# np.testing.assert_allclose( -# solution.sensitivity["p"], np.repeat(2 * solution.t, n)[:, np.newaxis], -# ) -# np.testing.assert_allclose( -# solution.sensitivity["q"], np.repeat(0.1 * solution.t, n)[:, np.newaxis], -# ) -# np.testing.assert_allclose(solution.sensitivity["r"], 1) -# np.testing.assert_allclose(solution.sensitivity["s"], 0) -# np.testing.assert_allclose( -# solution.sensitivity["all"], -# np.hstack( -# [ -# solution.sensitivity["p"], -# solution.sensitivity["q"], -# solution.sensitivity["r"], -# solution.sensitivity["s"], -# ] -# ), -# ) -# np.testing.assert_allclose( -# solution["var times s"].data, np.tile(0.5 * (-1 + 0.2 * solution.t), (n, 1)) -# ) -# np.testing.assert_allclose( -# solution["var times s"].sensitivity["p"], -# np.repeat(0.5 * (2 * solution.t), n)[:, np.newaxis], -# ) -# np.testing.assert_allclose( -# solution["var times s"].sensitivity["q"], -# np.repeat(0.5 * (0.1 * solution.t), n)[:, np.newaxis], -# ) -# np.testing.assert_allclose(solution["var times s"].sensitivity["r"], 0.5) -# np.testing.assert_allclose( -# solution["var times s"].sensitivity["s"], -# np.repeat(-1 + 0.2 * solution.t, n)[:, np.newaxis], -# ) -# np.testing.assert_allclose( -# solution["var times s"].sensitivity["all"], -# np.hstack( -# [ -# solution["var times s"].sensitivity["p"], -# solution["var times s"].sensitivity["q"], -# solution["var times s"].sensitivity["r"], -# solution["var times s"].sensitivity["s"], -# ] -# ), -# ) - -# def test_solve_sensitivity_scalar_var_vector_input(self): -# var = pybamm.Variable("var", "negative electrode") -# model = pybamm.BaseModel() -# # Set length scales to avoid warning -# model.length_scales = {"negative electrode": 1} - -# param = pybamm.InputParameter("param", "negative electrode") -# model.rhs = {var: -param * var} -# model.initial_conditions = {var: 2} -# model.variables = { -# "var": var, -# "integral of var": pybamm.Integral(var, pybamm.standard_spatial_vars.x_n), -# } - -# # create discretisation -# mesh = get_mesh_for_testing(xpts=5) -# spatial_methods = {"macroscale": pybamm.FiniteVolume()} -# disc = pybamm.Discretisation(mesh, spatial_methods) -# disc.process_model(model) -# n = disc.mesh["negative electrode"].npts - -# # Solve - constant input -# solver = pybamm.CasadiSolver( -# mode="fast", rtol=1e-10, atol=1e-10, sensitivity="explicit forward" -# ) -# t_eval = np.linspace(0, 1) -# solution = solver.solve(model, t_eval, inputs={"param": 7 * np.ones(n)}) -# l_n = mesh["negative electrode"].edges[-1] -# np.testing.assert_array_almost_equal( -# solution["var"].data, np.tile(2 * np.exp(-7 * t_eval), (n, 1)), decimal=4, -# ) - -# np.testing.assert_array_almost_equal( -# solution["var"].sensitivity["param"], -# np.vstack([np.eye(n) * -2 * t * np.exp(-7 * t) for t in t_eval]), -# ) -# np.testing.assert_array_almost_equal( -# solution["integral of var"].data, 2 * np.exp(-7 * t_eval) * l_n, decimal=4, -# ) -# np.testing.assert_array_almost_equal( -# solution["integral of var"].sensitivity["param"], -# np.tile(-2 * t_eval * np.exp(-7 * t_eval) * l_n / n, (n, 1)).T, -# ) - -# # Solve - linspace input -# p_eval = np.linspace(1, 2, n) -# solution = solver.solve(model, t_eval, inputs={"param": p_eval}) -# l_n = mesh["negative electrode"].edges[-1] -# np.testing.assert_array_almost_equal( -# solution["var"].data, 2 * np.exp(-p_eval[:, np.newaxis] * t_eval), decimal=4 -# ) -# np.testing.assert_array_almost_equal( -# solution["var"].sensitivity["param"], -# np.vstack([np.diag(-2 * t * np.exp(-p_eval * t)) for t in t_eval]), -# ) - -# np.testing.assert_array_almost_equal( -# solution["integral of var"].data, -# np.sum( -# 2 -# * np.exp(-p_eval[:, np.newaxis] * t_eval) -# * mesh["negative electrode"].d_edges[:, np.newaxis], -# axis=0, -# ), -# ) -# np.testing.assert_array_almost_equal( -# solution["integral of var"].sensitivity["param"], -# np.vstack([-2 * t * np.exp(-p_eval * t) * l_n / n for t in t_eval]), -# ) +class TestCasadiSolverODEsWithForwardSensitivityEquations(unittest.TestCase): + def test_solve_sensitivity_scalar_var_scalar_input(self): + # Create model + model = pybamm.BaseModel() + var = pybamm.Variable("var") + p = pybamm.InputParameter("p") + model.rhs = {var: p * var} + model.initial_conditions = {var: 1} + model.variables = {"var squared": var ** 2} + + # Solve + # Make sure that passing in extra options works + solver = pybamm.CasadiSolver( + mode="fast", rtol=1e-10, atol=1e-10, sensitivity="explicit forward" + ) + t_eval = np.linspace(0, 1, 80) + solution = solver.solve(model, t_eval, inputs={"p": 0.1}) + np.testing.assert_array_equal(solution.t, t_eval) + np.testing.assert_allclose(solution.y[0], np.exp(0.1 * solution.t)) + np.testing.assert_allclose( + solution.sensitivity["p"], + (solution.t * np.exp(0.1 * solution.t))[:, np.newaxis], + ) + np.testing.assert_allclose( + solution["var squared"].data, np.exp(0.1 * solution.t) ** 2 + ) + np.testing.assert_allclose( + solution["var squared"].sensitivity["p"], + (2 * np.exp(0.1 * solution.t) * solution.t * np.exp(0.1 * solution.t))[ + :, np.newaxis + ], + ) + + # More complicated model + # Create model + model = pybamm.BaseModel() + var = pybamm.Variable("var") + p = pybamm.InputParameter("p") + q = pybamm.InputParameter("q") + r = pybamm.InputParameter("r") + s = pybamm.InputParameter("s") + model.rhs = {var: p * q} + model.initial_conditions = {var: r} + model.variables = {"var times s": var * s} + + # Solve + # Make sure that passing in extra options works + solver = pybamm.CasadiSolver( + rtol=1e-10, atol=1e-10, sensitivity="explicit forward" + ) + t_eval = np.linspace(0, 1, 80) + solution = solver.solve( + model, t_eval, inputs={"p": 0.1, "q": 2, "r": -1, "s": 0.5} + ) + np.testing.assert_allclose(solution.y[0], -1 + 0.2 * solution.t) + np.testing.assert_allclose( + solution.sensitivity["p"], (2 * solution.t)[:, np.newaxis], + ) + np.testing.assert_allclose( + solution.sensitivity["q"], (0.1 * solution.t)[:, np.newaxis], + ) + np.testing.assert_allclose(solution.sensitivity["r"], 1) + np.testing.assert_allclose(solution.sensitivity["s"], 0) + np.testing.assert_allclose( + solution.sensitivity["all"], + np.hstack( + [ + solution.sensitivity["p"], + solution.sensitivity["q"], + solution.sensitivity["r"], + solution.sensitivity["s"], + ] + ), + ) + np.testing.assert_allclose( + solution["var times s"].data, 0.5 * (-1 + 0.2 * solution.t) + ) + np.testing.assert_allclose( + solution["var times s"].sensitivity["p"], + 0.5 * (2 * solution.t)[:, np.newaxis], + ) + np.testing.assert_allclose( + solution["var times s"].sensitivity["q"], + 0.5 * (0.1 * solution.t)[:, np.newaxis], + ) + np.testing.assert_allclose(solution["var times s"].sensitivity["r"], 0.5) + np.testing.assert_allclose( + solution["var times s"].sensitivity["s"], + (-1 + 0.2 * solution.t)[:, np.newaxis], + ) + np.testing.assert_allclose( + solution["var times s"].sensitivity["all"], + np.hstack( + [ + solution["var times s"].sensitivity["p"], + solution["var times s"].sensitivity["q"], + solution["var times s"].sensitivity["r"], + solution["var times s"].sensitivity["s"], + ] + ), + ) + + def test_solve_sensitivity_vector_var_scalar_input(self): + var = pybamm.Variable("var", "negative electrode") + model = pybamm.BaseModel() + # Set length scales to avoid warning + model.length_scales = {"negative electrode": 1} + param = pybamm.InputParameter("param") + model.rhs = {var: -param * var} + model.initial_conditions = {var: 2} + model.variables = {"var": var} + + # create discretisation + disc = get_discretisation_for_testing() + disc.process_model(model) + n = disc.mesh["negative electrode"].npts + + # Solve - scalar input + solver = pybamm.CasadiSolver(sensitivity="explicit forward") + t_eval = np.linspace(0, 1) + solution = solver.solve(model, t_eval, inputs={"param": 7}) + np.testing.assert_array_almost_equal( + solution["var"].data, np.tile(2 * np.exp(-7 * t_eval), (n, 1)), decimal=4, + ) + np.testing.assert_array_almost_equal( + solution["var"].sensitivity["param"], + np.repeat(-2 * t_eval * np.exp(-7 * t_eval), n)[:, np.newaxis], + decimal=4, + ) + + # More complicated model + # Create model + model = pybamm.BaseModel() + # Set length scales to avoid warning + model.length_scales = {"negative electrode": 1} + var = pybamm.Variable("var", "negative electrode") + p = pybamm.InputParameter("p") + q = pybamm.InputParameter("q") + r = pybamm.InputParameter("r") + s = pybamm.InputParameter("s") + model.rhs = {var: p * q} + model.initial_conditions = {var: r} + model.variables = {"var times s": var * s} + + # Discretise + disc.process_model(model) + + # Solve + # Make sure that passing in extra options works + solver = pybamm.CasadiSolver( + rtol=1e-10, atol=1e-10, sensitivity="explicit forward" + ) + t_eval = np.linspace(0, 1, 80) + solution = solver.solve( + model, t_eval, inputs={"p": 0.1, "q": 2, "r": -1, "s": 0.5} + ) + np.testing.assert_allclose(solution.y, np.tile(-1 + 0.2 * solution.t, (n, 1))) + np.testing.assert_allclose( + solution.sensitivity["p"], np.repeat(2 * solution.t, n)[:, np.newaxis], + ) + np.testing.assert_allclose( + solution.sensitivity["q"], np.repeat(0.1 * solution.t, n)[:, np.newaxis], + ) + np.testing.assert_allclose(solution.sensitivity["r"], 1) + np.testing.assert_allclose(solution.sensitivity["s"], 0) + np.testing.assert_allclose( + solution.sensitivity["all"], + np.hstack( + [ + solution.sensitivity["p"], + solution.sensitivity["q"], + solution.sensitivity["r"], + solution.sensitivity["s"], + ] + ), + ) + np.testing.assert_allclose( + solution["var times s"].data, np.tile(0.5 * (-1 + 0.2 * solution.t), (n, 1)) + ) + np.testing.assert_allclose( + solution["var times s"].sensitivity["p"], + np.repeat(0.5 * (2 * solution.t), n)[:, np.newaxis], + ) + np.testing.assert_allclose( + solution["var times s"].sensitivity["q"], + np.repeat(0.5 * (0.1 * solution.t), n)[:, np.newaxis], + ) + np.testing.assert_allclose(solution["var times s"].sensitivity["r"], 0.5) + np.testing.assert_allclose( + solution["var times s"].sensitivity["s"], + np.repeat(-1 + 0.2 * solution.t, n)[:, np.newaxis], + ) + np.testing.assert_allclose( + solution["var times s"].sensitivity["all"], + np.hstack( + [ + solution["var times s"].sensitivity["p"], + solution["var times s"].sensitivity["q"], + solution["var times s"].sensitivity["r"], + solution["var times s"].sensitivity["s"], + ] + ), + ) + + def test_solve_sensitivity_scalar_var_vector_input(self): + var = pybamm.Variable("var", "negative electrode") + model = pybamm.BaseModel() + # Set length scales to avoid warning + model.length_scales = {"negative electrode": 1} + + param = pybamm.InputParameter("param", "negative electrode") + model.rhs = {var: -param * var} + model.initial_conditions = {var: 2} + model.variables = { + "var": var, + "integral of var": pybamm.Integral(var, pybamm.standard_spatial_vars.x_n), + } + + # create discretisation + mesh = get_mesh_for_testing(xpts=5) + spatial_methods = {"macroscale": pybamm.FiniteVolume()} + disc = pybamm.Discretisation(mesh, spatial_methods) + disc.process_model(model) + n = disc.mesh["negative electrode"].npts + + # Solve - constant input + solver = pybamm.CasadiSolver( + mode="fast", rtol=1e-10, atol=1e-10, sensitivity="explicit forward" + ) + t_eval = np.linspace(0, 1) + solution = solver.solve(model, t_eval, inputs={"param": 7 * np.ones(n)}) + l_n = mesh["negative electrode"].edges[-1] + np.testing.assert_array_almost_equal( + solution["var"].data, np.tile(2 * np.exp(-7 * t_eval), (n, 1)), decimal=4, + ) + + np.testing.assert_array_almost_equal( + solution["var"].sensitivity["param"], + np.vstack([np.eye(n) * -2 * t * np.exp(-7 * t) for t in t_eval]), + ) + np.testing.assert_array_almost_equal( + solution["integral of var"].data, 2 * np.exp(-7 * t_eval) * l_n, decimal=4, + ) + np.testing.assert_array_almost_equal( + solution["integral of var"].sensitivity["param"], + np.tile(-2 * t_eval * np.exp(-7 * t_eval) * l_n / n, (n, 1)).T, + ) + + # Solve - linspace input + p_eval = np.linspace(1, 2, n) + solution = solver.solve(model, t_eval, inputs={"param": p_eval}) + l_n = mesh["negative electrode"].edges[-1] + np.testing.assert_array_almost_equal( + solution["var"].data, 2 * np.exp(-p_eval[:, np.newaxis] * t_eval), decimal=4 + ) + np.testing.assert_array_almost_equal( + solution["var"].sensitivity["param"], + np.vstack([np.diag(-2 * t * np.exp(-p_eval * t)) for t in t_eval]), + ) + + np.testing.assert_array_almost_equal( + solution["integral of var"].data, + np.sum( + 2 + * np.exp(-p_eval[:, np.newaxis] * t_eval) + * mesh["negative electrode"].d_edges[:, np.newaxis], + axis=0, + ), + ) + np.testing.assert_array_almost_equal( + solution["integral of var"].sensitivity["param"], + np.vstack([-2 * t * np.exp(-p_eval * t) * l_n / n for t in t_eval]), + ) if __name__ == "__main__": From 94314fb7b22397621ee15e82f3fa8471e4ebd87d Mon Sep 17 00:00:00 2001 From: Valentin Sulzer Date: Thu, 23 Jul 2020 14:28:31 -0400 Subject: [PATCH 12/73] #1100 flake8 --- pybamm/solvers/base_solver.py | 2 +- pybamm/solvers/casadi_algebraic_solver.py | 1 - tests/unit/test_solvers/test_casadi_solver.py | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/pybamm/solvers/base_solver.py b/pybamm/solvers/base_solver.py index 840ac7fe39..0c04666aed 100644 --- a/pybamm/solvers/base_solver.py +++ b/pybamm/solvers/base_solver.py @@ -205,7 +205,7 @@ def set_up(self, model, inputs=None): # Only allow solving sensitivity equations with the casadi format for now if ( - self.sensitivity is "explicit forward" + self.sensitivity == "explicit forward" and model.convert_to_format != "casadi" ): raise NotImplementedError( diff --git a/pybamm/solvers/casadi_algebraic_solver.py b/pybamm/solvers/casadi_algebraic_solver.py index 07bd36e6cc..7ea214ce10 100644 --- a/pybamm/solvers/casadi_algebraic_solver.py +++ b/pybamm/solvers/casadi_algebraic_solver.py @@ -3,7 +3,6 @@ # import casadi import pybamm -import numbers import numpy as np diff --git a/tests/unit/test_solvers/test_casadi_solver.py b/tests/unit/test_solvers/test_casadi_solver.py index 3035c82263..a93cb6f952 100644 --- a/tests/unit/test_solvers/test_casadi_solver.py +++ b/tests/unit/test_solvers/test_casadi_solver.py @@ -6,7 +6,6 @@ import numpy as np from tests import get_mesh_for_testing, get_discretisation_for_testing from scipy.sparse import eye -from scipy.optimize import least_squares class TestCasadiSolver(unittest.TestCase): From 03cc0ebfe41b5a668572c037fe484723fd04d7e5 Mon Sep 17 00:00:00 2001 From: Valentin Sulzer Date: Mon, 27 Jul 2020 20:39:39 -0400 Subject: [PATCH 13/73] #1100 reformat Solution syntax --- pybamm/solvers/algebraic_solver.py | 9 ++- pybamm/solvers/base_solver.py | 14 ++--- pybamm/solvers/casadi_algebraic_solver.py | 4 +- pybamm/solvers/casadi_solver.py | 4 +- pybamm/solvers/dummy_solver.py | 4 +- pybamm/solvers/idaklu_solver.py | 3 + pybamm/solvers/jax_solver.py | 57 +++++++++++-------- pybamm/solvers/scikits_dae_solver.py | 3 + pybamm/solvers/scikits_ode_solver.py | 3 + pybamm/solvers/scipy_solver.py | 7 ++- pybamm/solvers/solution.py | 48 +++++++--------- .../test_solvers/test_algebraic_solver.py | 3 + tests/unit/test_solvers/test_base_solver.py | 3 + .../unit/test_solvers/test_scikits_solvers.py | 1 + tests/unit/test_solvers/test_scipy_solver.py | 22 +++++-- tests/unit/test_solvers/test_solution.py | 14 ++--- 16 files changed, 113 insertions(+), 86 deletions(-) diff --git a/pybamm/solvers/algebraic_solver.py b/pybamm/solvers/algebraic_solver.py index dd4e0d0b20..0b56792070 100644 --- a/pybamm/solvers/algebraic_solver.py +++ b/pybamm/solvers/algebraic_solver.py @@ -59,9 +59,9 @@ def _integrate(self, model, t_eval, inputs=None): inputs : dict, optional Any input parameters to pass to the model when solving """ - inputs = inputs or {} + inputs_dict = inputs or {} if model.convert_to_format == "casadi": - inputs = casadi.vertcat(*[x for x in inputs.values()]) + inputs = casadi.vertcat(*[x for x in inputs_dict.values()]) y0 = model.y0 if isinstance(y0, casadi.DM): @@ -210,4 +210,7 @@ def jac_norm(y): 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) - return pybamm.Solution(t_eval, y_sol, termination="success") + return pybamm.Solution( + t_eval, y_sol, termination="success", model=model, inputs=inputs_dict + ) + diff --git a/pybamm/solvers/base_solver.py b/pybamm/solvers/base_solver.py index 50ae92c34d..3b502b0963 100644 --- a/pybamm/solvers/base_solver.py +++ b/pybamm/solvers/base_solver.py @@ -34,11 +34,11 @@ class BaseSolver(object): sensitivity : str, optional Whether (and how) to calculate sensitivities when solving. Options are: - - "explicit forward": explicitly formulate the sensitivity equations. - The formulation is as per "Park, S., Kato, D., Gima, Z., - Klein, R., & Moura, S. (2018). Optimal experimental design for parameterization - of an electrochemical lithium-ion battery model. Journal of The Electrochemical - Society, 165(7), A1309.". See #1100 for details + - "explicit forward": explicitly formulate the sensitivity equations. \ + The formulation is as per "Park, S., Kato, D., Gima, Z., \ + Klein, R., & Moura, S. (2018). Optimal experimental design for parameterization\ + of an electrochemical lithium-ion battery model. Journal of The Electrochemical\ + Society, 165(7), A1309.". See #1100 for details \ - see specific solvers for other options """ @@ -891,10 +891,6 @@ def step( solution.set_up_time = set_up_time solution.solve_time = timer.time() - # Add model and inputs to solution - solution.model = model - solution.inputs = ext_and_inputs - # Identify the event that caused termination termination = self.get_termination_reason(solution, model.events) diff --git a/pybamm/solvers/casadi_algebraic_solver.py b/pybamm/solvers/casadi_algebraic_solver.py index 7ea214ce10..1526f2d2f8 100644 --- a/pybamm/solvers/casadi_algebraic_solver.py +++ b/pybamm/solvers/casadi_algebraic_solver.py @@ -25,7 +25,7 @@ class CasadiAlgebraicSolver(pybamm.BaseSolver): Whether (and how) to calculate sensitivities when solving. Options are: - None: no sensitivities - - "explicit forward": explicitly formulate the sensitivity equations. + - "explicit forward": explicitly formulate the sensitivity equations. \ See :class:`pybamm.BaseSolver` - "casadi": use casadi to differentiate through the rootfinding operator @@ -66,7 +66,7 @@ def _integrate(self, model, t_eval, inputs=None): # Record whether there are any symbolic inputs inputs_dict = inputs or {} # Create casadi objects for the root-finder - inputs = casadi.vertcat(*[v for v in inputs.values()]) + inputs = casadi.vertcat(*[v for v in inputs_dict.values()]) # Create symbolic inputs symbolic_inputs = casadi.MX.sym("inputs", inputs.shape[0]) diff --git a/pybamm/solvers/casadi_solver.py b/pybamm/solvers/casadi_solver.py index a5a0bed28d..7d585e0545 100644 --- a/pybamm/solvers/casadi_solver.py +++ b/pybamm/solvers/casadi_solver.py @@ -58,11 +58,11 @@ class CasadiSolver(pybamm.BaseSolver): Any options to pass to the CasADi integrator when calling the integrator. Please consult `CasADi documentation `_ for details. - sensitivity : bool, optional + sensitivity : str, optional Whether (and how) to calculate sensitivities when solving. Options are: - None: no sensitivities - - "explicit forward": explicitly formulate the sensitivity equations. + - "explicit forward": explicitly formulate the sensitivity equations. \ See :class:`pybamm.BaseSolver` - "casadi": use casadi to differentiate through the integrator """ diff --git a/pybamm/solvers/dummy_solver.py b/pybamm/solvers/dummy_solver.py index 483bba8a77..ff5fef28d3 100644 --- a/pybamm/solvers/dummy_solver.py +++ b/pybamm/solvers/dummy_solver.py @@ -33,4 +33,6 @@ def _integrate(self, model, t_eval, inputs=None): """ y_sol = np.zeros((1, t_eval.size)) - return pybamm.Solution(t_eval, y_sol, termination="final time") + return pybamm.Solution( + t_eval, y_sol, termination="final time", model=model, inputs=inputs + ) diff --git a/pybamm/solvers/idaklu_solver.py b/pybamm/solvers/idaklu_solver.py index 6e7cc7fcc3..bb32ea85c8 100644 --- a/pybamm/solvers/idaklu_solver.py +++ b/pybamm/solvers/idaklu_solver.py @@ -154,6 +154,7 @@ def _integrate(self, model, t_eval, inputs=None): t_eval : numeric type The times at which to compute the solution """ + inputs_dict = inputs if model.rhs_eval.form == "casadi": # stack inputs inputs = casadi.vertcat(*[x for x in inputs.values()]) @@ -272,6 +273,8 @@ def rootfn(t, y): t[-1], np.transpose(y_out[-1])[:, np.newaxis], termination, + model=model, + inputs=inputs_dict, ) else: raise pybamm.SolverError(sol.message) diff --git a/pybamm/solvers/jax_solver.py b/pybamm/solvers/jax_solver.py index db912d0da1..b8b9cfa815 100644 --- a/pybamm/solvers/jax_solver.py +++ b/pybamm/solvers/jax_solver.py @@ -45,16 +45,17 @@ class JaxSolver(pybamm.BaseSolver): for details. """ - def __init__(self, method='RK45', root_method=None, - rtol=1e-6, atol=1e-6, extra_options=None): + def __init__( + self, method="RK45", root_method=None, rtol=1e-6, atol=1e-6, extra_options=None + ): # note: bdf solver itself calculates consistent initial conditions so can set # root_method to none, allow user to override this behavior super().__init__(method, rtol, atol, root_method=root_method) - method_options = ['RK45', 'BDF'] + method_options = ["RK45", "BDF"] if method not in method_options: - raise ValueError('method must be one of {}'.format(method_options)) + raise ValueError("method must be one of {}".format(method_options)) self.ode_solver = False - if method == 'RK45': + if method == "RK45": self.ode_solver = True self.extra_options = extra_options or {} self.name = "JAX solver ({})".format(method) @@ -80,8 +81,9 @@ def get_solve(self, model, t_eval): """ if model not in self._cached_solves: if model not in self.models_set_up: - raise RuntimeError("Model is not set up for solving, run" - "`solver.solve(model)` first") + raise RuntimeError( + "Model is not set up for solving, run" "`solver.solve(model)` first" + ) self._cached_solves[model] = self.create_solve(model, t_eval) @@ -106,32 +108,35 @@ def create_solve(self, model, t_eval): """ if model.convert_to_format != "jax": - raise RuntimeError("Model must be converted to JAX to use this solver" - " (i.e. `model.convert_to_format = 'jax')") + raise RuntimeError( + "Model must be converted to JAX to use this solver" + " (i.e. `model.convert_to_format = 'jax')" + ) if model.terminate_events_eval: - raise RuntimeError("Terminate events not supported for this solver." - " Model has the following events:" - " {}.\nYou can remove events using `model.events = []`." - " It might be useful to first solve the model using a" - " different solver to obtain the time of the event, then" - " re-solve using no events and a fixed" - " end-time".format(model.events)) + raise RuntimeError( + "Terminate events not supported for this solver." + " Model has the following events:" + " {}.\nYou can remove events using `model.events = []`." + " It might be useful to first solve the model using a" + " different solver to obtain the time of the event, then" + " re-solve using no events and a fixed" + " end-time".format(model.events) + ) # Initial conditions, make sure they are an 0D array y0 = jnp.array(model.y0).reshape(-1) mass = None - if self.method == 'BDF': + if self.method == "BDF": mass = model.mass_matrix.entries.toarray() def rhs_ode(y, t, inputs): - return model.rhs_eval(t, y, inputs), + return (model.rhs_eval(t, y, inputs),) def rhs_dae(y, t, inputs): - return jnp.concatenate([ - model.rhs_eval(t, y, inputs), - model.algebraic_eval(t, y, inputs), - ]) + return jnp.concatenate( + [model.rhs_eval(t, y, inputs), model.algebraic_eval(t, y, inputs)] + ) def solve_model_rk45(inputs): y = odeint( @@ -158,7 +163,7 @@ def solve_model_bdf(inputs): ) return jnp.transpose(y) - if self.method == 'RK45': + if self.method == "RK45": return jax.jit(solve_model_rk45) else: return jax.jit(solve_model_bdf) @@ -194,5 +199,7 @@ def _integrate(self, model, t_eval, inputs=None): termination = "final time" t_event = None y_event = onp.array(None) - return pybamm.Solution(t_eval, y, - t_event, y_event, termination) + return pybamm.Solution( + t_eval, y, t_event, y_event, termination, model=model, inputs=inputs + ) + diff --git a/pybamm/solvers/scikits_dae_solver.py b/pybamm/solvers/scikits_dae_solver.py index 6f3b56ec8f..af2fa22dd4 100644 --- a/pybamm/solvers/scikits_dae_solver.py +++ b/pybamm/solvers/scikits_dae_solver.py @@ -81,6 +81,7 @@ def _integrate(self, model, t_eval, inputs=None): Any input parameters to pass to the model when solving """ + inputs_dict = inputs if model.convert_to_format == "casadi": inputs = casadi.vertcat(*[x for x in inputs.values()]) @@ -150,6 +151,8 @@ def jacfn(t, y, ydot, residuals, cj, J): t_root, np.transpose(sol.roots.y), termination, + model=model, + inputs=inputs_dict, ) else: raise pybamm.SolverError(sol.message) diff --git a/pybamm/solvers/scikits_ode_solver.py b/pybamm/solvers/scikits_ode_solver.py index 457e5b520c..479b648fa4 100644 --- a/pybamm/solvers/scikits_ode_solver.py +++ b/pybamm/solvers/scikits_ode_solver.py @@ -80,6 +80,7 @@ def _integrate(self, model, t_eval, inputs=None): Any input parameters to pass to the model when solving """ + inputs_dict = inputs if model.rhs_eval.form == "casadi": inputs = casadi.vertcat(*[x for x in inputs.values()]) @@ -167,6 +168,8 @@ def jac_times_setupfn(t, y, fy, userdata): t_root, np.transpose(sol.roots.y), termination, + model=model, + inputs=inputs_dict, ) else: raise pybamm.SolverError(sol.message) diff --git a/pybamm/solvers/scipy_solver.py b/pybamm/solvers/scipy_solver.py index eb5c7a5ba2..07010ba8ee 100644 --- a/pybamm/solvers/scipy_solver.py +++ b/pybamm/solvers/scipy_solver.py @@ -23,8 +23,11 @@ class ScipySolver(pybamm.BaseSolver): Any options to pass to the solver. Please consult `SciPy documentation `_ for details. - sensitivity : bool, optional - Whether to explicitly formulate and solve the forward sensitivity equations. + sensitivity : str, optional + Whether (and how) to calculate sensitivities when solving. Options are: + + - None: no sensitivities + - "explicit forward": explicitly formulate the sensitivity equations. \ See :class:`pybamm.BaseSolver` """ diff --git a/pybamm/solvers/solution.py b/pybamm/solvers/solution.py index 58b3f90b10..0f0ea847e5 100644 --- a/pybamm/solvers/solution.py +++ b/pybamm/solvers/solution.py @@ -57,9 +57,9 @@ def __init__( if isinstance(y, casadi.DM): y = y.full() - # if model or inputs are None, initialize empty, to be populated later - self.inputs = inputs or pybamm.FuzzyDict() - self._model = model or pybamm.BaseModel() + # if inputs are None, initialize empty, to be populated later + inputs = inputs or pybamm.FuzzyDict() + self.set_inputs(inputs) # If the model has been provided, split up y into solution and sensitivity # Don't do this if the sensitivity equations have not been computed (i.e. if @@ -70,6 +70,7 @@ def __init__( model is None or isinstance(y, casadi.Function) or model.len_rhs_and_alg == y.shape[0] + or model.len_rhs_and_alg == 0 # for the dummy solver ): self._y = y self.sensitivity = {} @@ -129,6 +130,8 @@ def __init__( start = end self.sensitivity = sensitivity + model = model or pybamm.BaseModel() + self.set_model(model) self._t_event = t_event self._y_event = y_event self._termination = termination @@ -163,10 +166,8 @@ def model(self): "Model used for solution" return self._model - @model.setter - def model(self, value): + def set_model(self, value): "Updates the model" - assert isinstance(value, pybamm.BaseModel) self._model = value @property @@ -174,28 +175,19 @@ def inputs(self): "Values of the inputs" return self._inputs - @inputs.setter - def inputs(self, inputs): + def set_inputs(self, inputs): "Updates the input values" - # If there are symbolic inputs, just store them as given - if any(isinstance(v, casadi.MX) for v in inputs.values()): - self.has_symbolic_inputs = True - self._inputs = inputs - # Otherwise, make them the same size as the time vector - else: - self.has_symbolic_inputs = False - self._inputs = {} - for name, inp in inputs.items(): - # Convert number to vector of the right shape - if isinstance(inp, numbers.Number): - inp = inp * np.ones((1, len(self.t))) - # Tile a vector - else: - if inp.ndim == 1: - inp = np.tile(inp, (len(self.t), 1)).T - else: - inp = np.tile(inp, len(self.t)) - self._inputs[name] = inp + self._inputs = {} + for name, inp in inputs.items(): + # Convert number to vector of the right shape + if isinstance(inp, numbers.Number): + inp = inp * np.ones((1, len(self.t))) + # Otherwise, tile a vector + elif inp.ndim == 1: + inp = np.tile(inp, (len(self.t), 1)).T + elif inp.shape[1] != len(self.t): + inp = np.tile(inp, len(self.t)) + self._inputs[name] = inp @property def t_event(self): @@ -434,6 +426,6 @@ def append(self, solution, start_index=1, create_sub_solutions=False): solution.termination, copy_this=solution, model=self.model, - inputs=copy.copy(self.inputs), + inputs=copy.copy(solution.inputs), ) ) diff --git a/tests/unit/test_solvers/test_algebraic_solver.py b/tests/unit/test_solvers/test_algebraic_solver.py index 4e7d660b55..adb52acdeb 100644 --- a/tests/unit/test_solvers/test_algebraic_solver.py +++ b/tests/unit/test_solvers/test_algebraic_solver.py @@ -44,6 +44,7 @@ class Model: timescale_eval = 1 jac_algebraic_eval = None convert_to_format = "python" + len_rhs_and_alg = 1 def algebraic_eval(self, t, y, inputs): return y + 2 @@ -66,6 +67,7 @@ class Model: timescale_eval = 1 jac_algebraic_eval = None convert_to_format = "casadi" + len_rhs_and_alg = 1 def algebraic_eval(self, t, y, inputs): # algebraic equation has no real root @@ -95,6 +97,7 @@ class Model: rhs = {} timescale_eval = 1 convert_to_format = "python" + len_rhs_and_alg = 2 def algebraic_eval(self, t, y, inputs): return A @ y - b diff --git a/tests/unit/test_solvers/test_base_solver.py b/tests/unit/test_solvers/test_base_solver.py index 9d60ebd16f..605aa089c7 100644 --- a/tests/unit/test_solvers/test_base_solver.py +++ b/tests/unit/test_solvers/test_base_solver.py @@ -119,6 +119,7 @@ def __init__(self): ) self.convert_to_format = "casadi" self.bounds = (np.array([-np.inf]), np.array([np.inf])) + self.len_rhs_and_alg = 1 def rhs_eval(self, t, y, inputs): return np.array([]) @@ -154,6 +155,8 @@ def __init__(self): ) self.convert_to_format = "casadi" self.bounds = (-np.inf * np.ones(4), np.inf * np.ones(4)) + self.len_rhs = 1 + self.len_rhs_and_alg = 4 def rhs_eval(self, t, y, inputs): return y[0:1] diff --git a/tests/unit/test_solvers/test_scikits_solvers.py b/tests/unit/test_solvers/test_scikits_solvers.py index 6ab8fe2dcb..70a362959c 100644 --- a/tests/unit/test_solvers/test_scikits_solvers.py +++ b/tests/unit/test_solvers/test_scikits_solvers.py @@ -97,6 +97,7 @@ class Model: terminate_events_eval = [] timescale_eval = 1 convert_to_format = "python" + len_rhs_and_alg = 2 def residuals_eval(self, t, y, ydot, inputs): return np.array( diff --git a/tests/unit/test_solvers/test_scipy_solver.py b/tests/unit/test_solvers/test_scipy_solver.py index 87dd968df0..f22ed5873b 100644 --- a/tests/unit/test_solvers/test_scipy_solver.py +++ b/tests/unit/test_solvers/test_scipy_solver.py @@ -360,7 +360,9 @@ def test_solve_sensitivity_scalar_var_scalar_input(self): # Solve # Make sure that passing in extra options works - solver = pybamm.ScipySolver(rtol=1e-10, atol=1e-10, sensitivity=True) + solver = pybamm.ScipySolver( + rtol=1e-10, atol=1e-10, sensitivity="explicit forward" + ) t_eval = np.linspace(0, 1, 80) solution = solver.solve(model, t_eval, inputs={"p": 0.1}) np.testing.assert_array_equal(solution.t, t_eval) @@ -393,7 +395,9 @@ def test_solve_sensitivity_scalar_var_scalar_input(self): # Solve # Make sure that passing in extra options works - solver = pybamm.ScipySolver(rtol=1e-10, atol=1e-10, sensitivity=True) + solver = pybamm.ScipySolver( + rtol=1e-10, atol=1e-10, sensitivity="explicit forward" + ) t_eval = np.linspace(0, 1, 80) solution = solver.solve( model, t_eval, inputs={"p": 0.1, "q": 2, "r": -1, "s": 0.5} @@ -462,7 +466,7 @@ def test_solve_sensitivity_vector_var_scalar_input(self): n = disc.mesh["negative electrode"].npts # Solve - scalar input - solver = pybamm.ScipySolver(sensitivity=True) + solver = pybamm.ScipySolver(sensitivity="explicit forward") t_eval = np.linspace(0, 1) solution = solver.solve(model, t_eval, inputs={"param": 7}) np.testing.assert_array_almost_equal( @@ -493,7 +497,9 @@ def test_solve_sensitivity_vector_var_scalar_input(self): # Solve # Make sure that passing in extra options works - solver = pybamm.ScipySolver(rtol=1e-10, atol=1e-10, sensitivity=True) + solver = pybamm.ScipySolver( + rtol=1e-10, atol=1e-10, sensitivity="explicit forward" + ) t_eval = np.linspace(0, 1, 80) solution = solver.solve( model, t_eval, inputs={"p": 0.1, "q": 2, "r": -1, "s": 0.5} @@ -568,7 +574,9 @@ def test_solve_sensitivity_scalar_var_vector_input(self): n = disc.mesh["negative electrode"].npts # Solve - constant input - solver = pybamm.ScipySolver(rtol=1e-10, atol=1e-10, sensitivity=True) + solver = pybamm.ScipySolver( + rtol=1e-10, atol=1e-10, sensitivity="explicit forward" + ) t_eval = np.linspace(0, 1) solution = solver.solve(model, t_eval, inputs={"param": 7 * np.ones(n)}) l_n = mesh["negative electrode"].edges[-1] @@ -589,7 +597,9 @@ def test_solve_sensitivity_scalar_var_vector_input(self): ) # Solve - linspace input - solver = pybamm.ScipySolver(rtol=1e-10, atol=1e-10, sensitivity=True) + solver = pybamm.ScipySolver( + rtol=1e-10, atol=1e-10, sensitivity="explicit forward" + ) t_eval = np.linspace(0, 1) p_eval = np.linspace(1, 2, n) solution = solver.solve(model, t_eval, inputs={"param": p_eval}) diff --git a/tests/unit/test_solvers/test_solution.py b/tests/unit/test_solvers/test_solution.py index 65e1c223cb..686eaa0b10 100644 --- a/tests/unit/test_solvers/test_solution.py +++ b/tests/unit/test_solvers/test_solution.py @@ -29,17 +29,16 @@ def test_append(self): # Set up first solution t1 = np.linspace(0, 1) y1 = np.tile(t1, (20, 1)) - sol1 = pybamm.Solution(t1, y1) + model = pybamm.BaseModel() + model.len_rhs_and_alg = 20 + sol1 = pybamm.Solution(t1, y1, model=model, inputs={"a": 1}) sol1.solve_time = 1.5 - sol1.model = pybamm.BaseModel() - sol1.inputs = {"a": 1} # Set up second solution t2 = np.linspace(1, 2) y2 = np.tile(t2, (20, 1)) - sol2 = pybamm.Solution(t2, y2) + sol2 = pybamm.Solution(t2, y2, model=model, inputs={"a": 2}) sol2.solve_time = 1 - sol2.inputs = {"a": 2} sol1.append(sol2, create_sub_solutions=True) # Test @@ -98,6 +97,7 @@ def test_getitem(self): def test_save(self): model = pybamm.BaseModel() + model.length_scales = {"negative electrode": 1} # create both 1D and 2D variables c = pybamm.Variable("c") d = pybamm.Variable("d", domain="negative electrode") @@ -138,9 +138,7 @@ def test_save(self): np.testing.assert_array_almost_equal(df["2c"], solution.data["2c"]) # raise error if format is unknown - with self.assertRaisesRegex( - ValueError, "format 'wrong_format' not recognised" - ): + with self.assertRaisesRegex(ValueError, "format 'wrong_format' not recognised"): solution.save_data("test.csv", to_format="wrong_format") # test save whole solution From 2854ce5e78a1d60414ab05113fb4080bbb6a0887 Mon Sep 17 00:00:00 2001 From: Valentin Sulzer Date: Mon, 28 Dec 2020 15:06:57 +0100 Subject: [PATCH 14/73] #1100 fixing tests --- pybamm/plotting/quick_plot.py | 2 + pybamm/solvers/casadi_algebraic_solver.py | 7 +- pybamm/solvers/casadi_solver.py | 4 +- pybamm/solvers/solution.py | 5 +- .../test_casadi_algebraic_solver.py | 212 +++++++++--------- tests/unit/test_solvers/test_scipy_solver.py | 26 ++- 6 files changed, 135 insertions(+), 121 deletions(-) diff --git a/pybamm/plotting/quick_plot.py b/pybamm/plotting/quick_plot.py index 3e76654f8a..43613bf72c 100644 --- a/pybamm/plotting/quick_plot.py +++ b/pybamm/plotting/quick_plot.py @@ -199,6 +199,8 @@ def __init__( "Electrolyte potential [V]", "Terminal voltage [V]", ] + else: + raise NotImplementedError(models) # Prepare dictionary of variables # output_variables is a list of strings or lists, e.g. diff --git a/pybamm/solvers/casadi_algebraic_solver.py b/pybamm/solvers/casadi_algebraic_solver.py index 68559de075..13b92520a3 100644 --- a/pybamm/solvers/casadi_algebraic_solver.py +++ b/pybamm/solvers/casadi_algebraic_solver.py @@ -63,7 +63,6 @@ def _integrate(self, model, t_eval, inputs=None): inputs : dict, optional Any input parameters to pass to the model when solving. """ - # Record whether there are any symbolic inputs inputs_dict = inputs or {} # Create casadi objects for the root-finder inputs = casadi.vertcat(*[v for v in inputs_dict.values()]) @@ -74,12 +73,14 @@ def _integrate(self, model, t_eval, inputs=None): y0 = model.y0 # If y0 already satisfies the tolerance for all t then keep it - if has_symbolic_inputs is False and all( + if all( np.all(abs(model.casadi_algebraic(t, y0, inputs).full()) < self.tol) for t in t_eval ): pybamm.logger.debug("Keeping same solution at all times") - return pybamm.Solution(t_eval, y0, termination="success") + return pybamm.Solution( + t_eval, y0, termination="success", model=model, inputs=inputs_dict + ) # 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 diff --git a/pybamm/solvers/casadi_solver.py b/pybamm/solvers/casadi_solver.py index c5efa2622d..48388fe09a 100644 --- a/pybamm/solvers/casadi_solver.py +++ b/pybamm/solvers/casadi_solver.py @@ -456,7 +456,7 @@ def _run_integrator(self, model, y0, inputs_dict, t_eval): ) integration_time = timer.time() y_sol = casadi.vertcat(sol["xf"], sol["zf"]) - sol = pybamm.Solution(t_eval, y_sol) + sol = pybamm.Solution(t_eval, y_sol, model=model, inputs=inputs_dict) sol.integration_time = integration_time return sol else: @@ -489,7 +489,7 @@ def _run_integrator(self, model, y0, inputs_dict, t_eval): # Save the solution, can just reuse and change the inputs self.y_sols[model] = y_sol - sol = pybamm.Solution(t_eval, y_sol) + sol = pybamm.Solution(t_eval, y_sol, model=model, inputs=inputs_dict) sol.integration_time = integration_time return sol except RuntimeError as e: diff --git a/pybamm/solvers/solution.py b/pybamm/solvers/solution.py index e07d69806d..ee2279ae38 100644 --- a/pybamm/solvers/solution.py +++ b/pybamm/solvers/solution.py @@ -129,8 +129,7 @@ def __init__( start = end self.sensitivity = sensitivity - model = model or pybamm.BaseModel() - self.set_model(model) + self.model = model self._t_event = t_event self._y_event = y_event self._termination = termination @@ -230,6 +229,8 @@ def set_inputs(self, inputs): inp = inp * np.ones((1, len(self.t))) # Tile a vector else: + if inp.ndim == 1: + inp = inp[:, np.newaxis] inp = np.tile(inp, len(self.t)) self._inputs[name] = inp self._all_inputs_as_MX_dict = {} diff --git a/tests/unit/test_solvers/test_casadi_algebraic_solver.py b/tests/unit/test_solvers/test_casadi_algebraic_solver.py index eb1260a48b..759bacaaaf 100644 --- a/tests/unit/test_solvers/test_casadi_algebraic_solver.py +++ b/tests/unit/test_solvers/test_casadi_algebraic_solver.py @@ -175,112 +175,112 @@ def test_solve_with_symbolic_input(self): np.testing.assert_array_equal(solution["var"].sensitivity["param"], -1) np.testing.assert_array_equal(solution["var"].sensitivity["all"], -1) - def test_least_squares_fit(self): - # Simple system: a single algebraic equation - var = pybamm.Variable("var", domain="negative electrode") - model = pybamm.BaseModel() - # Set length scale to avoid warning - model.length_scales = {"negative electrode": 1} - - p = pybamm.InputParameter("p") - q = pybamm.InputParameter("q") - model.algebraic = {var: (var - p)} - model.initial_conditions = {var: 3} - model.variables = {"objective": (var - q) ** 2 + (p - 3) ** 2} - - # create discretisation - disc = tests.get_discretisation_for_testing() - disc.process_model(model) - - # Solve - solver = pybamm.CasadiAlgebraicSolver(sensitivity="casadi") - - def objective(x): - solution = solver.solve(model, [0], inputs={"p": x[0], "q": x[1]}) - return solution["objective"].data.flatten() - - # without jacobian - lsq_sol = least_squares(objective, [2, 2], method="lm") - np.testing.assert_array_almost_equal(lsq_sol.x, [3, 3], decimal=3) - - def jac(x): - solution = solver.solve(model, [0], inputs={"p": x[0], "q": x[1]}) - return solution["objective"].sensitivity["all"] - - # with jacobian - lsq_sol = least_squares(objective, [2, 2], jac=jac, method="lm") - np.testing.assert_array_almost_equal(lsq_sol.x, [3, 3], decimal=3) - - def test_solve_with_symbolic_input_vector_variable_scalar_input(self): - var = pybamm.Variable("var", "negative electrode") - model = pybamm.BaseModel() - # Set length scale to avoid warning - model.length_scales = {"negative electrode": 1} - param = pybamm.InputParameter("param") - model.algebraic = {var: var + param} - model.initial_conditions = {var: 2} - model.variables = {"var": var} - - # create discretisation - disc = tests.get_discretisation_for_testing() - disc.process_model(model) - - # Solve - scalar input - solver = pybamm.CasadiAlgebraicSolver(sensitivity="casadi") - solution = solver.solve(model, [0], inputs={"param": 7}) - np.testing.assert_array_equal(solution["var"].data, -7) - solution = solver.solve(model, [0], inputs={"param": 3}) - np.testing.assert_array_equal(solution["var"].data, -3) - np.testing.assert_array_equal(solution["var"].sensitivity["param"], -1) - - def test_solve_with_symbolic_input_vector_variable_vector_input(self): - var = pybamm.Variable("var", "negative electrode") - model = pybamm.BaseModel() - # Set length scale to avoid warning - model.length_scales = {"negative electrode": 1} - param = pybamm.InputParameter("param", "negative electrode") - model.algebraic = {var: var + param} - model.initial_conditions = {var: 2} - model.variables = {"var": var} - - # create discretisation - disc = tests.get_discretisation_for_testing() - disc.process_model(model) - n = disc.mesh["negative electrode"].npts - - # Solve - vector input - solver = pybamm.CasadiAlgebraicSolver(sensitivity="casadi") - solution = solver.solve(model, [0], inputs={"param": 3 * np.ones(n)}) - - np.testing.assert_array_almost_equal(solution["var"].data, -3) - np.testing.assert_array_almost_equal( - solution["var"].sensitivity["param"], -np.eye(40) - ) - - p = np.linspace(0, 1, n)[:, np.newaxis] - solution = solver.solve(model, [0], inputs={"param": 2 * p}) - np.testing.assert_array_almost_equal(solution["var"].data, -2 * p) - np.testing.assert_array_almost_equal( - solution["var"].sensitivity["param"], -np.eye(40) - ) - - def test_solve_with_symbolic_input_in_initial_conditions(self): - # Simple system: a single algebraic equation - var = pybamm.Variable("var") - model = pybamm.BaseModel() - model.algebraic = {var: var + 2} - model.initial_conditions = {var: pybamm.InputParameter("param")} - model.variables = {"var": var} - - # create discretisation - disc = pybamm.Discretisation() - disc.process_model(model) - - # Solve - solver = pybamm.CasadiAlgebraicSolver(sensitivity="casadi") - solution = solver.solve(model, [0], inputs={"param": 7}) - np.testing.assert_array_equal(solution["var"].data, -2) - np.testing.assert_array_equal(solution["var"].sensitivity["param"], 0) + # def test_least_squares_fit(self): + # # Simple system: a single algebraic equation + # var = pybamm.Variable("var", domain="negative electrode") + # model = pybamm.BaseModel() + # # Set length scale to avoid warning + # model.length_scales = {"negative electrode": 1} + + # p = pybamm.InputParameter("p") + # q = pybamm.InputParameter("q") + # model.algebraic = {var: (var - p)} + # model.initial_conditions = {var: 3} + # model.variables = {"objective": (var - q) ** 2 + (p - 3) ** 2} + + # # create discretisation + # disc = tests.get_discretisation_for_testing() + # disc.process_model(model) + + # # Solve + # solver = pybamm.CasadiAlgebraicSolver(sensitivity="casadi") + + # def objective(x): + # solution = solver.solve(model, [0], inputs={"p": x[0], "q": x[1]}) + # return solution["objective"].data.flatten() + + # # without jacobian + # lsq_sol = least_squares(objective, [2, 2], method="lm") + # np.testing.assert_array_almost_equal(lsq_sol.x, [3, 3], decimal=3) + + # def jac(x): + # solution = solver.solve(model, [0], inputs={"p": x[0], "q": x[1]}) + # return solution["objective"].sensitivity["all"] + + # # with jacobian + # lsq_sol = least_squares(objective, [2, 2], jac=jac, method="lm") + # np.testing.assert_array_almost_equal(lsq_sol.x, [3, 3], decimal=3) + + # def test_solve_with_symbolic_input_vector_variable_scalar_input(self): + # var = pybamm.Variable("var", "negative electrode") + # model = pybamm.BaseModel() + # # Set length scale to avoid warning + # model.length_scales = {"negative electrode": 1} + # param = pybamm.InputParameter("param") + # model.algebraic = {var: var + param} + # model.initial_conditions = {var: 2} + # model.variables = {"var": var} + + # # create discretisation + # disc = tests.get_discretisation_for_testing() + # disc.process_model(model) + + # # Solve - scalar input + # solver = pybamm.CasadiAlgebraicSolver(sensitivity="casadi") + # solution = solver.solve(model, [0], inputs={"param": 7}) + # np.testing.assert_array_equal(solution["var"].data, -7) + # solution = solver.solve(model, [0], inputs={"param": 3}) + # np.testing.assert_array_equal(solution["var"].data, -3) + # np.testing.assert_array_equal(solution["var"].sensitivity["param"], -1) + + # def test_solve_with_symbolic_input_vector_variable_vector_input(self): + # var = pybamm.Variable("var", "negative electrode") + # model = pybamm.BaseModel() + # # Set length scale to avoid warning + # model.length_scales = {"negative electrode": 1} + # param = pybamm.InputParameter("param", "negative electrode") + # model.algebraic = {var: var + param} + # model.initial_conditions = {var: 2} + # model.variables = {"var": var} + + # # create discretisation + # disc = tests.get_discretisation_for_testing() + # disc.process_model(model) + # n = disc.mesh["negative electrode"].npts + + # # Solve - vector input + # solver = pybamm.CasadiAlgebraicSolver(sensitivity="casadi") + # solution = solver.solve(model, [0], inputs={"param": 3 * np.ones(n)}) + + # np.testing.assert_array_almost_equal(solution["var"].data, -3) + # np.testing.assert_array_almost_equal( + # solution["var"].sensitivity["param"], -np.eye(40) + # ) + + # p = np.linspace(0, 1, n)[:, np.newaxis] + # solution = solver.solve(model, [0], inputs={"param": 2 * p}) + # np.testing.assert_array_almost_equal(solution["var"].data, -2 * p) + # np.testing.assert_array_almost_equal( + # solution["var"].sensitivity["param"], -np.eye(40) + # ) + + # def test_solve_with_symbolic_input_in_initial_conditions(self): + # # Simple system: a single algebraic equation + # var = pybamm.Variable("var") + # model = pybamm.BaseModel() + # model.algebraic = {var: var + 2} + # model.initial_conditions = {var: pybamm.InputParameter("param")} + # model.variables = {"var": var} + + # # create discretisation + # disc = pybamm.Discretisation() + # disc.process_model(model) + + # # Solve + # solver = pybamm.CasadiAlgebraicSolver(sensitivity="casadi") + # solution = solver.solve(model, [0], inputs={"param": 7}) + # np.testing.assert_array_equal(solution["var"].data, -2) + # np.testing.assert_array_equal(solution["var"].sensitivity["param"], 0) if __name__ == "__main__": diff --git a/tests/unit/test_solvers/test_scipy_solver.py b/tests/unit/test_solvers/test_scipy_solver.py index 3aee337011..28acce7350 100644 --- a/tests/unit/test_solvers/test_scipy_solver.py +++ b/tests/unit/test_solvers/test_scipy_solver.py @@ -402,10 +402,12 @@ def test_solve_sensitivity_scalar_var_scalar_input(self): ) np.testing.assert_allclose(solution.y[0], -1 + 0.2 * solution.t) np.testing.assert_allclose( - solution.sensitivity["p"], (2 * solution.t)[:, np.newaxis], + solution.sensitivity["p"], + (2 * solution.t)[:, np.newaxis], ) np.testing.assert_allclose( - solution.sensitivity["q"], (0.1 * solution.t)[:, np.newaxis], + solution.sensitivity["q"], + (0.1 * solution.t)[:, np.newaxis], ) np.testing.assert_allclose(solution.sensitivity["r"], 1) np.testing.assert_allclose(solution.sensitivity["s"], 0) @@ -468,7 +470,9 @@ def test_solve_sensitivity_vector_var_scalar_input(self): t_eval = np.linspace(0, 1) solution = solver.solve(model, t_eval, inputs={"param": 7}) np.testing.assert_array_almost_equal( - solution["var"].data, np.tile(2 * np.exp(-7 * t_eval), (n, 1)), decimal=4, + solution["var"].data, + np.tile(2 * np.exp(-7 * t_eval), (n, 1)), + decimal=4, ) np.testing.assert_array_almost_equal( solution["var"].sensitivity["param"], @@ -504,10 +508,12 @@ def test_solve_sensitivity_vector_var_scalar_input(self): ) np.testing.assert_allclose(solution.y, np.tile(-1 + 0.2 * solution.t, (n, 1))) np.testing.assert_allclose( - solution.sensitivity["p"], np.repeat(2 * solution.t, n)[:, np.newaxis], + solution.sensitivity["p"], + np.repeat(2 * solution.t, n)[:, np.newaxis], ) np.testing.assert_allclose( - solution.sensitivity["q"], np.repeat(0.1 * solution.t, n)[:, np.newaxis], + solution.sensitivity["q"], + np.repeat(0.1 * solution.t, n)[:, np.newaxis], ) np.testing.assert_allclose(solution.sensitivity["r"], 1) np.testing.assert_allclose(solution.sensitivity["s"], 0) @@ -550,7 +556,7 @@ def test_solve_sensitivity_vector_var_scalar_input(self): ), ) - def test_solve_sensitivity_scalar_var_vector_input(self): + def test_solve_sensitivity_vector_var_vector_input(self): var = pybamm.Variable("var", "negative electrode") model = pybamm.BaseModel() # Set length scales to avoid warning @@ -579,7 +585,9 @@ def test_solve_sensitivity_scalar_var_vector_input(self): solution = solver.solve(model, t_eval, inputs={"param": 7 * np.ones(n)}) l_n = mesh["negative electrode"].edges[-1] np.testing.assert_array_almost_equal( - solution["var"].data, np.tile(2 * np.exp(-7 * t_eval), (n, 1)), decimal=4, + solution["var"].data, + np.tile(2 * np.exp(-7 * t_eval), (n, 1)), + decimal=4, ) np.testing.assert_array_almost_equal( @@ -587,7 +595,9 @@ def test_solve_sensitivity_scalar_var_vector_input(self): np.vstack([np.eye(n) * -2 * t * np.exp(-7 * t) for t in t_eval]), ) np.testing.assert_array_almost_equal( - solution["integral of var"].data, 2 * np.exp(-7 * t_eval) * l_n, decimal=4, + solution["integral of var"].data, + 2 * np.exp(-7 * t_eval) * l_n, + decimal=4, ) np.testing.assert_array_almost_equal( solution["integral of var"].sensitivity["param"], From e70e05750de29461b6a245142346e1e2ea60465f Mon Sep 17 00:00:00 2001 From: Valentin Sulzer Date: Mon, 28 Dec 2020 17:21:53 +0100 Subject: [PATCH 15/73] #1100 fixed some solver tests --- pybamm/solvers/casadi_algebraic_solver.py | 2 +- pybamm/solvers/processed_variable.py | 56 ++--- pybamm/solvers/solution.py | 53 +++-- .../test_solvers/test_algebraic_solver.py | 6 +- .../test_casadi_algebraic_solver.py | 212 +++++++++--------- .../test_solvers/test_processed_variable.py | 151 ++++++++++--- 6 files changed, 277 insertions(+), 203 deletions(-) diff --git a/pybamm/solvers/casadi_algebraic_solver.py b/pybamm/solvers/casadi_algebraic_solver.py index 13b92520a3..8f6119dd9a 100644 --- a/pybamm/solvers/casadi_algebraic_solver.py +++ b/pybamm/solvers/casadi_algebraic_solver.py @@ -73,7 +73,7 @@ def _integrate(self, model, t_eval, inputs=None): y0 = model.y0 # If y0 already satisfies the tolerance for all t then keep it - if all( + if self.sensitivity != "casadi" and all( np.all(abs(model.casadi_algebraic(t, y0, inputs).full()) < self.tol) for t in t_eval ): diff --git a/pybamm/solvers/processed_variable.py b/pybamm/solvers/processed_variable.py index e7168da0b5..b037bce8e2 100644 --- a/pybamm/solvers/processed_variable.py +++ b/pybamm/solvers/processed_variable.py @@ -54,48 +54,21 @@ def __init__(self, base_variable, base_variable_casadi, solution, warn=True): self.base_variable = base_variable self.base_variable_casadi = base_variable_casadi self.t_sol = solution.t - self.u_sol = solution.y self.mesh = base_variable.mesh - self.inputs = solution.inputs self.domain = base_variable.domain self.auxiliary_domains = base_variable.auxiliary_domains self.warn = warn + self.inputs = solution.inputs + self.symbolic_inputs = solution._symbolic_inputs + + self.u_sol = solution.y + self.y_sym = solution._y_sym + # Sensitivity starts off uninitialized, only set when called self._sensitivity = None self.solution_sensitivity = solution.sensitivity - # Special case: symbolic solution, with casadi - if isinstance(solution.y, casadi.Function): - # Evaluate solution at specific inputs value - inputs_stacked = casadi.vertcat(*solution.inputs.values()) - self.u_sol = solution.y(inputs_stacked).full() - # Convert variable to casadi - t_MX = casadi.MX.sym("t") - y_MX = casadi.MX.sym("y", self.u_sol.shape[0]) - # Make all inputs symbolic first for converting to casadi - symbolic_inputs_dict = { - name: casadi.MX.sym(name, value.shape[0]) - for name, value in solution.inputs.items() - } - - # The symbolic_inputs will be used for sensitivity - symbolic_inputs = casadi.vertcat(*symbolic_inputs_dict.values()) - var_casadi = base_variable.to_casadi( - t_MX, y_MX, inputs=symbolic_inputs_dict - ) - self.base_variable_sym = casadi.Function( - "variable", [t_MX, y_MX, symbolic_inputs], [var_casadi] - ) - # Store symbolic inputs for sensitivity - self.symbolic_inputs = symbolic_inputs - self.y_sym = solution.y(symbolic_inputs) - else: - self.u_sol = solution.y - self.base_variable_sym = None - self.symbolic_inputs = None - self.y_sym = None - # Set timescale self.timescale = solution.timescale_eval self.t_pts = self.t_sol * self.timescale @@ -565,17 +538,16 @@ def sensitivity(self): return {} # Otherwise initialise and return sensitivity if self._sensitivity is None: - # Check that we can compute sensitivities - if self.base_variable_sym is None and self.solution_sensitivity == {}: + if self.solution_sensitivity != {}: + self.initialise_sensitivity_explicit_forward() + elif self.y_sym is not None: + self.initialise_sensitivity_casadi() + else: raise ValueError( "Cannot compute sensitivities. The 'sensitivity' argument of the " "solver should be changed from 'None' to allow sensitivity " "calculations. Check solver documentation for details." ) - if self.base_variable_sym is None: - self.initialise_sensitivity_explicit_forward() - else: - self.initialise_sensitivity_casadi() return self._sensitivity def initialise_sensitivity_explicit_forward(self): @@ -639,7 +611,7 @@ def initialise_0D_symbolic(): for idx in range(len(self.t_sol)): t = self.t_sol[idx] u = self.y_sym[:, idx] - next_entries = self.base_variable_sym(t, u, self.symbolic_inputs) + next_entries = self.base_variable_casadi(t, u, self.symbolic_inputs) if idx == 0: entries = next_entries else: @@ -653,7 +625,7 @@ def initialise_1D_symbolic(): for idx in range(len(self.t_sol)): t = self.t_sol[idx] u = self.y_sym[:, idx] - next_entries = self.base_variable_sym(t, u, self.symbolic_inputs) + next_entries = self.base_variable_casadi(t, u, self.symbolic_inputs) if idx == 0: entries = next_entries else: @@ -662,7 +634,7 @@ def initialise_1D_symbolic(): return entries inputs_stacked = casadi.vertcat(*self.inputs.values()) - self.base_eval = self.base_variable_sym( + self.base_eval = self.base_variable_casadi( self.t_sol[0], self.u_sol[:, 0], inputs_stacked ) if ( diff --git a/pybamm/solvers/solution.py b/pybamm/solvers/solution.py index ee2279ae38..d9e7dff643 100644 --- a/pybamm/solvers/solution.py +++ b/pybamm/solvers/solution.py @@ -53,12 +53,7 @@ def __init__( inputs=None, ): self.t = t - if isinstance(y, casadi.DM): - y = y.full() - - # if inputs are None, initialize empty, to be populated later - inputs = inputs or pybamm.FuzzyDict() - self.set_inputs(inputs) + self.inputs = inputs # If the model has been provided, split up y into solution and sensitivity # Don't do this if the sensitivity equations have not been computed (i.e. if @@ -107,8 +102,12 @@ def __init__( # tn_xn_p0, tn_xn_p1, ..., tn_xn_pn # 1, Extract rhs and alg sensitivities and reshape into 3D matrices # with shape (n_p, n_states, n_t) - ode_sens = y[n_rhs:len_rhs_and_sens, :].reshape(n_p, n_rhs, n_t) - alg_sens = y[len_rhs_and_sens + n_alg :, :].reshape(n_p, n_alg, n_t) + if isinstance(y, casadi.DM): + y_full = y.full() + else: + y_full = y + ode_sens = y_full[n_rhs:len_rhs_and_sens, :].reshape(n_p, n_rhs, n_t) + alg_sens = y_full[len_rhs_and_sens + n_alg :, :].reshape(n_p, n_alg, n_t) # 2. Concatenate into a single 3D matrix with shape (n_p, n_states, n_t) # i.e. along first axis full_sens_matrix = np.concatenate([ode_sens, alg_sens], axis=1) @@ -163,8 +162,16 @@ def y(self): @y.setter def y(self, y): - self._y = y - self._y_MX = casadi.MX.sym("y", y.shape[0]) + if isinstance(y, casadi.Function): + self._y_fn = None + inputs_stacked = casadi.vertcat(*self.inputs.values()) + self._y = y(inputs_stacked) + self._y_sym = y(self._symbolic_inputs) + else: + self._y = y + self._y_fn = None + self._y_sym = None + self._y_MX = casadi.MX.sym("y", self._y.shape[0]) @property def model(self): @@ -196,8 +203,12 @@ def inputs(self): "Values of the inputs" return self._inputs - def set_inputs(self, inputs): + @inputs.setter + def inputs(self, inputs): "Updates the input values" + # if inputs are None, initialize empty, to be populated later + inputs = inputs or pybamm.FuzzyDict() + # self._inputs = {} # for name, inp in inputs.items(): # # Convert number to vector of the right shape @@ -233,13 +244,13 @@ def set_inputs(self, inputs): inp = inp[:, np.newaxis] inp = np.tile(inp, len(self.t)) self._inputs[name] = inp - self._all_inputs_as_MX_dict = {} - for key, value in self._inputs.items(): - self._all_inputs_as_MX_dict[key] = casadi.MX.sym("input", value.shape[0]) + self._symbolic_inputs_dict = { + name: casadi.MX.sym(name, value.shape[0]) + for name, value in self.inputs.items() + } - self._all_inputs_as_MX = casadi.vertcat( - *[p for p in self._all_inputs_as_MX_dict.values()] - ) + # The symbolic_inputs will be used for sensitivity + self._symbolic_inputs = casadi.vertcat(*self._symbolic_inputs_dict.values()) @property def t_event(self): @@ -298,12 +309,12 @@ def update(self, variables): # Convert variable to casadi # Make all inputs symbolic first for converting to casadi var_sym = var_pybamm.to_casadi( - self._t_MX, self._y_MX, inputs=self._all_inputs_as_MX_dict + self._t_MX, self._y_MX, inputs=self._symbolic_inputs_dict ) var_casadi = casadi.Function( "variable", - [self._t_MX, self._y_MX, self._all_inputs_as_MX], + [self._t_MX, self._y_MX, self._symbolic_inputs], [var_sym], ) self.model._variables_casadi[key] = var_casadi @@ -359,8 +370,8 @@ def clear_casadi_attributes(self): "Remove casadi objects for pickling, will be computed again automatically" self._t_MX = None self._y_MX = None - self._all_inputs_as_MX = None - self._all_inputs_as_MX_dict = None + self._symbolic_inputs = None + self._symbolic_inputs_dict = None def save(self, filename): """Save the whole solution using pickle""" diff --git a/tests/unit/test_solvers/test_algebraic_solver.py b/tests/unit/test_solvers/test_algebraic_solver.py index adb52acdeb..3d1c232983 100644 --- a/tests/unit/test_solvers/test_algebraic_solver.py +++ b/tests/unit/test_solvers/test_algebraic_solver.py @@ -38,7 +38,7 @@ def test_wrong_solver(self): def test_simple_root_find(self): # Simple system: a single algebraic equation - class Model: + class Model(pybamm.BaseModel): y0 = np.array([2]) rhs = {} timescale_eval = 1 @@ -61,7 +61,7 @@ def algebraic_eval(self, t, y, inputs): self.assertNotEqual(solution.y, -2) def test_root_find_fail(self): - class Model: + class Model(pybamm.BaseModel): y0 = np.array([2]) rhs = {} timescale_eval = 1 @@ -92,7 +92,7 @@ def test_with_jacobian(self): A = np.array([[4, 3], [1, -1]]) b = np.array([0, 7]) - class Model: + class Model(pybamm.BaseModel): y0 = np.zeros(2) rhs = {} timescale_eval = 1 diff --git a/tests/unit/test_solvers/test_casadi_algebraic_solver.py b/tests/unit/test_solvers/test_casadi_algebraic_solver.py index 759bacaaaf..eb1260a48b 100644 --- a/tests/unit/test_solvers/test_casadi_algebraic_solver.py +++ b/tests/unit/test_solvers/test_casadi_algebraic_solver.py @@ -175,112 +175,112 @@ def test_solve_with_symbolic_input(self): np.testing.assert_array_equal(solution["var"].sensitivity["param"], -1) np.testing.assert_array_equal(solution["var"].sensitivity["all"], -1) - # def test_least_squares_fit(self): - # # Simple system: a single algebraic equation - # var = pybamm.Variable("var", domain="negative electrode") - # model = pybamm.BaseModel() - # # Set length scale to avoid warning - # model.length_scales = {"negative electrode": 1} - - # p = pybamm.InputParameter("p") - # q = pybamm.InputParameter("q") - # model.algebraic = {var: (var - p)} - # model.initial_conditions = {var: 3} - # model.variables = {"objective": (var - q) ** 2 + (p - 3) ** 2} - - # # create discretisation - # disc = tests.get_discretisation_for_testing() - # disc.process_model(model) - - # # Solve - # solver = pybamm.CasadiAlgebraicSolver(sensitivity="casadi") - - # def objective(x): - # solution = solver.solve(model, [0], inputs={"p": x[0], "q": x[1]}) - # return solution["objective"].data.flatten() - - # # without jacobian - # lsq_sol = least_squares(objective, [2, 2], method="lm") - # np.testing.assert_array_almost_equal(lsq_sol.x, [3, 3], decimal=3) - - # def jac(x): - # solution = solver.solve(model, [0], inputs={"p": x[0], "q": x[1]}) - # return solution["objective"].sensitivity["all"] - - # # with jacobian - # lsq_sol = least_squares(objective, [2, 2], jac=jac, method="lm") - # np.testing.assert_array_almost_equal(lsq_sol.x, [3, 3], decimal=3) - - # def test_solve_with_symbolic_input_vector_variable_scalar_input(self): - # var = pybamm.Variable("var", "negative electrode") - # model = pybamm.BaseModel() - # # Set length scale to avoid warning - # model.length_scales = {"negative electrode": 1} - # param = pybamm.InputParameter("param") - # model.algebraic = {var: var + param} - # model.initial_conditions = {var: 2} - # model.variables = {"var": var} - - # # create discretisation - # disc = tests.get_discretisation_for_testing() - # disc.process_model(model) - - # # Solve - scalar input - # solver = pybamm.CasadiAlgebraicSolver(sensitivity="casadi") - # solution = solver.solve(model, [0], inputs={"param": 7}) - # np.testing.assert_array_equal(solution["var"].data, -7) - # solution = solver.solve(model, [0], inputs={"param": 3}) - # np.testing.assert_array_equal(solution["var"].data, -3) - # np.testing.assert_array_equal(solution["var"].sensitivity["param"], -1) - - # def test_solve_with_symbolic_input_vector_variable_vector_input(self): - # var = pybamm.Variable("var", "negative electrode") - # model = pybamm.BaseModel() - # # Set length scale to avoid warning - # model.length_scales = {"negative electrode": 1} - # param = pybamm.InputParameter("param", "negative electrode") - # model.algebraic = {var: var + param} - # model.initial_conditions = {var: 2} - # model.variables = {"var": var} - - # # create discretisation - # disc = tests.get_discretisation_for_testing() - # disc.process_model(model) - # n = disc.mesh["negative electrode"].npts - - # # Solve - vector input - # solver = pybamm.CasadiAlgebraicSolver(sensitivity="casadi") - # solution = solver.solve(model, [0], inputs={"param": 3 * np.ones(n)}) - - # np.testing.assert_array_almost_equal(solution["var"].data, -3) - # np.testing.assert_array_almost_equal( - # solution["var"].sensitivity["param"], -np.eye(40) - # ) - - # p = np.linspace(0, 1, n)[:, np.newaxis] - # solution = solver.solve(model, [0], inputs={"param": 2 * p}) - # np.testing.assert_array_almost_equal(solution["var"].data, -2 * p) - # np.testing.assert_array_almost_equal( - # solution["var"].sensitivity["param"], -np.eye(40) - # ) - - # def test_solve_with_symbolic_input_in_initial_conditions(self): - # # Simple system: a single algebraic equation - # var = pybamm.Variable("var") - # model = pybamm.BaseModel() - # model.algebraic = {var: var + 2} - # model.initial_conditions = {var: pybamm.InputParameter("param")} - # model.variables = {"var": var} - - # # create discretisation - # disc = pybamm.Discretisation() - # disc.process_model(model) - - # # Solve - # solver = pybamm.CasadiAlgebraicSolver(sensitivity="casadi") - # solution = solver.solve(model, [0], inputs={"param": 7}) - # np.testing.assert_array_equal(solution["var"].data, -2) - # np.testing.assert_array_equal(solution["var"].sensitivity["param"], 0) + def test_least_squares_fit(self): + # Simple system: a single algebraic equation + var = pybamm.Variable("var", domain="negative electrode") + model = pybamm.BaseModel() + # Set length scale to avoid warning + model.length_scales = {"negative electrode": 1} + + p = pybamm.InputParameter("p") + q = pybamm.InputParameter("q") + model.algebraic = {var: (var - p)} + model.initial_conditions = {var: 3} + model.variables = {"objective": (var - q) ** 2 + (p - 3) ** 2} + + # create discretisation + disc = tests.get_discretisation_for_testing() + disc.process_model(model) + + # Solve + solver = pybamm.CasadiAlgebraicSolver(sensitivity="casadi") + + def objective(x): + solution = solver.solve(model, [0], inputs={"p": x[0], "q": x[1]}) + return solution["objective"].data.flatten() + + # without jacobian + lsq_sol = least_squares(objective, [2, 2], method="lm") + np.testing.assert_array_almost_equal(lsq_sol.x, [3, 3], decimal=3) + + def jac(x): + solution = solver.solve(model, [0], inputs={"p": x[0], "q": x[1]}) + return solution["objective"].sensitivity["all"] + + # with jacobian + lsq_sol = least_squares(objective, [2, 2], jac=jac, method="lm") + np.testing.assert_array_almost_equal(lsq_sol.x, [3, 3], decimal=3) + + def test_solve_with_symbolic_input_vector_variable_scalar_input(self): + var = pybamm.Variable("var", "negative electrode") + model = pybamm.BaseModel() + # Set length scale to avoid warning + model.length_scales = {"negative electrode": 1} + param = pybamm.InputParameter("param") + model.algebraic = {var: var + param} + model.initial_conditions = {var: 2} + model.variables = {"var": var} + + # create discretisation + disc = tests.get_discretisation_for_testing() + disc.process_model(model) + + # Solve - scalar input + solver = pybamm.CasadiAlgebraicSolver(sensitivity="casadi") + solution = solver.solve(model, [0], inputs={"param": 7}) + np.testing.assert_array_equal(solution["var"].data, -7) + solution = solver.solve(model, [0], inputs={"param": 3}) + np.testing.assert_array_equal(solution["var"].data, -3) + np.testing.assert_array_equal(solution["var"].sensitivity["param"], -1) + + def test_solve_with_symbolic_input_vector_variable_vector_input(self): + var = pybamm.Variable("var", "negative electrode") + model = pybamm.BaseModel() + # Set length scale to avoid warning + model.length_scales = {"negative electrode": 1} + param = pybamm.InputParameter("param", "negative electrode") + model.algebraic = {var: var + param} + model.initial_conditions = {var: 2} + model.variables = {"var": var} + + # create discretisation + disc = tests.get_discretisation_for_testing() + disc.process_model(model) + n = disc.mesh["negative electrode"].npts + + # Solve - vector input + solver = pybamm.CasadiAlgebraicSolver(sensitivity="casadi") + solution = solver.solve(model, [0], inputs={"param": 3 * np.ones(n)}) + + np.testing.assert_array_almost_equal(solution["var"].data, -3) + np.testing.assert_array_almost_equal( + solution["var"].sensitivity["param"], -np.eye(40) + ) + + p = np.linspace(0, 1, n)[:, np.newaxis] + solution = solver.solve(model, [0], inputs={"param": 2 * p}) + np.testing.assert_array_almost_equal(solution["var"].data, -2 * p) + np.testing.assert_array_almost_equal( + solution["var"].sensitivity["param"], -np.eye(40) + ) + + def test_solve_with_symbolic_input_in_initial_conditions(self): + # Simple system: a single algebraic equation + var = pybamm.Variable("var") + model = pybamm.BaseModel() + model.algebraic = {var: var + 2} + model.initial_conditions = {var: pybamm.InputParameter("param")} + model.variables = {"var": var} + + # create discretisation + disc = pybamm.Discretisation() + disc.process_model(model) + + # Solve + solver = pybamm.CasadiAlgebraicSolver(sensitivity="casadi") + solution = solver.solve(model, [0], inputs={"param": 7}) + np.testing.assert_array_equal(solution["var"].data, -2) + np.testing.assert_array_equal(solution["var"].sensitivity["param"], 0) if __name__ == "__main__": diff --git a/tests/unit/test_solvers/test_processed_variable.py b/tests/unit/test_solvers/test_processed_variable.py index 153d8a092f..38e8863e73 100644 --- a/tests/unit/test_solvers/test_processed_variable.py +++ b/tests/unit/test_solvers/test_processed_variable.py @@ -37,7 +37,10 @@ def test_processed_variable_0D(self): y_sol = np.array([np.linspace(0, 5)]) var_casadi = to_casadi(var, y_sol) processed_var = pybamm.ProcessedVariable( - var, var_casadi, pybamm.Solution(t_sol, y_sol), warn=False + var, + var_casadi, + pybamm.Solution(t_sol, y_sol, model=pybamm.BaseModel()), + warn=False, ) np.testing.assert_array_equal(processed_var.entries, t_sol * y_sol[0]) @@ -48,7 +51,10 @@ def test_processed_variable_0D(self): y_sol = np.array([1])[:, np.newaxis] var_casadi = to_casadi(var, y_sol) processed_var = pybamm.ProcessedVariable( - var, var_casadi, pybamm.Solution(t_sol, y_sol), warn=False + var, + var_casadi, + pybamm.Solution(t_sol, y_sol, model=pybamm.BaseModel()), + warn=False, ) np.testing.assert_array_equal(processed_var.entries, y_sol[0]) @@ -69,13 +75,19 @@ def test_processed_variable_1D(self): var_casadi = to_casadi(var_sol, y_sol) processed_var = pybamm.ProcessedVariable( - var_sol, var_casadi, pybamm.Solution(t_sol, y_sol), warn=False + var_sol, + var_casadi, + pybamm.Solution(t_sol, y_sol, model=pybamm.BaseModel()), + warn=False, ) np.testing.assert_array_equal(processed_var.entries, y_sol) np.testing.assert_array_equal(processed_var(t_sol, x_sol), y_sol) eqn_casadi = to_casadi(eqn_sol, y_sol) processed_eqn = pybamm.ProcessedVariable( - eqn_sol, eqn_casadi, pybamm.Solution(t_sol, y_sol), warn=False + eqn_sol, + eqn_casadi, + pybamm.Solution(t_sol, y_sol, model=pybamm.BaseModel()), + warn=False, ) np.testing.assert_array_equal( processed_eqn(t_sol, x_sol), t_sol * y_sol + x_sol[:, np.newaxis] @@ -92,7 +104,10 @@ def test_processed_variable_1D(self): x_s_edge.mesh = disc.mesh["separator"] x_s_casadi = to_casadi(x_s_edge, y_sol) processed_x_s_edge = pybamm.ProcessedVariable( - x_s_edge, x_s_casadi, pybamm.Solution(t_sol, y_sol), warn=False + x_s_edge, + x_s_casadi, + pybamm.Solution(t_sol, y_sol, model=pybamm.BaseModel()), + warn=False, ) np.testing.assert_array_equal( x_s_edge.entries[:, 0], processed_x_s_edge.entries[:, 0] @@ -105,7 +120,10 @@ def test_processed_variable_1D(self): y_sol = np.ones_like(x_sol)[:, np.newaxis] eqn_casadi = to_casadi(eqn_sol, y_sol) processed_eqn2 = pybamm.ProcessedVariable( - eqn_sol, eqn_casadi, pybamm.Solution(t_sol, y_sol), warn=False + eqn_sol, + eqn_casadi, + pybamm.Solution(t_sol, y_sol, model=pybamm.BaseModel()), + warn=False, ) np.testing.assert_array_equal( processed_eqn2.entries, y_sol + x_sol[:, np.newaxis] @@ -130,6 +148,7 @@ def test_processed_variable_1D_unknown_domain(self): np.linspace(0, 1, 1), np.zeros((var_pts[x])), "test", + model=pybamm.BaseModel(), ) c = pybamm.StateVector(slice(0, var_pts[x]), domain=["SEI layer"]) @@ -162,7 +181,10 @@ def test_processed_variable_2D_x_r(self): var_casadi = to_casadi(var_sol, y_sol) processed_var = pybamm.ProcessedVariable( - var_sol, var_casadi, pybamm.Solution(t_sol, y_sol), warn=False + var_sol, + var_casadi, + pybamm.Solution(t_sol, y_sol, model=pybamm.BaseModel()), + warn=False, ) np.testing.assert_array_equal( processed_var.entries, @@ -194,7 +216,10 @@ def test_processed_variable_2D_x_z(self): var_casadi = to_casadi(var_sol, y_sol) processed_var = pybamm.ProcessedVariable( - var_sol, var_casadi, pybamm.Solution(t_sol, y_sol), warn=False + var_sol, + var_casadi, + pybamm.Solution(t_sol, y_sol, model=pybamm.BaseModel()), + warn=False, ) np.testing.assert_array_equal( processed_var.entries, @@ -211,7 +236,10 @@ def test_processed_variable_2D_x_z(self): x_s_edge.secondary_mesh = disc.mesh["current collector"] x_s_casadi = to_casadi(x_s_edge, y_sol) processed_x_s_edge = pybamm.ProcessedVariable( - x_s_edge, x_s_casadi, pybamm.Solution(t_sol, y_sol), warn=False + x_s_edge, + x_s_casadi, + pybamm.Solution(t_sol, y_sol, model=pybamm.BaseModel()), + warn=False, ) np.testing.assert_array_equal( x_s_edge.entries.flatten(), processed_x_s_edge.entries[:, :, 0].T.flatten() @@ -242,7 +270,10 @@ def test_processed_variable_2D_space_only(self): var_casadi = to_casadi(var_sol, y_sol) processed_var = pybamm.ProcessedVariable( - var_sol, var_casadi, pybamm.Solution(t_sol, y_sol), warn=False + var_sol, + var_casadi, + pybamm.Solution(t_sol, y_sol, model=pybamm.BaseModel()), + warn=False, ) np.testing.assert_array_equal( processed_var.entries, @@ -263,7 +294,10 @@ def test_processed_variable_2D_scikit(self): var_casadi = to_casadi(var_sol, u_sol) processed_var = pybamm.ProcessedVariable( - var_sol, var_casadi, pybamm.Solution(t_sol, u_sol), warn=False + var_sol, + var_casadi, + pybamm.Solution(t_sol, u_sol, model=pybamm.BaseModel()), + warn=False, ) np.testing.assert_array_equal( processed_var.entries, np.reshape(u_sol, [len(y), len(z), len(t_sol)]) @@ -283,7 +317,10 @@ def test_processed_variable_2D_fixed_t_scikit(self): var_casadi = to_casadi(var_sol, u_sol) processed_var = pybamm.ProcessedVariable( - var_sol, var_casadi, pybamm.Solution(t_sol, u_sol), warn=False + var_sol, + var_casadi, + pybamm.Solution(t_sol, u_sol, model=pybamm.BaseModel()), + warn=False, ) np.testing.assert_array_equal( processed_var.entries, np.reshape(u_sol, [len(y), len(z), len(t_sol)]) @@ -302,7 +339,10 @@ def test_processed_var_0D_interpolation(self): y_sol = np.array([np.linspace(0, 5, 1000)]) var_casadi = to_casadi(var, y_sol) processed_var = pybamm.ProcessedVariable( - var, var_casadi, pybamm.Solution(t_sol, y_sol), warn=False + var, + var_casadi, + pybamm.Solution(t_sol, y_sol, model=pybamm.BaseModel()), + warn=False, ) # vector np.testing.assert_array_equal(processed_var(t_sol), y_sol[0]) @@ -312,7 +352,10 @@ def test_processed_var_0D_interpolation(self): eqn_casadi = to_casadi(eqn, y_sol) processed_eqn = pybamm.ProcessedVariable( - eqn, eqn_casadi, pybamm.Solution(t_sol, y_sol), warn=False + eqn, + eqn_casadi, + pybamm.Solution(t_sol, y_sol, model=pybamm.BaseModel()), + warn=False, ) np.testing.assert_array_equal(processed_eqn(t_sol), t_sol * y_sol[0]) np.testing.assert_array_almost_equal(processed_eqn(0.5), 0.5 * 2.5) @@ -333,7 +376,10 @@ def test_processed_var_0D_fixed_t_interpolation(self): y_sol = np.array([[100]]) eqn_casadi = to_casadi(eqn, y_sol) processed_var = pybamm.ProcessedVariable( - eqn, eqn_casadi, pybamm.Solution(t_sol, y_sol), warn=False + eqn, + eqn_casadi, + pybamm.Solution(t_sol, y_sol, model=pybamm.BaseModel()), + warn=False, ) np.testing.assert_array_equal(processed_var(), 200) @@ -354,7 +400,10 @@ def test_processed_var_1D_interpolation(self): var_casadi = to_casadi(var_sol, y_sol) processed_var = pybamm.ProcessedVariable( - var_sol, var_casadi, pybamm.Solution(t_sol, y_sol), warn=False + var_sol, + var_casadi, + pybamm.Solution(t_sol, y_sol, model=pybamm.BaseModel()), + warn=False, ) # 2 vectors np.testing.assert_array_almost_equal(processed_var(t_sol, x_sol), y_sol) @@ -371,7 +420,10 @@ def test_processed_var_1D_interpolation(self): ) eqn_casadi = to_casadi(eqn_sol, y_sol) processed_eqn = pybamm.ProcessedVariable( - eqn_sol, eqn_casadi, pybamm.Solution(t_sol, y_sol), warn=False + eqn_sol, + eqn_casadi, + pybamm.Solution(t_sol, y_sol, model=pybamm.BaseModel()), + warn=False, ) # 2 vectors np.testing.assert_array_almost_equal( @@ -388,7 +440,10 @@ def test_processed_var_1D_interpolation(self): x_casadi = to_casadi(x_disc, y_sol) processed_x = pybamm.ProcessedVariable( - x_disc, x_casadi, pybamm.Solution(t_sol, y_sol), warn=False + x_disc, + x_casadi, + pybamm.Solution(t_sol, y_sol, model=pybamm.BaseModel()), + warn=False, ) np.testing.assert_array_almost_equal(processed_x(x=x_sol), x_sol[:, np.newaxis]) @@ -399,7 +454,10 @@ def test_processed_var_1D_interpolation(self): r_n.mesh = disc.mesh["negative particle"] r_n_casadi = to_casadi(r_n, y_sol) processed_r_n = pybamm.ProcessedVariable( - r_n, r_n_casadi, pybamm.Solution(t_sol, y_sol), warn=False + r_n, + r_n_casadi, + pybamm.Solution(t_sol, y_sol, model=pybamm.BaseModel()), + warn=False, ) np.testing.assert_array_equal(r_n.entries[:, 0], processed_r_n.entries[:, 0]) np.testing.assert_array_almost_equal( @@ -420,7 +478,10 @@ def test_processed_var_1D_fixed_t_interpolation(self): eqn_casadi = to_casadi(eqn_sol, y_sol) processed_var = pybamm.ProcessedVariable( - eqn_sol, eqn_casadi, pybamm.Solution(t_sol, y_sol), warn=False + eqn_sol, + eqn_casadi, + pybamm.Solution(t_sol, y_sol, model=pybamm.BaseModel()), + warn=False, ) # vector @@ -455,7 +516,10 @@ def test_processed_var_2D_interpolation(self): var_casadi = to_casadi(var_sol, y_sol) processed_var = pybamm.ProcessedVariable( - var_sol, var_casadi, pybamm.Solution(t_sol, y_sol), warn=False + var_sol, + var_casadi, + pybamm.Solution(t_sol, y_sol, model=pybamm.BaseModel()), + warn=False, ) # 3 vectors np.testing.assert_array_equal( @@ -500,7 +564,10 @@ def test_processed_var_2D_interpolation(self): var_casadi = to_casadi(var_sol, y_sol) processed_var = pybamm.ProcessedVariable( - var_sol, var_casadi, pybamm.Solution(t_sol, y_sol), warn=False + var_sol, + var_casadi, + pybamm.Solution(t_sol, y_sol, model=pybamm.BaseModel()), + warn=False, ) # 3 vectors np.testing.assert_array_equal( @@ -532,7 +599,10 @@ def test_processed_var_2D_fixed_t_interpolation(self): var_casadi = to_casadi(var_sol, y_sol) processed_var = pybamm.ProcessedVariable( - var_sol, var_casadi, pybamm.Solution(t_sol, y_sol), warn=False + var_sol, + var_casadi, + pybamm.Solution(t_sol, y_sol, model=pybamm.BaseModel()), + warn=False, ) # 2 vectors np.testing.assert_array_equal(processed_var(x=x_sol, r=r_sol).shape, (10, 40)) @@ -558,7 +628,10 @@ def test_processed_var_2D_secondary_broadcast(self): var_casadi = to_casadi(var_sol, y_sol) processed_var = pybamm.ProcessedVariable( - var_sol, var_casadi, pybamm.Solution(t_sol, y_sol), warn=False + var_sol, + var_casadi, + pybamm.Solution(t_sol, y_sol, model=pybamm.BaseModel()), + warn=False, ) # 3 vectors np.testing.assert_array_equal( @@ -594,7 +667,10 @@ def test_processed_var_2D_secondary_broadcast(self): var_casadi = to_casadi(var_sol, y_sol) processed_var = pybamm.ProcessedVariable( - var_sol, var_casadi, pybamm.Solution(t_sol, y_sol), warn=False + var_sol, + var_casadi, + pybamm.Solution(t_sol, y_sol, model=pybamm.BaseModel()), + warn=False, ) # 3 vectors np.testing.assert_array_equal( @@ -615,7 +691,10 @@ def test_processed_var_2D_scikit_interpolation(self): var_casadi = to_casadi(var_sol, u_sol) processed_var = pybamm.ProcessedVariable( - var_sol, var_casadi, pybamm.Solution(t_sol, u_sol), warn=False + var_sol, + var_casadi, + pybamm.Solution(t_sol, u_sol, model=pybamm.BaseModel()), + warn=False, ) # 3 vectors np.testing.assert_array_equal( @@ -656,7 +735,10 @@ def test_processed_var_2D_fixed_t_scikit_interpolation(self): var_casadi = to_casadi(var_sol, u_sol) processed_var = pybamm.ProcessedVariable( - var_sol, var_casadi, pybamm.Solution(t_sol, u_sol), warn=False + var_sol, + var_casadi, + pybamm.Solution(t_sol, u_sol, model=pybamm.BaseModel()), + warn=False, ) # 2 vectors np.testing.assert_array_equal(processed_var(y=y_sol, z=z_sol).shape, (15, 15)) @@ -725,7 +807,10 @@ def test_call_failure(self): var_casadi = to_casadi(var_sol, y_sol) processed_var = pybamm.ProcessedVariable( - var_sol, var_casadi, pybamm.Solution(t_sol, y_sol), warn=False + var_sol, + var_casadi, + pybamm.Solution(t_sol, y_sol, model=pybamm.BaseModel()), + warn=False, ) with self.assertRaisesRegex(ValueError, "x cannot be None"): processed_var(0) @@ -745,7 +830,10 @@ def test_call_failure(self): var_casadi = to_casadi(var_sol, y_sol) processed_var = pybamm.ProcessedVariable( - var_sol, var_casadi, pybamm.Solution(t_sol, y_sol), warn=False + var_sol, + var_casadi, + pybamm.Solution(t_sol, y_sol, model=pybamm.BaseModel()), + warn=False, ) with self.assertRaisesRegex(ValueError, "r cannot be None"): processed_var(0) @@ -772,7 +860,10 @@ def test_3D_raises_error(self): with self.assertRaisesRegex(NotImplementedError, "Shape not recognized"): pybamm.ProcessedVariable( - var_sol, var_casadi, pybamm.Solution(t_sol, u_sol), warn=False + var_sol, + var_casadi, + pybamm.Solution(t_sol, u_sol, model=pybamm.BaseModel()), + warn=False, ) From f794956689d6aeb10f1136b7f93c451fc5776fd8 Mon Sep 17 00:00:00 2001 From: Valentin Sulzer Date: Wed, 20 Jan 2021 19:17:59 -0500 Subject: [PATCH 16/73] #1100 working on tests --- pybamm/solvers/processed_variable.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pybamm/solvers/processed_variable.py b/pybamm/solvers/processed_variable.py index 26bd6326be..84e567e174 100644 --- a/pybamm/solvers/processed_variable.py +++ b/pybamm/solvers/processed_variable.py @@ -63,7 +63,6 @@ def __init__(self, base_variable, base_variable_casadi, solution, warn=True): self.auxiliary_domains = base_variable.auxiliary_domains self.warn = warn - self.inputs = solution.inputs self.symbolic_inputs = solution._symbolic_inputs self.u_sol = solution.y @@ -71,7 +70,7 @@ def __init__(self, base_variable, base_variable_casadi, solution, warn=True): # Sensitivity starts off uninitialized, only set when called self._sensitivity = None - self.solution_sensitivity = solution.sensitivity + self.all_sensitivities = solution.all_sensitivities # Set timescale self.timescale = solution.timescale_eval From c7ddbf56e30dba537aaa55e6a9598d3090df0e74 Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Sat, 8 May 2021 08:46:19 +0100 Subject: [PATCH 17/73] #1477 draft out a test for idaklu and changes to base solver for sensitivities --- pybamm/expression_tree/operations/evaluate.py | 34 ++++++--- pybamm/solvers/base_solver.py | 71 ++++++++++++++++--- pybamm/solvers/idaklu_solver.py | 19 ++++- tests/unit/test_solvers/test_idaklu_solver.py | 53 ++++++++++++++ 4 files changed, 156 insertions(+), 21 deletions(-) diff --git a/pybamm/expression_tree/operations/evaluate.py b/pybamm/expression_tree/operations/evaluate.py index 08b40eb605..bb37c781b6 100644 --- a/pybamm/expression_tree/operations/evaluate.py +++ b/pybamm/expression_tree/operations/evaluate.py @@ -557,7 +557,7 @@ def __init__(self, symbol): constants[symbol_id] = jax.device_put(constants[symbol_id]) # get a list of constant arguments to input to the function - arg_list = [ + self._arg_list = [ id_to_python_variable(symbol_id, True) for symbol_id in constants.keys() ] @@ -578,8 +578,8 @@ def __init__(self, symbol): # add function def to first line args = "t=None, y=None, y_dot=None, inputs=None, known_evals=None" - if arg_list: - args = ",".join(arg_list) + ", " + args + if self._arg_list: + args = ",".join(self._arg_list) + ", " + args python_str = "def evaluate_jax({}):\n".format(args) + python_str # calculate the final variable that will output the result of calling `evaluate` @@ -604,17 +604,33 @@ def __init__(self, symbol): compiled_function = compile(python_str, result_var, "exec") exec(compiled_function) - n = len(arg_list) - static_argnums = tuple(static_argnums) - self._jit_evaluate = jax.jit(self._evaluate_jax, static_argnums=static_argnums) + self._static_argnums = tuple(static_argnums) + self._jit_evaluate = jax.jit(self._evaluate_jax, + static_argnums=self._static_argnums) - # store a jit version of evaluate_jax's jacobian + def get_jacobian(self): + n = len(self._arg_list) + + # forward mode autodiff wrt y, which is argument 1 after arg_list jacobian_evaluate = jax.jacfwd(self._evaluate_jax, argnums=1 + n) - self._jac_evaluate = jax.jit(jacobian_evaluate, static_argnums=static_argnums) - def get_jacobian(self): + self._jac_evaluate = jax.jit(jacobian_evaluate, + static_argnums=self._static_argnums) + + return EvaluatorJaxJacobian(self._jac_evaluate, self._constants) + + def get_sensitivities(self): + n = len(self._arg_list) + + # forward mode autodiff wrt inputs, which is argument 3 after arg_list + jacobian_evaluate = jax.jacfwd(self._evaluate_jax, argnums=3 + n) + + self._sens_evaluate = jax.jit(jacobian_evaluate, + static_argnums=self._static_argnums) + return EvaluatorJaxJacobian(self._jac_evaluate, self._constants) + def debug(self, t=None, y=None, y_dot=None, inputs=None, known_evals=None): # generated code assumes y is a column vector if y is not None and y.ndim == 1: diff --git a/pybamm/solvers/base_solver.py b/pybamm/solvers/base_solver.py index 4af9f9726b..ead7048db2 100644 --- a/pybamm/solvers/base_solver.py +++ b/pybamm/solvers/base_solver.py @@ -34,6 +34,15 @@ class BaseSolver(object): The tolerance for the initial-condition solver (default is 1e-6). extrap_tol : float, optional The tolerance to assert whether extrapolation occurs or not. Default is 0. + sensitivity : str, optional + Whether (and how) to calculate sensitivities when solving. Options are: + - "explicit forward": explicitly formulate the sensitivity equations. \ + The formulation is as per "Park, S., Kato, D., Gima, Z., \ + Klein, R., & Moura, S. (2018). Optimal experimental design for parameterization\ + of an electrochemical lithium-ion battery model. Journal of The Electrochemical\ + Society, 165(7), A1309.". See #1100 for details \ + - see individual solvers for other options + """ def __init__( @@ -45,6 +54,7 @@ def __init__( root_tol=1e-6, extrap_tol=0, max_steps="deprecated", + sensitivity=None ): self._method = method self._rtol = rtol @@ -63,6 +73,7 @@ def __init__( self.name = "Base solver" self.ode_solver = False self.algebraic_solver = False + self.sensitivity = sensitivity @property def method(self): @@ -203,6 +214,10 @@ def set_up(self, model, inputs=None, t_eval=None): y = pybamm.StateVector(slice(0, model.concatenated_initial_conditions.size)) # set up Jacobian object, for re-use of dict jacobian = pybamm.Jacobian() + jacobian_parameters = { + p: pybamm.Jacobian() for p in inputs.keys() + } + else: # Convert model attributes to casadi t_casadi = casadi.MX.sym("t") @@ -225,12 +240,42 @@ def report(string): if use_jacobian is None: use_jacobian = model.use_jacobian - if model.convert_to_format != "casadi": - # Process with pybamm functions - if model.convert_to_format == "jax": - report(f"Converting {name} to jax") - jax_func = pybamm.EvaluatorJax(func) + if model.convert_to_format == "jax": + report(f"Converting {name} to jax") + func = pybamm.EvaluatorJax(func) + if self.sensitivity: + report(f"Calculating sensitivities for {name} using jax") + jacp_dict = func.get_sensitivities() + else: + jacp_dict = None + if use_jacobian: + report(f"Calculating jacobian for {name} using jax") + jac = func.get_jacobian() + jac = jac.evaluate + else: + jac = None + + func = func.evaluate + + elif model.convert_to_format != "casadi": + # Process with pybamm functions, optionally converting + # to python evaluator + if self.sensitivity: + report(f"Calculating sensitivities for {name}") + jacp_dict = { + p: jwrtp.jac(func, pybamm.InputParameter(p)) + for jwrtp, p in + zip(jacobian_parameters, inputs.keys()) + } + if model.convert_to_format == "python": + report(f"Converting sensitivities for {name} to python") + jacp_dict = { + p: pybamm.EvaluatorPython(jacp) + for p, jacp in jacp_dict.items() + } + else: + jacp_dict = None if use_jacobian: report(f"Calculating jacobian for {name}") @@ -238,9 +283,6 @@ def report(string): if model.convert_to_format == "python": report(f"Converting jacobian for {name} to python") jac = pybamm.EvaluatorPython(jac) - elif model.convert_to_format == "jax": - report(f"Converting jacobian for {name} to jax") - jac = jax_func.get_jacobian() jac = jac.evaluate else: jac = None @@ -248,9 +290,6 @@ def report(string): if model.convert_to_format == "python": report(f"Converting {name} to python") func = pybamm.EvaluatorPython(func) - if model.convert_to_format == "jax": - report(f"Converting {name} to jax") - func = jax_func func = func.evaluate @@ -266,6 +305,16 @@ def report(string): ) else: jac = None + + if self.sensitivity: + report(f"Calculating sensitivities for {name} using CasADi") + jacp_dict = { + name: casadi.jacobian(func, p) + for name, p in p_casadi.items() + } + else: + jacp_dict = None + func = casadi.Function( name, [t_casadi, y_casadi, p_casadi_stacked], [func] ) diff --git a/pybamm/solvers/idaklu_solver.py b/pybamm/solvers/idaklu_solver.py index e73f7ea4dd..3ced65d3eb 100644 --- a/pybamm/solvers/idaklu_solver.py +++ b/pybamm/solvers/idaklu_solver.py @@ -38,6 +38,15 @@ class IDAKLUSolver(pybamm.BaseSolver): The tolerance for the initial-condition solver (default is 1e-6). extrap_tol : float, optional The tolerance to assert whether extrapolation occurs or not (default is 0). + sensitivity : str, optional + Whether (and how) to calculate sensitivities when solving. Options are: + - "explicit forward": explicitly formulate the sensitivity equations. \ + The formulation is as per "Park, S., Kato, D., Gima, Z., \ + Klein, R., & Moura, S. (2018). Optimal experimental design for parameterization\ + of an electrochemical lithium-ion battery model. Journal of The Electrochemical\ + Society, 165(7), A1309.". See #1100 for details \ + - "idas": use Sundials IDAS to compute forward sensitivities + """ def __init__( @@ -48,13 +57,21 @@ def __init__( root_tol=1e-6, extrap_tol=0, max_steps="deprecated", + sensitivity="idas" ): if idaklu_spec is None: raise ImportError("KLU is not installed") super().__init__( - "ida", rtol, atol, root_method, root_tol, extrap_tol, max_steps + "ida", + rtol, + atol, + root_method, + root_tol, + extrap_tol, + max_steps, + sensitivity=sensitivity, ) self.name = "IDA KLU solver" diff --git a/tests/unit/test_solvers/test_idaklu_solver.py b/tests/unit/test_solvers/test_idaklu_solver.py index 773f9b9e30..b204bde6d7 100644 --- a/tests/unit/test_solvers/test_idaklu_solver.py +++ b/tests/unit/test_solvers/test_idaklu_solver.py @@ -46,6 +46,59 @@ def test_ida_roberts_klu(self): true_solution = 0.1 * solution.t np.testing.assert_array_almost_equal(solution.y[0, :], true_solution) + def test_ida_roberts_klu_sensitivities(self): + # this test implements a python version of the ida Roberts + # example provided in sundials + # see sundials ida examples pdf + for form in ["python", "casadi"]: + model = pybamm.BaseModel() + model.convert_to_format = form + u = pybamm.Variable("u") + v = pybamm.Variable("v") + a = pybamm.InputParameter("a") + model.rhs = {u: a * v} + model.algebraic = {v: 1 - v} + model.initial_conditions = {u: 0, v: 1} + model.events = [pybamm.Event("1", u - 0.2), pybamm.Event("2", v)] + + disc = pybamm.Discretisation() + disc.process_model(model) + + solver = pybamm.IDAKLUSolver(root_method="lm") + + t_eval = np.linspace(0, 3, 100) + a_value = 0.1 + sol = solver.solve(model, t_eval, inputs={"a": a_value}) + + # test that final time is time of event + # y = 0.1 t + y0 so y=0.2 when t=2 + np.testing.assert_array_almost_equal(sol.t[-1], 2.0) + + # test that final value is the event value + np.testing.assert_array_almost_equal(sol.y[0, -1], 0.2) + + # test that y[1] remains constant + np.testing.assert_array_almost_equal( + sol.y[1, :], np.ones(sol.t.shape) + ) + + # test that y[0] = to true solution + true_solution = 0.1 * sol.t + np.testing.assert_array_almost_equal(sol.y[0, :], true_solution) + + # evaluate the sensitivities using idas + dyda_ida = sol.sensitivities["a"] + + # evaluate the sensitivities using finite difference + h = 1e-6 + sol_plus = solver.solve(model, t_eval, inputs={"a": a_value + 0.5 * h}) + sol_neg = solver.solve(model, t_eval, inputs={"a": a_value - 0.5 * h}) + dyda_fd = (sol_plus.y - sol_neg.y) / h + + np.testing.assert_array_almost_equal( + dyda_ida, dyda_fd + ) + def test_set_atol(self): model = pybamm.lithium_ion.DFN() geometry = model.default_geometry From 41565dada6e94d3678df77dbc0e85f21bacffc29 Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Sat, 22 May 2021 09:24:23 +0100 Subject: [PATCH 18/73] #1477 python sensitivities seem ok, working on casadi --- pybamm/expression_tree/concatenations.py | 12 +++ pybamm/solvers/base_solver.py | 109 +++++++++++++++----- tests/unit/test_solvers/test_base_solver.py | 39 +++++++ 3 files changed, 132 insertions(+), 28 deletions(-) diff --git a/pybamm/expression_tree/concatenations.py b/pybamm/expression_tree/concatenations.py index 5b289198c4..a6ec54fdaf 100644 --- a/pybamm/expression_tree/concatenations.py +++ b/pybamm/expression_tree/concatenations.py @@ -42,6 +42,18 @@ def __str__(self): out = out[:-2] + ")" return out + def _diff(self, variable): + """ See :meth:`pybamm.Symbol._diff()`. """ + children_diffs = [ + child.diff(variable) for child in self.cached_children + ] + if len(children_diffs) == 1: + diff = children_diffs[0] + else: + diff = self.__class__(children_diffs) + + return diff + def get_children_domains(self, children): # combine domains from children domain = [] diff --git a/pybamm/solvers/base_solver.py b/pybamm/solvers/base_solver.py index ead7048db2..2c48b2c022 100644 --- a/pybamm/solvers/base_solver.py +++ b/pybamm/solvers/base_solver.py @@ -134,7 +134,8 @@ def copy(self): new_solver.models_set_up = {} return new_solver - def set_up(self, model, inputs=None, t_eval=None): + def set_up(self, model, inputs=None, t_eval=None, + calculate_sensitivites=False): """Unpack model, perform checks, and calculate jacobian. Parameters @@ -146,6 +147,10 @@ def set_up(self, model, inputs=None, t_eval=None): Any input parameters to pass to the model when solving t_eval : numeric type, optional The times (in seconds) at which to compute the solution + calculate_sensitivites : list of str or bool + If true, solver calculates sensitivities of all input parameters. + If only a subset of sensitivities are required, can also pass a + list of input parameter names """ pybamm.logger.info("Start solver set-up") @@ -209,14 +214,28 @@ def set_up(self, model, inputs=None, t_eval=None): ) model.convert_to_format = "casadi" + # find all the input parameters in the model + input_parameters = {} + for equation in [model.concatenated_rhs, + model.concatenated_algebraic, + model.concatenated_initial_conditions]: + input_parameters.update({ + symbol._id: symbol for symbol in equation.pre_order() + if isinstance(symbol, pybamm.InputParameter) + }) + + # from here on, calculate_sensitivites is now only a list + if isinstance(calculate_sensitivites, bool): + if calculate_sensitivites: + calculate_sensitivites = [p for p in inputs.keys()] + else: + calculate_sensitivites = [] + if model.convert_to_format != "casadi": # Create Jacobian from concatenated rhs and algebraic y = pybamm.StateVector(slice(0, model.concatenated_initial_conditions.size)) # set up Jacobian object, for re-use of dict jacobian = pybamm.Jacobian() - jacobian_parameters = { - p: pybamm.Jacobian() for p in inputs.keys() - } else: # Convert model attributes to casadi @@ -244,8 +263,11 @@ def report(string): if model.convert_to_format == "jax": report(f"Converting {name} to jax") func = pybamm.EvaluatorJax(func) - if self.sensitivity: - report(f"Calculating sensitivities for {name} using jax") + if calculate_sensitivites: + report(( + f"Calculating sensitivities for {name} with respect " + f"to parameters {calculate_sensitivites} using jax" + )) jacp_dict = func.get_sensitivities() else: jacp_dict = None @@ -261,12 +283,16 @@ def report(string): elif model.convert_to_format != "casadi": # Process with pybamm functions, optionally converting # to python evaluator - if self.sensitivity: - report(f"Calculating sensitivities for {name}") + print('calculate_sensitivites = ', calculate_sensitivites) + if calculate_sensitivites: + report(( + f"Calculating sensitivities for {name} with respect " + f"to parameters {calculate_sensitivites}" + )) + print(type(func)) jacp_dict = { - p: jwrtp.jac(func, pybamm.InputParameter(p)) - for jwrtp, p in - zip(jacobian_parameters, inputs.keys()) + p: func.diff(pybamm.InputParameter(p)) + for p in calculate_sensitivites } if model.convert_to_format == "python": report(f"Converting sensitivities for {name} to python") @@ -274,6 +300,7 @@ def report(string): p: pybamm.EvaluatorPython(jacp) for p, jacp in jacp_dict.items() } + jacp_dict = {k: v.evaluate for k, v in jacp_dict.items()} else: jacp_dict = None @@ -306,12 +333,18 @@ def report(string): else: jac = None - if self.sensitivity: - report(f"Calculating sensitivities for {name} using CasADi") - jacp_dict = { - name: casadi.jacobian(func, p) - for name, p in p_casadi.items() - } + if calculate_sensitivites: + report(( + f"Calculating sensitivities for {name} with respect " + f"to parameters {calculate_sensitivites} using CasADi" + )) + jacp_dict = {} + for pname in calculate_sensitivites: + p_diff = casadi.jacobian(func, p_casadi[pname]) + jacp_dict[pname] = casadi.Function( + name, [t_casadi, y_casadi, p_casadi_stacked], + [p_diff] + ) else: jacp_dict = None @@ -326,7 +359,12 @@ def report(string): jac_call = SolverCallable(jac, name + "_jac", model) else: jac_call = None - return func, func_call, jac_call + if jacp_dict is not None: + jacp_call = { + k: SolverCallable(v, name + "_sensitivity_wrt_" + k, model) + for k, v in jacp_dict.items() + } + return func, func_call, jac_call, jacp_call # Check for heaviside and modulo functions in rhs and algebraic and add # discontinuity events if these exist. @@ -400,8 +438,8 @@ def report(string): init_eval = InitialConditions(initial_conditions, model) # Process rhs, algebraic and event expressions - rhs, rhs_eval, jac_rhs = process(model.concatenated_rhs, "RHS") - algebraic, algebraic_eval, jac_algebraic = process( + rhs, rhs_eval, jac_rhs, jacp_rhs = process(model.concatenated_rhs, "RHS") + algebraic, algebraic_eval, jac_algebraic, jacp_algebraic = process( model.concatenated_algebraic, "algebraic" ) @@ -486,19 +524,23 @@ def report(string): # No rhs equations: residuals is algebraic only model.residuals_eval = Residuals(algebraic, "residuals", model) model.jacobian_eval = jac_algebraic + model.sensitivities_eval = jacp_algebraic elif len(model.algebraic) == 0: # No algebraic equations: residuals is rhs only model.residuals_eval = Residuals(rhs, "residuals", model) model.jacobian_eval = jac_rhs + model.sensitivities_eval = jacp_rhs # Calculate consistent initial conditions for the algebraic equations else: all_states = pybamm.NumpyConcatenation( model.concatenated_rhs, model.concatenated_algebraic ) # Process again, uses caching so should be quick - residuals_eval, jacobian_eval = process(all_states, "residuals")[1:] + residuals_eval, jacobian_eval, jacobian_wrtp_eval = \ + process(all_states, "residuals")[1:] model.residuals_eval = residuals_eval model.jacobian_eval = jacobian_eval + model.sensitivities_eval = jacobian_wrtp_eval pybamm.logger.info("Finish solver set-up") @@ -589,6 +631,7 @@ def solve( inputs=None, initial_conditions=None, nproc=None, + calculate_sensitivities=False ): """ Execute the solver setup and calculate the solution of the model at @@ -614,6 +657,10 @@ def solve( nproc : int, optional Number of processes to use when solving for more than one set of input parameters. Defaults to value returned by "os.cpu_count()". + calculate_sensitivites : list of str or bool + If true, solver calculates sensitivities of all input parameters. + If only a subset of sensitivities are required, can also pass a + list of input parameter names Returns ------- @@ -690,7 +737,8 @@ def solve( # not depend on input parameters. Thefore only `ext_and_inputs[0]` # is passed to `set_up`. # See https://github.com/pybamm-team/PyBaMM/pull/1261 - self.set_up(model, ext_and_inputs_list[0], t_eval) + self.set_up(model, ext_and_inputs_list[0], t_eval, + calculate_sensitivities) self.models_set_up.update( {model: {"initial conditions": model.concatenated_initial_conditions}} ) @@ -701,7 +749,8 @@ def solve( # If the new initial conditions are different, set up again # Doing the whole setup again might be slow, but no need to prematurely # optimize this - self.set_up(model, ext_and_inputs_list[0], t_eval) + self.set_up(model, ext_and_inputs_list[0], t_eval, + calculate_sensitivities) self.models_set_up[model][ "initial conditions" ] = model.concatenated_initial_conditions @@ -951,6 +1000,9 @@ def step( save : bool Turn on to store the solution of all previous timesteps + + + Raises ------ :class:`pybamm.ModelError` @@ -1241,12 +1293,13 @@ def __init__(self, function, name, model): self.timescale = self.model.timescale_eval def __call__(self, t, y, inputs): - if self.name in ["RHS", "algebraic", "residuals"]: - pybamm.logger.debug( - "Evaluating {} for {} at t={}".format( - self.name, self.model.name, t * self.timescale - ) + pybamm.logger.debug( + "Evaluating {} for {} at t={}".format( + self.name, self.model.name, t * self.timescale ) + ) + if self.name in ["RHS", "algebraic", "residuals"]: + return self.function(t, y, inputs).flatten() else: return self.function(t, y, inputs) diff --git a/tests/unit/test_solvers/test_base_solver.py b/tests/unit/test_solvers/test_base_solver.py index 12a4530626..9fe14629be 100644 --- a/tests/unit/test_solvers/test_base_solver.py +++ b/tests/unit/test_solvers/test_base_solver.py @@ -322,6 +322,45 @@ def test_extrapolation_warnings(self): with self.assertWarns(pybamm.SolverWarning): solver.solve(model, t_eval=[0, 1]) + def test_sensitivities(self): + pybamm.set_logging_level('DEBUG') + + def exact_diff_a(v, a, b): + return v**2 + 2 * a + + def exact_diff_b(v, a, b): + return v + + for f in ['', 'python', 'casadi']: + model = pybamm.BaseModel() + v = pybamm.Variable("v") + a = pybamm.InputParameter("a") + b = pybamm.InputParameter("b") + model.rhs = {v: a * v**2 + b * v + a**2} + model.initial_conditions = {v: 1} + model.convert_to_format = f + solver = pybamm.ScipySolver() + solver.set_up(model, calculate_sensitivites=True, + inputs={'a': 0, 'b': 0}) + for v_value in [0.1, -0.2, 1.5, 8.4]: + for a_value in [0.12, 1.5]: + for b_value in [0.82, 1.9]: + y = np.array([v_value]) + t = 0 + inputs = {'a': a_value, 'b': b_value} + + self.assertAlmostEqual( + model.sensitivities_eval['a']( + t=0, y=y, inputs=inputs + ), + exact_diff_a(v_value, a_value, b_value) + ) + self.assertAlmostEqual( + model.sensitivities_eval['b']( + t=0, y=y, inputs=inputs + ), + exact_diff_b(v_value, a_value, b_value) + ) if __name__ == "__main__": print("Add -v for more debug output") From 7d97c45e85b4a6a0f01ca286557e748bf9ea1f32 Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Mon, 24 May 2021 11:35:47 +0100 Subject: [PATCH 19/73] #1477 evaluating sensitivities ok for all convert_tos --- pybamm/expression_tree/operations/evaluate.py | 24 ++++++++- pybamm/solvers/base_solver.py | 20 +++++--- tests/unit/test_solvers/test_base_solver.py | 51 +++++++++++++------ 3 files changed, 72 insertions(+), 23 deletions(-) diff --git a/pybamm/expression_tree/operations/evaluate.py b/pybamm/expression_tree/operations/evaluate.py index bb37c781b6..cc16574066 100644 --- a/pybamm/expression_tree/operations/evaluate.py +++ b/pybamm/expression_tree/operations/evaluate.py @@ -628,7 +628,7 @@ def get_sensitivities(self): self._sens_evaluate = jax.jit(jacobian_evaluate, static_argnums=self._static_argnums) - return EvaluatorJaxJacobian(self._jac_evaluate, self._constants) + return EvaluatorJaxSensitivities(self._sens_evaluate, self._constants) def debug(self, t=None, y=None, y_dot=None, inputs=None, known_evals=None): @@ -687,3 +687,25 @@ def evaluate(self, t=None, y=None, y_dot=None, inputs=None, known_evals=None): return result, known_evals else: return result + +class EvaluatorJaxSensitivities: + def __init__(self, jac_evaluate, constants): + self._jac_evaluate = jac_evaluate + self._constants = constants + + def evaluate(self, t=None, y=None, y_dot=None, inputs=None, known_evals=None): + """ + Acts as a drop-in replacement for :func:`pybamm.Symbol.evaluate` + """ + # generated code assumes y is a column vector + if y is not None and y.ndim == 1: + y = y.reshape(-1, 1) + + # execute code + result = self._jac_evaluate(*self._constants, t, y, y_dot, inputs, known_evals) + + # don't need known_evals, but need to reproduce Symbol.evaluate signature + if known_evals is not None: + return result, known_evals + else: + return result diff --git a/pybamm/solvers/base_solver.py b/pybamm/solvers/base_solver.py index 2c48b2c022..515facb959 100644 --- a/pybamm/solvers/base_solver.py +++ b/pybamm/solvers/base_solver.py @@ -269,6 +269,7 @@ def report(string): f"to parameters {calculate_sensitivites} using jax" )) jacp_dict = func.get_sensitivities() + jacp_dict = jacp_dict.evaluate else: jacp_dict = None if use_jacobian: @@ -283,13 +284,11 @@ def report(string): elif model.convert_to_format != "casadi": # Process with pybamm functions, optionally converting # to python evaluator - print('calculate_sensitivites = ', calculate_sensitivites) if calculate_sensitivites: report(( f"Calculating sensitivities for {name} with respect " f"to parameters {calculate_sensitivites}" )) - print(type(func)) jacp_dict = { p: func.diff(pybamm.InputParameter(p)) for p in calculate_sensitivites @@ -360,10 +359,17 @@ def report(string): else: jac_call = None if jacp_dict is not None: - jacp_call = { - k: SolverCallable(v, name + "_sensitivity_wrt_" + k, model) - for k, v in jacp_dict.items() - } + if model.convert_to_format == "jax": + jacp_call = SolverCallable( + jacp_dict, name + "_sensitivity_wrt_inputs", model + ) + else: + jacp_call = { + k: SolverCallable(v, name + "_sensitivity_wrt_" + k, model) + for k, v in jacp_dict.items() + } + else: + jacp_call = None return func, func_call, jac_call, jacp_call # Check for heaviside and modulo functions in rhs and algebraic and add @@ -520,6 +526,8 @@ def report(string): "rhs", [t_casadi, y_casadi, p_casadi_stacked], [explicit_rhs] ) model.casadi_algebraic = algebraic + model.casadi_sensitivities_rhs = jacp_rhs + model.casadi_sensitivities_algebraic = jacp_algebraic if len(model.rhs) == 0: # No rhs equations: residuals is algebraic only model.residuals_eval = Residuals(algebraic, "residuals", model) diff --git a/tests/unit/test_solvers/test_base_solver.py b/tests/unit/test_solvers/test_base_solver.py index 9fe14629be..66379634bf 100644 --- a/tests/unit/test_solvers/test_base_solver.py +++ b/tests/unit/test_solvers/test_base_solver.py @@ -323,15 +323,14 @@ def test_extrapolation_warnings(self): solver.solve(model, t_eval=[0, 1]) def test_sensitivities(self): - pybamm.set_logging_level('DEBUG') def exact_diff_a(v, a, b): - return v**2 + 2 * a + return np.array([v**2 + 2 * a]) def exact_diff_b(v, a, b): - return v + return np.array([v]) - for f in ['', 'python', 'casadi']: + for f in ['', 'python', 'casadi', 'jax']: model = pybamm.BaseModel() v = pybamm.Variable("v") a = pybamm.InputParameter("a") @@ -342,25 +341,45 @@ def exact_diff_b(v, a, b): solver = pybamm.ScipySolver() solver.set_up(model, calculate_sensitivites=True, inputs={'a': 0, 'b': 0}) + all_inputs = [] for v_value in [0.1, -0.2, 1.5, 8.4]: for a_value in [0.12, 1.5]: for b_value in [0.82, 1.9]: y = np.array([v_value]) t = 0 inputs = {'a': a_value, 'b': b_value} + all_inputs.append((t, y, inputs)) + for t, y, inputs in all_inputs: + if f == 'casadi': + use_inputs = casadi.vertcat(*[x for x in inputs.values()]) + else: + use_inputs = inputs + if f == 'jax': + sens = model.sensitivities_eval( + t, y, use_inputs + ) + np.testing.assert_array_equal( + sens['a'], + exact_diff_a(y, inputs['a'], inputs['b']) + ) + np.testing.assert_array_equal( + sens['b'], + exact_diff_b(y, inputs['a'], inputs['b']) + ) + else: + np.testing.assert_array_equal( + model.sensitivities_eval['a']( + t, y, use_inputs + ), + exact_diff_a(y, inputs['a'], inputs['b']) + ) + np.testing.assert_array_equal( + model.sensitivities_eval['b']( + t, y, use_inputs + ), + exact_diff_b(y, inputs['a'], inputs['b']) + ) - self.assertAlmostEqual( - model.sensitivities_eval['a']( - t=0, y=y, inputs=inputs - ), - exact_diff_a(v_value, a_value, b_value) - ) - self.assertAlmostEqual( - model.sensitivities_eval['b']( - t=0, y=y, inputs=inputs - ), - exact_diff_b(v_value, a_value, b_value) - ) if __name__ == "__main__": print("Add -v for more debug output") From 7c39f3fd9fb84960eee443f6b7efe95695b097aa Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Mon, 24 May 2021 12:06:08 +0100 Subject: [PATCH 20/73] #1477 update test_sensitivities to use a dae --- tests/unit/test_solvers/test_base_solver.py | 38 +++++++++++-------- tests/unit/test_solvers/test_idaklu_solver.py | 5 ++- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/tests/unit/test_solvers/test_base_solver.py b/tests/unit/test_solvers/test_base_solver.py index 66379634bf..82eda24e8c 100644 --- a/tests/unit/test_solvers/test_base_solver.py +++ b/tests/unit/test_solvers/test_base_solver.py @@ -324,37 +324,43 @@ def test_extrapolation_warnings(self): def test_sensitivities(self): - def exact_diff_a(v, a, b): - return np.array([v**2 + 2 * a]) + def exact_diff_a(y, a, b): + return np.array([ + [y[0]**2 + 2 * a], + [y[0]] + ]) - def exact_diff_b(v, a, b): - return np.array([v]) + def exact_diff_b(y, a, b): + return np.array([[y[0]], [0]]) - for f in ['', 'python', 'casadi', 'jax']: + for convert_to_format in ['', 'python', 'casadi', 'jax']: model = pybamm.BaseModel() v = pybamm.Variable("v") + u = pybamm.Variable("u") a = pybamm.InputParameter("a") b = pybamm.InputParameter("b") model.rhs = {v: a * v**2 + b * v + a**2} - model.initial_conditions = {v: 1} - model.convert_to_format = f - solver = pybamm.ScipySolver() + model.algebraic = {u: a * v - u} + model.initial_conditions = {v: 1, u: a * 1} + model.convert_to_format = convert_to_format + solver = pybamm.CasadiSolver() solver.set_up(model, calculate_sensitivites=True, inputs={'a': 0, 'b': 0}) all_inputs = [] for v_value in [0.1, -0.2, 1.5, 8.4]: - for a_value in [0.12, 1.5]: - for b_value in [0.82, 1.9]: - y = np.array([v_value]) - t = 0 - inputs = {'a': a_value, 'b': b_value} - all_inputs.append((t, y, inputs)) + for u_value in [0.13, -0.23, 1.3, 13.4]: + for a_value in [0.12, 1.5]: + for b_value in [0.82, 1.9]: + y = np.array([v_value, u_value]) + t = 0 + inputs = {'a': a_value, 'b': b_value} + all_inputs.append((t, y, inputs)) for t, y, inputs in all_inputs: - if f == 'casadi': + if model.convert_to_format == 'casadi': use_inputs = casadi.vertcat(*[x for x in inputs.values()]) else: use_inputs = inputs - if f == 'jax': + if model.convert_to_format == 'jax': sens = model.sensitivities_eval( t, y, use_inputs ) diff --git a/tests/unit/test_solvers/test_idaklu_solver.py b/tests/unit/test_solvers/test_idaklu_solver.py index b204bde6d7..6b431edda1 100644 --- a/tests/unit/test_solvers/test_idaklu_solver.py +++ b/tests/unit/test_solvers/test_idaklu_solver.py @@ -68,7 +68,10 @@ def test_ida_roberts_klu_sensitivities(self): t_eval = np.linspace(0, 3, 100) a_value = 0.1 - sol = solver.solve(model, t_eval, inputs={"a": a_value}) + sol = solver.solve( + model, t_eval, inputs={"a": a_value}, + calculate_sensitivities=True + ) # test that final time is time of event # y = 0.1 t + y0 so y=0.2 when t=2 From f10fdfcd79f7c2904ea527e586dbb03a5195c702 Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Thu, 10 Jun 2021 09:48:16 +0100 Subject: [PATCH 21/73] #1477 sensitivivity calc in idas-klu is running, now need to extract the sensitivity solution --- FindSUNDIALS.cmake | 4 +- pybamm/expression_tree/concatenations.py | 2 +- pybamm/solvers/base_solver.py | 63 +++++-- pybamm/solvers/c_solvers/idaklu.cpp | 155 ++++++++++++++++-- pybamm/solvers/idaklu_solver.py | 56 ++++++- pybamm/solvers/solution.py | 1 + tests/unit/test_solvers/test_base_solver.py | 39 ++--- tests/unit/test_solvers/test_idaklu_solver.py | 1 + 8 files changed, 261 insertions(+), 60 deletions(-) diff --git a/FindSUNDIALS.cmake b/FindSUNDIALS.cmake index 7b866056b8..7b3051d903 100644 --- a/FindSUNDIALS.cmake +++ b/FindSUNDIALS.cmake @@ -27,7 +27,7 @@ # find the SUNDIALS include directories find_path(SUNDIALS_INCLUDE_DIR NAMES - ida/ida.h + idas/idas.h sundials/sundials_math.h sundials/sundials_types.h sunlinsol/sunlinsol_klu.h @@ -39,7 +39,7 @@ find_path(SUNDIALS_INCLUDE_DIR ) set(SUNDIALS_WANT_COMPONENTS - sundials_ida + sundials_idas sundials_sunlinsolklu sundials_sunmatrixsparse sundials_nvecserial diff --git a/pybamm/expression_tree/concatenations.py b/pybamm/expression_tree/concatenations.py index a6ec54fdaf..9642ed3777 100644 --- a/pybamm/expression_tree/concatenations.py +++ b/pybamm/expression_tree/concatenations.py @@ -50,7 +50,7 @@ def _diff(self, variable): if len(children_diffs) == 1: diff = children_diffs[0] else: - diff = self.__class__(children_diffs) + diff = self.__class__(*children_diffs) return diff diff --git a/pybamm/solvers/base_solver.py b/pybamm/solvers/base_solver.py index 515facb959..a7b1ab4960 100644 --- a/pybamm/solvers/base_solver.py +++ b/pybamm/solvers/base_solver.py @@ -268,10 +268,10 @@ def report(string): f"Calculating sensitivities for {name} with respect " f"to parameters {calculate_sensitivites} using jax" )) - jacp_dict = func.get_sensitivities() - jacp_dict = jacp_dict.evaluate + jacp = func.get_sensitivities() + jacp = jacp.evaluate else: - jacp_dict = None + jacp = None if use_jacobian: report(f"Calculating jacobian for {name} using jax") jac = func.get_jacobian() @@ -299,9 +299,13 @@ def report(string): p: pybamm.EvaluatorPython(jacp) for p, jacp in jacp_dict.items() } - jacp_dict = {k: v.evaluate for k, v in jacp_dict.items()} + + # jacp should be a function that returns a dict of sensitivities + def jacp(*args, **kwargs): + return {k: v.evaluate(*args, **kwargs) + for k, v in jacp_dict.items()} else: - jacp_dict = None + jacp = None if use_jacobian: report(f"Calculating jacobian for {name}") @@ -344,8 +348,13 @@ def report(string): name, [t_casadi, y_casadi, p_casadi_stacked], [p_diff] ) + # jacp should be a function that returns a dict of sensitivities + def jacp(*args, **kwargs): + return {k: v(*args, **kwargs) + for k, v in jacp_dict.items()} + else: - jacp_dict = None + jacp = None func = casadi.Function( name, [t_casadi, y_casadi, p_casadi_stacked], [func] @@ -358,16 +367,11 @@ def report(string): jac_call = SolverCallable(jac, name + "_jac", model) else: jac_call = None - if jacp_dict is not None: - if model.convert_to_format == "jax": - jacp_call = SolverCallable( - jacp_dict, name + "_sensitivity_wrt_inputs", model - ) - else: - jacp_call = { - k: SolverCallable(v, name + "_sensitivity_wrt_" + k, model) - for k, v in jacp_dict.items() - } + if jacp is not None: + jacp_call = SensitivityCallable( + jacp, name + "_sensitivity_wrt_inputs", model, + model.convert_to_format + ) else: jacp_call = None return func, func_call, jac_call, jacp_call @@ -1323,6 +1327,33 @@ def function(self, t, y, inputs): else: return self._function(t, y, inputs=inputs, known_evals={})[0] +class SensitivityCallable: + """A class that will be called by the solver when integrating""" + + def __init__(self, function, name, model, form): + self._function = function + self.form = form + self.name = name + self.model = model + self.timescale = self.model.timescale_eval + + def __call__(self, t, y, inputs): + pybamm.logger.debug( + "Evaluating sensitivities of {} for {} at t={}".format( + self.name, self.model.name, t * self.timescale + ) + ) + return self.function(t, y, inputs) + + def function(self, t, y, inputs): + if self.form == "casadi": + return self._function(t, y, inputs) + elif self.form == "jax": + return self._function(t, y, inputs=inputs) + else: + ret_with_known_evals = \ + self._function(t, y, inputs=inputs, known_evals={}) + return {k: v[0] for k, v in ret_with_known_evals.items()} class Residuals(SolverCallable): """Returns information about residuals at time t and state y""" diff --git a/pybamm/solvers/c_solvers/idaklu.cpp b/pybamm/solvers/c_solvers/idaklu.cpp index 55704e2161..5bc3fa050f 100644 --- a/pybamm/solvers/c_solvers/idaklu.cpp +++ b/pybamm/solvers/c_solvers/idaklu.cpp @@ -1,7 +1,7 @@ #include #include -#include /* prototypes for IDA fcts., consts. */ +#include /* prototypes for IDAS fcts., consts. */ #include /* access to serial N_Vector */ #include /* defs. of SUNRabs, SUNRexp, etc. */ #include /* defs. of realtype, sunindextype */ @@ -11,12 +11,21 @@ #include #include #include + +#include namespace py = pybind11; using residual_type = std::function( double, py::array_t, py::array_t)>; +using sensitivities_type = std::function, realtype, + py::array_t, py::array_t, + py::array_t, py::array_t + )>; using jacobian_type = std::function(double, py::array_t, double)>; + + using event_type = std::function(double, py::array_t)>; using np_array = py::array_t; @@ -27,14 +36,20 @@ class PybammFunctions { public: int number_of_states; + int number_of_parameters; int number_of_events; PybammFunctions(const residual_type &res, const jacobian_type &jac, + const sensitivities_type &sens, const jac_get_type &get_jac_data_in, const jac_get_type &get_jac_row_vals_in, const jac_get_type &get_jac_col_ptrs_in, - const event_type &event, const int n_s, int n_e) - : number_of_states(n_s), number_of_events(n_e), py_res(res), py_jac(jac), + const event_type &event, + const int n_s, int n_e, const int n_p) + : number_of_states(n_s), number_of_events(n_e), + number_of_parameters(n_p), + py_res(res), py_jac(jac), + py_sens(sens), py_event(event), py_get_jac_data(get_jac_data_in), py_get_jac_row_vals(get_jac_row_vals_in), py_get_jac_col_ptrs(get_jac_col_ptrs_in) @@ -44,12 +59,14 @@ class PybammFunctions py::array_t operator()(double t, py::array_t y, py::array_t yp) { + std::cout << "calling res()" << std::endl; return py_res(t, y, yp); } py::array_t res(double t, py::array_t y, py::array_t yp) { + std::cout << "calling res" << std::endl; return py_res(t, y, yp); } @@ -61,6 +78,24 @@ class PybammFunctions py_jac(t, y, cj); } + void sensitivities( + py::array_t resvalS, + double t, + py::array_t y, py::array_t yp, + py::array_t yS, py::array_t ypS) + { + // this function evaluates the sensitivity equations required by IDAS, + // returning them in resvalS, which is preallocated as a numpy array + // of size (np, n), where n is the number of states and np is the number + // of parameters + // + // yS and ypS are also shape (np, n), y and yp are shape (n) + // + // dF/dy * s_i + dF/dyd * sd + dFdp_i for i in range(np) + std::cout << "calling sensitivity" << std::endl; + py_sens(resvalS, t, y, yp, yS, ypS); + } + np_array get_jac_data() { return py_get_jac_data(); } np_array get_jac_row_vals() { return py_get_jac_row_vals(); } @@ -71,6 +106,7 @@ class PybammFunctions private: residual_type py_res; + sensitivities_type py_sens; jacobian_type py_jac; event_type py_event; jac_get_type py_get_jac_data; @@ -81,6 +117,7 @@ class PybammFunctions int residual(realtype tres, N_Vector yy, N_Vector yp, N_Vector rr, void *user_data) { + std::cout << "calling orignal res" <(user_data); PybammFunctions python_functions = *python_functions_ptr; @@ -106,6 +143,7 @@ int residual(realtype tres, N_Vector yy, N_Vector yp, N_Vector rr, { rval[i] = r_np_ptr[i]; } + std::cout << "back in original res" <(user_data); + PybammFunctions python_functions = *python_functions_ptr; + + realtype *yval = N_VGetArrayPointer(yy); + realtype *ypval = N_VGetArrayPointer(yp); + realtype *ySval = N_VGetArrayPointer(yS[0]); + realtype *ypSval = N_VGetArrayPointer(ypS[0]); + realtype *resvalSval = N_VGetArrayPointer(resvalS[0]); + + int n = python_functions.number_of_states; + int np = python_functions.number_of_parameters; + + py::array_t y_np = py::array_t(n, yval); + py::array_t yp_np = py::array_t(n, ypval); + py::array_t yS_np = py::array_t( + std::vector{np, n}, ySval + ); + py::array_t ypS_np = py::array_t( + std::vector{np, n}, ypSval + ); + py::array_t resvalS_np = py::array_t( + std::vector{np, n}, resvalSval + ); + + python_functions.sensitivities( + resvalS_np, t, y_np, yp_np, yS_np, ypS_np + ); + + return 0; +} + class Solution { public: @@ -211,23 +307,23 @@ class Solution /* main program */ Solution solve(np_array t_np, np_array y0_np, np_array yp0_np, - residual_type res, jacobian_type jac, jac_get_type gjd, - jac_get_type gjrv, jac_get_type gjcp, int nnz, event_type event, + residual_type res, jacobian_type jac, + sensitivities_type sens, + jac_get_type gjd, jac_get_type gjrv, jac_get_type gjcp, + int nnz, event_type event, int number_of_events, int use_jacobian, np_array rhs_alg_id, - np_array atol_np, double rel_tol) + np_array atol_np, double rel_tol, int number_of_parameters) { auto t = t_np.unchecked<1>(); auto y0 = y0_np.unchecked<1>(); auto yp0 = yp0_np.unchecked<1>(); auto atol = atol_np.unchecked<1>(); - int number_of_states; - number_of_states = y0_np.request().size; - int number_of_timesteps; - number_of_timesteps = t_np.request().size; - + int number_of_states = y0_np.request().size; + int number_of_timesteps = t_np.request().size; void *ida_mem; // pointer to memory N_Vector yy, yp, avtol; // y, y', and absolute tolerance + N_Vector *yyS, *ypS; // y, y' for sensitivities realtype rtol, *yval, *ypval, *atval; int retval; SUNMatrix J; @@ -238,6 +334,11 @@ Solution solve(np_array t_np, np_array y0_np, np_array yp0_np, yp = N_VNew_Serial(number_of_states); avtol = N_VNew_Serial(number_of_states); + if (number_of_parameters > 0) { + yyS = N_VCloneVectorArray(number_of_parameters, yy); + ypS = N_VCloneVectorArray(number_of_parameters, yp); + } + // set initial value yval = N_VGetArrayPointer(yy); ypval = N_VGetArrayPointer(yp); @@ -250,6 +351,13 @@ Solution solve(np_array t_np, np_array y0_np, np_array yp0_np, atval[i] = atol[i]; } + if (number_of_parameters > 0) { + for (int is = 0 ; is < number_of_parameters; is++) { + N_VConst(NZERO, yyS[is]); + N_VConst(NZERO, ypS[is]); + } + } + // allocate memory for solver ida_mem = IDACreate(); @@ -266,8 +374,9 @@ Solution solve(np_array t_np, np_array y0_np, np_array yp0_np, IDARootInit(ida_mem, number_of_events, events); // set pybamm functions by passing pointer to it - PybammFunctions pybamm_functions(res, jac, gjd, gjrv, gjcp, event, - number_of_states, number_of_events); + PybammFunctions pybamm_functions(res, jac, sens, gjd, gjrv, gjcp, event, + number_of_states, number_of_events, + number_of_parameters); void *user_data = &pybamm_functions; IDASetUserData(ida_mem, user_data); @@ -282,6 +391,16 @@ Solution solve(np_array t_np, np_array y0_np, np_array yp0_np, IDASetJacFn(ida_mem, jacobian); } + if (number_of_parameters > 0) + { + std::cout << "running sensitivities with np = " << number_of_parameters << std::endl; + retval = IDASensInit(ida_mem, number_of_parameters, + IDA_SIMULTANEOUS, sensitivities, yyS, ypS); + std::cout << "retval from IDASensInit is " << retval << std::endl; + retval = IDASensEEtolerances(ida_mem); + std::cout << "retval from IDASensEEtolerances is " << retval << std::endl; + } + int t_i = 1; realtype tret; realtype t_next; @@ -299,6 +418,7 @@ Solution solve(np_array t_np, np_array y0_np, np_array yp0_np, } // calculate consistent initial conditions + std::cout << "calculating ICs" << std::endl; N_Vector id; auto id_np_val = rhs_alg_id.unchecked<1>(); id = N_VNew_Serial(number_of_states); @@ -313,10 +433,12 @@ Solution solve(np_array t_np, np_array y0_np, np_array yp0_np, IDASetId(ida_mem, id); IDACalcIC(ida_mem, IDA_YA_YDP_INIT, t(1)); + std::cout << "finished calculating ICs" << std::endl; while (true) { t_next = t(t_i); + std::cout << "next time step "< 0) { + IDASensFree(ida_mem); + } IDAFree(&ida_mem); SUNLinSolFree(LS); SUNMatDestroy(J); @@ -362,10 +487,12 @@ PYBIND11_MODULE(idaklu, m) m.doc() = "sundials solvers"; // optional module docstring m.def("solve", &solve, "The solve function", py::arg("t"), py::arg("y0"), - py::arg("yp0"), py::arg("res"), py::arg("jac"), py::arg("get_jac_data"), + py::arg("yp0"), py::arg("res"), py::arg("jac"), py::arg("sens"), + py::arg("get_jac_data"), py::arg("get_jac_row_vals"), py::arg("get_jac_col_ptr"), py::arg("nnz"), py::arg("events"), py::arg("number_of_events"), py::arg("use_jacobian"), py::arg("rhs_alg_id"), py::arg("atol"), py::arg("rtol"), + py::arg("number_of_sensitivity_parameters"), py::return_value_policy::take_ownership); py::class_(m, "solution") diff --git a/pybamm/solvers/idaklu_solver.py b/pybamm/solvers/idaklu_solver.py index 3ced65d3eb..349cea5ab5 100644 --- a/pybamm/solvers/idaklu_solver.py +++ b/pybamm/solvers/idaklu_solver.py @@ -196,6 +196,11 @@ def _integrate(self, model, t_eval, inputs_dict=None): mass_matrix = model.mass_matrix.entries + # construct residuals function by binding inputs + def resfn(t, y, ydot): + print("calling python res") + return model.residuals_eval(t, y, ydot, inputs) + if model.jacobian_eval: jac_y0_t0 = model.jacobian_eval(t_eval[0], y0, inputs) if sparse.issparse(jac_y0_t0): @@ -254,14 +259,62 @@ def rootfn(t, y): alg_ids = np.zeros(len(y0) - len(rhs_ids)) ids = np.concatenate((rhs_ids, alg_ids)) + number_of_sensitivity_parameters = 0 + if model.sensitivities_eval is not None: + sens0 = model.sensitivities_eval(t=0, y=y0, inputs=inputs) + print('sensitivities found are = ', sens0.keys()) + number_of_sensitivity_parameters = len(sens0.keys()) + + def sensfn(resvalS, t, y, yp, yS, ypS): + """ + this function evaluates the sensitivity equations required by IDAS, + returning them in resvalS, which is preallocated as a numpy array of size + (np, n), where n is the number of states and np is the number of parameters + + The equations returned are: + + dF/dy * s_i + dF/dyd * sd_i + dFdp_i for i in range(np) + + Parameters + ---------- + resvalS: ndarray of shape (np, n) + returns the sensitivity equations in this preallocated array + t: number + time value + y: ndarray of shape (n) + current state vector + yp: ndarray of shape (n) + current time derivative of state vector + yS: ndarray of shape (n) + current state vector of sensitivity equations + ypS: ndarray of shape (n) + current time derivative of state vector of sensitivity equations + + """ + + print('calling python sensfn') + np, n = resvalS.shape + dFdy = model.jacobian_eval(t, y, inputs) + dFdyd = mass_matrix + dFdp = model.sensitivities_eval(t, y, inputs) + print('dFdy', dFdy.shape) + print('dFdyd', dFdyd.shape) + print('dFdp_i', list(dFdp.values())[0].shape) + print('yS_i', yS[0].shape) + print('ypS_i', ypS[0].shape) + + for i, dFdp_i in enumerate(dFdp.values()): + resvalS[i] = dFdy @ yS[i] + dFdyd @ ypS[i] + dFdp_i.reshape(-1) + # solve timer = pybamm.Timer() sol = idaklu.solve( t_eval, y0, ydot0, - lambda t, y, ydot: model.residuals_eval(t, y, ydot, inputs), + resfn, jac_class.jac_res, + sensfn, jac_class.get_jac_data, jac_class.get_jac_row_vals, jac_class.get_jac_col_ptrs, @@ -272,6 +325,7 @@ def rootfn(t, y): ids, atol, rtol, + number_of_sensitivity_parameters, ) integration_time = timer.time() diff --git a/pybamm/solvers/solution.py b/pybamm/solvers/solution.py index 7023d921c6..ec743050b0 100644 --- a/pybamm/solvers/solution.py +++ b/pybamm/solvers/solution.py @@ -130,6 +130,7 @@ def y(self): self.set_y() return self._y + def set_y(self): try: if isinstance(self.all_ys[0], (casadi.DM, casadi.MX)): diff --git a/tests/unit/test_solvers/test_base_solver.py b/tests/unit/test_solvers/test_base_solver.py index 82eda24e8c..3c1bd4de30 100644 --- a/tests/unit/test_solvers/test_base_solver.py +++ b/tests/unit/test_solvers/test_base_solver.py @@ -343,7 +343,7 @@ def exact_diff_b(y, a, b): model.algebraic = {u: a * v - u} model.initial_conditions = {v: 1, u: a * 1} model.convert_to_format = convert_to_format - solver = pybamm.CasadiSolver() + solver = pybamm.IDAKLUSolver(root_method='lm') solver.set_up(model, calculate_sensitivites=True, inputs={'a': 0, 'b': 0}) all_inputs = [] @@ -360,31 +360,18 @@ def exact_diff_b(y, a, b): use_inputs = casadi.vertcat(*[x for x in inputs.values()]) else: use_inputs = inputs - if model.convert_to_format == 'jax': - sens = model.sensitivities_eval( - t, y, use_inputs - ) - np.testing.assert_array_equal( - sens['a'], - exact_diff_a(y, inputs['a'], inputs['b']) - ) - np.testing.assert_array_equal( - sens['b'], - exact_diff_b(y, inputs['a'], inputs['b']) - ) - else: - np.testing.assert_array_equal( - model.sensitivities_eval['a']( - t, y, use_inputs - ), - exact_diff_a(y, inputs['a'], inputs['b']) - ) - np.testing.assert_array_equal( - model.sensitivities_eval['b']( - t, y, use_inputs - ), - exact_diff_b(y, inputs['a'], inputs['b']) - ) + + sens = model.sensitivities_eval( + t, y, use_inputs + ) + np.testing.assert_allclose( + sens['a'], + exact_diff_a(y, inputs['a'], inputs['b']) + ) + np.testing.assert_allclose( + sens['b'], + exact_diff_b(y, inputs['a'], inputs['b']) + ) if __name__ == "__main__": diff --git a/tests/unit/test_solvers/test_idaklu_solver.py b/tests/unit/test_solvers/test_idaklu_solver.py index 6b431edda1..28f1ce54ee 100644 --- a/tests/unit/test_solvers/test_idaklu_solver.py +++ b/tests/unit/test_solvers/test_idaklu_solver.py @@ -68,6 +68,7 @@ def test_ida_roberts_klu_sensitivities(self): t_eval = np.linspace(0, 3, 100) a_value = 0.1 + print('starting solve.....') sol = solver.solve( model, t_eval, inputs={"a": a_value}, calculate_sensitivities=True From 1f3bc9d75b03d84ff0d64e9246f697a4682e7118 Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Mon, 14 Jun 2021 17:26:21 +0100 Subject: [PATCH 22/73] #1477 idaklu sensitivities works and tested for python, casadi and jax --- pybamm/solvers/c_solvers/idaklu.cpp | 162 ++++++++++-------- pybamm/solvers/idaklu_solver.py | 31 ++-- pybamm/solvers/solution.py | 19 +- tests/unit/test_solvers/test_idaklu_solver.py | 12 +- 4 files changed, 121 insertions(+), 103 deletions(-) diff --git a/pybamm/solvers/c_solvers/idaklu.cpp b/pybamm/solvers/c_solvers/idaklu.cpp index 5bc3fa050f..00afb95c16 100644 --- a/pybamm/solvers/c_solvers/idaklu.cpp +++ b/pybamm/solvers/c_solvers/idaklu.cpp @@ -11,24 +11,24 @@ #include #include #include +#include -#include +//#include namespace py = pybind11; -using residual_type = std::function( - double, py::array_t, py::array_t)>; + +using np_array = py::array_t; +PYBIND11_MAKE_OPAQUE(std::vector); +using residual_type = std::function; using sensitivities_type = std::function, realtype, - py::array_t, py::array_t, - py::array_t, py::array_t + std::vector&, realtype, const np_array&, + const np_array&, const std::vector&, + const std::vector& )>; -using jacobian_type = - std::function(double, py::array_t, double)>; - +using jacobian_type = std::function; using event_type = - std::function(double, py::array_t)>; -using np_array = py::array_t; + std::function; using jac_get_type = std::function; @@ -59,14 +59,12 @@ class PybammFunctions py::array_t operator()(double t, py::array_t y, py::array_t yp) { - std::cout << "calling res()" << std::endl; return py_res(t, y, yp); } py::array_t res(double t, py::array_t y, py::array_t yp) { - std::cout << "calling res" << std::endl; return py_res(t, y, yp); } @@ -79,10 +77,9 @@ class PybammFunctions } void sensitivities( - py::array_t resvalS, - double t, - py::array_t y, py::array_t yp, - py::array_t yS, py::array_t ypS) + std::vector& resvalS, + const double t, const np_array& y, const np_array& yp, + const std::vector& yS, const std::vector& ypS) { // this function evaluates the sensitivity equations required by IDAS, // returning them in resvalS, which is preallocated as a numpy array @@ -92,7 +89,6 @@ class PybammFunctions // yS and ypS are also shape (np, n), y and yp are shape (n) // // dF/dy * s_i + dF/dyd * sd + dFdp_i for i in range(np) - std::cout << "calling sensitivity" << std::endl; py_sens(resvalS, t, y, yp, yS, ypS); } @@ -117,7 +113,6 @@ class PybammFunctions int residual(realtype tres, N_Vector yy, N_Vector yp, N_Vector rr, void *user_data) { - std::cout << "calling orignal res" <(user_data); PybammFunctions python_functions = *python_functions_ptr; @@ -143,7 +138,6 @@ int residual(realtype tres, N_Vector yy, N_Vector yp, N_Vector rr, { rval[i] = r_np_ptr[i]; } - std::cout << "back in original res" <(user_data); PybammFunctions python_functions = *python_functions_ptr; - realtype *yval = N_VGetArrayPointer(yy); - realtype *ypval = N_VGetArrayPointer(yp); - realtype *ySval = N_VGetArrayPointer(yS[0]); - realtype *ypSval = N_VGetArrayPointer(ypS[0]); - realtype *resvalSval = N_VGetArrayPointer(resvalS[0]); - int n = python_functions.number_of_states; int np = python_functions.number_of_parameters; - py::array_t y_np = py::array_t(n, yval); - py::array_t yp_np = py::array_t(n, ypval); - py::array_t yS_np = py::array_t( - std::vector{np, n}, ySval - ); - py::array_t ypS_np = py::array_t( - std::vector{np, n}, ypSval - ); - py::array_t resvalS_np = py::array_t( - std::vector{np, n}, resvalSval - ); - - python_functions.sensitivities( - resvalS_np, t, y_np, yp_np, yS_np, ypS_np - ); + // memory managed by sundials, so pass a destructor that does nothing + auto state_vector_shape = std::vector{n, 1}; + np_array y_np = np_array(state_vector_shape, N_VGetArrayPointer(yy), + py::capsule(&yy, [](void* p) {})); + np_array yp_np = np_array(state_vector_shape, N_VGetArrayPointer(yp), + py::capsule(&yp, [](void* p) {})); + + std::vector yS_np(np); + for (int i = 0; i < np; i++) { + auto capsule = py::capsule(yS + i, [](void* p) {}); + yS_np[i] = np_array(state_vector_shape, N_VGetArrayPointer(yS[i]), capsule); + } + + std::vector ypS_np(np); + for (int i = 0; i < np; i++) { + auto capsule = py::capsule(ypS + i, [](void* p) {}); + ypS_np[i] = np_array(state_vector_shape, N_VGetArrayPointer(ypS[i]), capsule); + } + + std::vector resvalS_np(np); + for (int i = 0; i < np; i++) { + auto capsule = py::capsule(resvalS + i, [](void* p) {}); + resvalS_np[i] = np_array(state_vector_shape, + N_VGetArrayPointer(resvalS[i]), capsule); + } + + realtype *ptr1 = static_cast(resvalS_np[0].request().ptr); + const realtype* resvalSval = N_VGetArrayPointer(resvalS[0]); + + python_functions.sensitivities(resvalS_np, t, y_np, yp_np, yS_np, ypS_np); return 0; } @@ -295,14 +296,15 @@ int sensitivities(int Ns, realtype t, N_Vector yy, N_Vector yp, class Solution { public: - Solution(int retval, np_array t_np, np_array y_np) - : flag(retval), t(t_np), y(y_np) + Solution(int retval, np_array t_np, np_array y_np, np_array yS_np) + : flag(retval), t(t_np), y(y_np), yS(yS_np) { } int flag; np_array t; np_array y; + np_array yS; }; /* main program */ @@ -324,7 +326,7 @@ Solution solve(np_array t_np, np_array y0_np, np_array yp0_np, void *ida_mem; // pointer to memory N_Vector yy, yp, avtol; // y, y', and absolute tolerance N_Vector *yyS, *ypS; // y, y' for sensitivities - realtype rtol, *yval, *ypval, *atval; + realtype rtol, *yval, *ypval, *atval, *ySval; int retval; SUNMatrix J; SUNLinearSolver LS; @@ -341,6 +343,7 @@ Solution solve(np_array t_np, np_array y0_np, np_array yp0_np, // set initial value yval = N_VGetArrayPointer(yy); + ySval = N_VGetArrayPointer(yyS[0]); ypval = N_VGetArrayPointer(yp); atval = N_VGetArrayPointer(avtol); int i; @@ -351,11 +354,9 @@ Solution solve(np_array t_np, np_array y0_np, np_array yp0_np, atval[i] = atol[i]; } - if (number_of_parameters > 0) { - for (int is = 0 ; is < number_of_parameters; is++) { - N_VConst(NZERO, yyS[is]); - N_VConst(NZERO, ypS[is]); - } + for (int is = 0 ; is < number_of_parameters; is++) { + N_VConst(RCONST(0.0), yyS[is]); + N_VConst(RCONST(0.0), ypS[is]); } // allocate memory for solver @@ -393,12 +394,9 @@ Solution solve(np_array t_np, np_array y0_np, np_array yp0_np, if (number_of_parameters > 0) { - std::cout << "running sensitivities with np = " << number_of_parameters << std::endl; retval = IDASensInit(ida_mem, number_of_parameters, IDA_SIMULTANEOUS, sensitivities, yyS, ypS); - std::cout << "retval from IDASensInit is " << retval << std::endl; retval = IDASensEEtolerances(ida_mem); - std::cout << "retval from IDASensEEtolerances is " << retval << std::endl; } int t_i = 1; @@ -409,16 +407,21 @@ Solution solve(np_array t_np, np_array y0_np, np_array yp0_np, // set return vectors std::vector t_return(number_of_timesteps); std::vector y_return(number_of_timesteps * number_of_states); + std::vector yS_return(number_of_parameters * number_of_timesteps * number_of_states); t_return[0] = t(0); - int j; - for (j = 0; j < number_of_states; j++) + for (int j = 0; j < number_of_states; j++) { y_return[j] = yval[j]; } + for (int j = 0; j < number_of_parameters; j++) { + const int base_index = j * number_of_timesteps * number_of_states; + for (int k = 0; k < number_of_states; k++) { + yS_return[base_index + k] = ySval[j * number_of_states + k]; + } + } // calculate consistent initial conditions - std::cout << "calculating ICs" << std::endl; N_Vector id; auto id_np_val = rhs_alg_id.unchecked<1>(); id = N_VNew_Serial(number_of_states); @@ -433,33 +436,34 @@ Solution solve(np_array t_np, np_array y0_np, np_array yp0_np, IDASetId(ida_mem, id); IDACalcIC(ida_mem, IDA_YA_YDP_INIT, t(1)); - std::cout << "finished calculating ICs" << std::endl; while (true) { t_next = t(t_i); - std::cout << "next time step "< 0) { + N_VDestroyVectorArray(yyS, number_of_parameters); + N_VDestroyVectorArray(ypS, number_of_parameters); + } - py::array_t t_ret = py::array_t((t_i + 1), &t_return[0]); - py::array_t y_ret = - py::array_t((t_i + 1) * number_of_states, &y_return[0]); + np_array t_ret = np_array(t_i, &t_return[0]); + np_array y_ret = np_array(t_i * number_of_states, &y_return[0]); + np_array yS_ret = np_array( + std::vector{number_of_parameters, t_i, number_of_states}, + &yS_return[0] + ); - Solution sol(retval, t_ret, y_ret); + Solution sol(retval, t_ret, y_ret, yS_ret); return sol; } @@ -486,6 +497,8 @@ PYBIND11_MODULE(idaklu, m) { m.doc() = "sundials solvers"; // optional module docstring + py::bind_vector>(m, "VectorNdArray"); + m.def("solve", &solve, "The solve function", py::arg("t"), py::arg("y0"), py::arg("yp0"), py::arg("res"), py::arg("jac"), py::arg("sens"), py::arg("get_jac_data"), @@ -498,5 +511,6 @@ PYBIND11_MODULE(idaklu, m) py::class_(m, "solution") .def_readwrite("t", &Solution::t) .def_readwrite("y", &Solution::y) + .def_readwrite("yS", &Solution::yS) .def_readwrite("flag", &Solution::flag); } diff --git a/pybamm/solvers/idaklu_solver.py b/pybamm/solvers/idaklu_solver.py index 349cea5ab5..7a9fca801e 100644 --- a/pybamm/solvers/idaklu_solver.py +++ b/pybamm/solvers/idaklu_solver.py @@ -194,11 +194,13 @@ def _integrate(self, model, t_eval, inputs_dict=None): rtol = self._rtol atol = self._check_atol_type(atol, y0.size) - mass_matrix = model.mass_matrix.entries + if model.convert_to_format == "jax": + mass_matrix = model.mass_matrix.entries.toarray() + else: + mass_matrix = model.mass_matrix.entries # construct residuals function by binding inputs def resfn(t, y, ydot): - print("calling python res") return model.residuals_eval(t, y, ydot, inputs) if model.jacobian_eval: @@ -262,7 +264,6 @@ def rootfn(t, y): number_of_sensitivity_parameters = 0 if model.sensitivities_eval is not None: sens0 = model.sensitivities_eval(t=0, y=y0, inputs=inputs) - print('sensitivities found are = ', sens0.keys()) number_of_sensitivity_parameters = len(sens0.keys()) def sensfn(resvalS, t, y, yp, yS, ypS): @@ -283,28 +284,23 @@ def sensfn(resvalS, t, y, yp, yS, ypS): time value y: ndarray of shape (n) current state vector - yp: ndarray of shape (n) + yp: list (np) of ndarray of shape (n) current time derivative of state vector - yS: ndarray of shape (n) + yS: list (np) of ndarray of shape (n) current state vector of sensitivity equations - ypS: ndarray of shape (n) + ypS: list (np) of ndarray of shape (n) current time derivative of state vector of sensitivity equations """ - print('calling python sensfn') - np, n = resvalS.shape + np = len(resvalS) + n = resvalS[0].shape[0] dFdy = model.jacobian_eval(t, y, inputs) dFdyd = mass_matrix dFdp = model.sensitivities_eval(t, y, inputs) - print('dFdy', dFdy.shape) - print('dFdyd', dFdyd.shape) - print('dFdp_i', list(dFdp.values())[0].shape) - print('yS_i', yS[0].shape) - print('ypS_i', ypS[0].shape) for i, dFdp_i in enumerate(dFdp.values()): - resvalS[i] = dFdy @ yS[i] + dFdyd @ ypS[i] + dFdp_i.reshape(-1) + resvalS[i][:] = dFdy @ yS[i] - dFdyd @ ypS[i] + dFdp_i # solve timer = pybamm.Timer() @@ -335,6 +331,12 @@ def sensfn(resvalS, t, y, yp, yS, ypS): y_out = sol.y.reshape((number_of_timesteps, number_of_states)) # return solution, we need to tranpose y to match scipy's interface + if number_of_sensitivity_parameters != 0: + yS_out = { + name: sol.yS[i].transpose() for i, name in enumerate(sens0.keys()) + } + else: + yS_out = None if sol.flag in [0, 2]: # 0 = solved for all t_eval if sol.flag == 0: @@ -351,6 +353,7 @@ def sensfn(resvalS, t, y, yp, yS, ypS): t[-1], np.transpose(y_out[-1])[:, np.newaxis], termination, + sensitivities=yS_out, ) sol.integration_time = integration_time return sol diff --git a/pybamm/solvers/solution.py b/pybamm/solvers/solution.py index ec743050b0..f25c67f3dc 100644 --- a/pybamm/solvers/solution.py +++ b/pybamm/solvers/solution.py @@ -53,6 +53,7 @@ def __init__( t_event=None, y_event=None, termination="final time", + sensitivities=None ): if not isinstance(all_ts, list): all_ts = [all_ts] @@ -63,6 +64,7 @@ def __init__( self._all_ts = all_ts self._all_ys = all_ys self._all_models = all_models + self._sensitivities = sensitivities self._t_event = t_event self._y_event = y_event @@ -70,13 +72,16 @@ def __init__( # Set up inputs if not isinstance(all_inputs, list): - for key, value in all_inputs.items(): + all_inputs_copy = dict(all_inputs) + for key, value in all_inputs_copy.items(): if isinstance(value, numbers.Number): - all_inputs[key] = np.array([value]) - all_inputs = [all_inputs] - self.all_inputs = all_inputs + all_inputs_copy[key] = np.array([value]) + self.all_inputs = [all_inputs_copy] + else: + self.all_inputs = all_inputs + self.has_symbolic_inputs = any( - isinstance(v, casadi.MX) for v in all_inputs[0].values() + isinstance(v, casadi.MX) for v in self.all_inputs[0].values() ) # Copy the timescale_eval and lengthscale_evals if they exist @@ -130,6 +135,10 @@ def y(self): self.set_y() return self._y + @property + def sensitivities(self): + """Values of the sensitivities. Returns a dict of param_name: np_array""" + return self._sensitivities def set_y(self): try: diff --git a/tests/unit/test_solvers/test_idaklu_solver.py b/tests/unit/test_solvers/test_idaklu_solver.py index 28f1ce54ee..52f68a3e57 100644 --- a/tests/unit/test_solvers/test_idaklu_solver.py +++ b/tests/unit/test_solvers/test_idaklu_solver.py @@ -50,7 +50,7 @@ def test_ida_roberts_klu_sensitivities(self): # this test implements a python version of the ida Roberts # example provided in sundials # see sundials ida examples pdf - for form in ["python", "casadi"]: + for form in ["python", "casadi", "jax"]: model = pybamm.BaseModel() model.convert_to_format = form u = pybamm.Variable("u") @@ -59,7 +59,6 @@ def test_ida_roberts_klu_sensitivities(self): model.rhs = {u: a * v} model.algebraic = {v: 1 - v} model.initial_conditions = {u: 0, v: 1} - model.events = [pybamm.Event("1", u - 0.2), pybamm.Event("2", v)] disc = pybamm.Discretisation() disc.process_model(model) @@ -74,20 +73,13 @@ def test_ida_roberts_klu_sensitivities(self): calculate_sensitivities=True ) - # test that final time is time of event - # y = 0.1 t + y0 so y=0.2 when t=2 - np.testing.assert_array_almost_equal(sol.t[-1], 2.0) - - # test that final value is the event value - np.testing.assert_array_almost_equal(sol.y[0, -1], 0.2) - # test that y[1] remains constant np.testing.assert_array_almost_equal( sol.y[1, :], np.ones(sol.t.shape) ) # test that y[0] = to true solution - true_solution = 0.1 * sol.t + true_solution = a_value * sol.t np.testing.assert_array_almost_equal(sol.y[0, :], true_solution) # evaluate the sensitivities using idas From 47c5345b143e4e826ecd1643febe5a076ad69db3 Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Mon, 14 Jun 2021 17:35:49 +0100 Subject: [PATCH 23/73] #1477 flake8 --- pybamm/expression_tree/operations/evaluate.py | 4 ++-- pybamm/solvers/base_solver.py | 3 +++ pybamm/solvers/idaklu_solver.py | 2 -- tests/unit/test_solvers/test_base_solver.py | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/pybamm/expression_tree/operations/evaluate.py b/pybamm/expression_tree/operations/evaluate.py index cc16574066..640cfb8c0b 100644 --- a/pybamm/expression_tree/operations/evaluate.py +++ b/pybamm/expression_tree/operations/evaluate.py @@ -626,11 +626,10 @@ def get_sensitivities(self): jacobian_evaluate = jax.jacfwd(self._evaluate_jax, argnums=3 + n) self._sens_evaluate = jax.jit(jacobian_evaluate, - static_argnums=self._static_argnums) + static_argnums=self._static_argnums) return EvaluatorJaxSensitivities(self._sens_evaluate, self._constants) - def debug(self, t=None, y=None, y_dot=None, inputs=None, known_evals=None): # generated code assumes y is a column vector if y is not None and y.ndim == 1: @@ -688,6 +687,7 @@ def evaluate(self, t=None, y=None, y_dot=None, inputs=None, known_evals=None): else: return result + class EvaluatorJaxSensitivities: def __init__(self, jac_evaluate, constants): self._jac_evaluate = jac_evaluate diff --git a/pybamm/solvers/base_solver.py b/pybamm/solvers/base_solver.py index a7b1ab4960..470b6aa8a8 100644 --- a/pybamm/solvers/base_solver.py +++ b/pybamm/solvers/base_solver.py @@ -349,6 +349,7 @@ def jacp(*args, **kwargs): [p_diff] ) # jacp should be a function that returns a dict of sensitivities + def jacp(*args, **kwargs): return {k: v(*args, **kwargs) for k, v in jacp_dict.items()} @@ -1327,6 +1328,7 @@ def function(self, t, y, inputs): else: return self._function(t, y, inputs=inputs, known_evals={})[0] + class SensitivityCallable: """A class that will be called by the solver when integrating""" @@ -1355,6 +1357,7 @@ def function(self, t, y, inputs): self._function(t, y, inputs=inputs, known_evals={}) return {k: v[0] for k, v in ret_with_known_evals.items()} + class Residuals(SolverCallable): """Returns information about residuals at time t and state y""" diff --git a/pybamm/solvers/idaklu_solver.py b/pybamm/solvers/idaklu_solver.py index 7a9fca801e..5319c94702 100644 --- a/pybamm/solvers/idaklu_solver.py +++ b/pybamm/solvers/idaklu_solver.py @@ -293,8 +293,6 @@ def sensfn(resvalS, t, y, yp, yS, ypS): """ - np = len(resvalS) - n = resvalS[0].shape[0] dFdy = model.jacobian_eval(t, y, inputs) dFdyd = mass_matrix dFdp = model.sensitivities_eval(t, y, inputs) diff --git a/tests/unit/test_solvers/test_base_solver.py b/tests/unit/test_solvers/test_base_solver.py index 3c1bd4de30..9293233a20 100644 --- a/tests/unit/test_solvers/test_base_solver.py +++ b/tests/unit/test_solvers/test_base_solver.py @@ -362,7 +362,7 @@ def exact_diff_b(y, a, b): use_inputs = inputs sens = model.sensitivities_eval( - t, y, use_inputs + t, y, use_inputs ) np.testing.assert_allclose( sens['a'], From b3a30914caa05167e87070baad97d22cfb386e28 Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Mon, 14 Jun 2021 21:39:40 +0100 Subject: [PATCH 24/73] #1477 only call IDAGetSens if calculating sensitivities --- pybamm/solvers/c_solvers/idaklu.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pybamm/solvers/c_solvers/idaklu.cpp b/pybamm/solvers/c_solvers/idaklu.cpp index 00afb95c16..2aea5926f2 100644 --- a/pybamm/solvers/c_solvers/idaklu.cpp +++ b/pybamm/solvers/c_solvers/idaklu.cpp @@ -445,7 +445,9 @@ Solution solve(np_array t_np, np_array y0_np, np_array yp0_np, if (retval == IDA_TSTOP_RETURN || retval == IDA_SUCCESS || retval == IDA_ROOT_RETURN) { - IDAGetSens(ida_mem, &tret, yyS); + if (number_of_parameters > 0) { + IDAGetSens(ida_mem, &tret, yyS); + } t_return[t_i] = tret; for (int j = 0; j < number_of_states; j++) From 0cbe0a5bafe8166253d1888ff968889c831cd08a Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Mon, 28 Jun 2021 17:01:01 +0100 Subject: [PATCH 25/73] #1477 fix some bugs after merge --- pybamm/solvers/base_solver.py | 67 +++--- pybamm/solvers/casadi_algebraic_solver.py | 5 + pybamm/solvers/idaklu_solver.py | 3 + pybamm/solvers/solution.py | 201 +++++++++++------- tests/unit/test_solvers/test_idaklu_solver.py | 54 +++++ 5 files changed, 222 insertions(+), 108 deletions(-) diff --git a/pybamm/solvers/base_solver.py b/pybamm/solvers/base_solver.py index 065094cb06..ff3d7223ce 100644 --- a/pybamm/solvers/base_solver.py +++ b/pybamm/solvers/base_solver.py @@ -37,15 +37,15 @@ class BaseSolver(object): The tolerance to assert whether extrapolation occurs or not. Default is 0. sensitivity : str, optional Whether (and how) to calculate sensitivities when solving. Options are: - - None (default): user must give the names of input parameters to calculate - sensitivity via the "solve" method, the individual solver is responsible for + - None (default): the individual solver is responsible for calculating the sensitivity wrt these parameters, and providing the result in the solution instance returned. At the moment this is only implemented for the IDAKLU solver.\ - - "explicit forward": explicitly formulate the sensitivity equations for *all* - the input parameters. The formulation is as per "Park, S., Kato, D., Gima, Z., \ - Klein, R., & Moura, S. (2018). Optimal experimental design for parameterization\ - of an electrochemical lithium-ion battery model. Journal of The Electrochemical\ + - "explicit forward": explicitly formulate the sensitivity equations for + the chosen input parameters. The formulation is as per + "Park, S., Kato, D., Gima, Z., Klein, R., & Moura, S. (2018).\ + Optimal experimental design for parameterization of an electrochemical + lithium-ion battery model. Journal of The Electrochemical\ Society, 165(7), A1309.". See #1100 for details. At the moment this is only implemented using convert_to_format = 'casadi'. \ - see individual solvers for other options @@ -140,6 +140,8 @@ def copy(self): new_solver.models_set_up = {} return new_solver + + def set_up(self, model, inputs=None, t_eval=None, calculate_sensitivites=False): """Unpack model, perform checks, and calculate jacobian. @@ -236,6 +238,9 @@ def set_up(self, model, inputs=None, t_eval=None, calculate_sensitivites = [p for p in inputs.keys()] else: calculate_sensitivites = [] + # save sensitivity parameters so we can identify them later on + # (FYI: this is used in the Solution class) + model.calculate_sensitivities = calculate_sensitivites # Only allow solving explicit sensitivity equations with the casadi format for now if ( @@ -269,8 +274,13 @@ def set_up(self, model, inputs=None, t_eval=None, p_casadi_stacked = casadi.vertcat(*[p for p in p_casadi.values()]) # sensitivity vectors if self.sensitivity == "explicit forward": - S_x = casadi.MX.sym("S_x", model.len_rhs * p_casadi_stacked.shape[0]) - S_z = casadi.MX.sym("S_z", model.len_alg * p_casadi_stacked.shape[0]) + pS_casadi_stacked = casadi.vertcat( + *[p_casadi[name] for name in calculate_sensitivites] + ) + model.len_rhs_sens = model.len_rhs * pS_casadi_stacked.shape[0] + model.len_alg_sens = model.len_alg * pS_casadi_stacked.shape[0] + S_x = casadi.MX.sym("S_x", model.len_rhs_sens) + S_z = casadi.MX.sym("S_z", model.len_alg_sens) y_and_S = casadi.vertcat(y_diff, S_x, y_alg, S_z) else: y_and_S = y_casadi @@ -356,16 +366,16 @@ def jacp(*args, **kwargs): if name == "rhs" and model.len_rhs > 0: report("Creating sensitivity equations for rhs using CasADi") df_dx = casadi.jacobian(func, y_diff) - df_dp = casadi.jacobian(func, p_casadi_stacked) + df_dp = casadi.jacobian(func, pS_casadi_stacked) S_x_mat = S_x.reshape( - (model.len_rhs, p_casadi_stacked.shape[0]) + (model.len_rhs, pS_casadi_stacked.shape[0]) ) if model.len_alg == 0: S_rhs = (df_dx @ S_x_mat + df_dp).reshape((-1, 1)) else: df_dz = casadi.jacobian(func, y_alg) S_z_mat = S_z.reshape( - (model.len_alg, p_casadi_stacked.shape[0]) + (model.len_alg, pS_casadi_stacked.shape[0]) ) S_rhs = (df_dx @ S_x_mat + df_dz @ S_z_mat + df_dp).reshape( (-1, 1) @@ -376,16 +386,16 @@ def jacp(*args, **kwargs): "Creating sensitivity equations for algebraic using CasADi" ) dg_dz = casadi.jacobian(func, y_alg) - dg_dp = casadi.jacobian(func, p_casadi_stacked) + dg_dp = casadi.jacobian(func, pS_casadi_stacked) S_z_mat = S_z.reshape( - (model.len_alg, p_casadi_stacked.shape[0]) + (model.len_alg, pS_casadi_stacked.shape[0]) ) if model.len_rhs == 0: S_alg = (dg_dz @ S_z_mat + dg_dp).reshape((-1, 1)) else: dg_dx = casadi.jacobian(func, y_diff) S_x_mat = S_x.reshape( - (model.len_rhs, p_casadi_stacked.shape[0]) + (model.len_rhs, pS_casadi_stacked.shape[0]) ) S_alg = (dg_dx @ S_x_mat + dg_dz @ S_z_mat + dg_dp).reshape( (-1, 1) @@ -393,17 +403,17 @@ def jacp(*args, **kwargs): func = casadi.vertcat(func, S_alg) elif name == "initial_conditions": if model.len_rhs == 0 or model.len_alg == 0: - S_0 = casadi.jacobian(func, p_casadi_stacked).reshape( + S_0 = casadi.jacobian(func, pS_casadi_stacked).reshape( (-1, 1) ) func = casadi.vertcat(func, S_0) else: x0 = func[: model.len_rhs] z0 = func[model.len_rhs :] - Sx_0 = casadi.jacobian(x0, p_casadi_stacked).reshape( + Sx_0 = casadi.jacobian(x0, pS_casadi_stacked).reshape( (-1, 1) ) - Sz_0 = casadi.jacobian(z0, p_casadi_stacked).reshape( + Sz_0 = casadi.jacobian(z0, pS_casadi_stacked).reshape( (-1, 1) ) func = casadi.vertcat(x0, Sx_0, z0, Sz_0) @@ -416,7 +426,7 @@ def jacp(*args, **kwargs): else: jac = None - if calculate_sensitivites: + if calculate_sensitivites and self.sensitivity != "explicit forward": report(( f"Calculating sensitivities for {name} with respect " f"to parameters {calculate_sensitivites} using CasADi" @@ -529,12 +539,11 @@ def jacp(*args, **kwargs): init_eval = InitialConditions(initial_conditions, model) if self.sensitivity == "explicit forward": - init_eval.y_dummy = np.zeros( - ( - model.len_rhs_and_alg * (np.vstack(list(inputs.values())).size + 1), - 1, - ) + y0_total_size = ( + model.len_rhs + model.len_rhs_sens + + model.len_alg + model.len_alg_sens ) + init_eval.y_dummy = np.zeros((y0_total_size, 1)) else: init_eval.y_dummy = np.zeros((model.len_rhs_and_alg, 1)) @@ -546,6 +555,7 @@ def jacp(*args, **kwargs): # Calculate initial conditions model.y0 = init_eval(inputs) + print('YYYYY', model.y0) casadi_terminate_events = [] terminate_events_eval = [] @@ -710,6 +720,7 @@ def _set_initial_conditions(self, model, inputs, update_rhs): model.y0 = casadi.Function("y0", [symbolic_inputs], [y0]) else: model.y0 = y0 + print('ASDF', model.y0) def calculate_consistent_state(self, model, time=0, inputs=None): """ @@ -736,13 +747,15 @@ def calculate_consistent_state(self, model, time=0, inputs=None): if self.root_method is None: return model.y0 try: - root_sol = self.root_method._integrate(model, [time], inputs) + root_sol = self.root_method._integrate(model, np.array([time]), inputs) except pybamm.SolverError as e: raise pybamm.SolverError( "Could not find consistent states: {}".format(e.args[0]) ) pybamm.logger.debug("Found consistent states") - y0 = root_sol.all_ys[0] + + # use all_ys_and_sens in case we are solving the full sensitivity equations + y0 = root_sol.all_ys_and_sens[0] if isinstance(y0, np.ndarray): y0 = y0.flatten() return y0 @@ -1428,7 +1441,7 @@ def __call__(self, t, y, inputs): self.name, self.model.name, t * self.timescale ) ) - if self.name in ["RHS", "algebraic", "residuals"]: + if self.name in ["RHS", "algebraic", "residuals", "event"]: return self.function(t, y, inputs).flatten() else: @@ -1437,7 +1450,7 @@ def __call__(self, t, y, inputs): def function(self, t, y, inputs): if self.form == "casadi": states_eval = self._function(t, y, inputs) - if self.name in ["rhs", "algebraic", "residuals", "event"]: + if self.name in ["RHS", "algebraic", "residuals", "event"]: return states_eval.full() else: # keep jacobians sparse diff --git a/pybamm/solvers/casadi_algebraic_solver.py b/pybamm/solvers/casadi_algebraic_solver.py index 2ce2fd3379..2db859a858 100644 --- a/pybamm/solvers/casadi_algebraic_solver.py +++ b/pybamm/solvers/casadi_algebraic_solver.py @@ -76,12 +76,14 @@ def _integrate(self, model, t_eval, inputs_dict=None): inputs = casadi.vertcat(*[v for v in inputs_dict.values()]) y0 = model.y0 + print('algebraic', y0) # If y0 already satisfies the tolerance for all t then keep it if self.sensitivity != "casadi" and all( np.all(abs(model.casadi_algebraic(t, y0, inputs).full()) < self.tol) for t in t_eval ): + print('keeping soln', y0.full()) pybamm.logger.debug("Keeping same solution at all times") return pybamm.Solution( t_eval, y0, model, inputs_dict, termination="success" @@ -92,14 +94,17 @@ def _integrate(self, model, t_eval, inputs_dict=None): # equations will be equal to the initial condition provided. This allows this # solver to be used for initialising the DAE solvers if model.rhs == {}: + print('no rhs') len_rhs = 0 y0_diff = casadi.DM() y0_alg = y0 else: # Check y0 to see if it includes sensitivities if model.len_rhs_and_alg == y0.shape[0]: + print('doesnt include sens') len_rhs = model.len_rhs else: + print('includes sens', inputs.shape[0]) len_rhs = model.len_rhs * (inputs.shape[0] + 1) y0_diff = y0[:len_rhs] y0_alg = y0[len_rhs:] diff --git a/pybamm/solvers/idaklu_solver.py b/pybamm/solvers/idaklu_solver.py index edeedc1661..0aaea0c85e 100644 --- a/pybamm/solvers/idaklu_solver.py +++ b/pybamm/solvers/idaklu_solver.py @@ -188,9 +188,12 @@ def _integrate(self, model, t_eval, inputs_dict=None): atol = self._atol y0 = model.y0 + print('idaklu, y0', y0) if isinstance(y0, casadi.DM): y0 = y0.full().flatten() + print('idaklu, y0', y0) + rtol = self._rtol atol = self._check_atol_type(atol, y0.size) diff --git a/pybamm/solvers/solution.py b/pybamm/solvers/solution.py index fe733e7446..961c25651c 100644 --- a/pybamm/solvers/solution.py +++ b/pybamm/solvers/solution.py @@ -41,6 +41,9 @@ class Solution(object): the event happens. termination : str String to indicate why the solution terminated + sensitivities: None or dict + Will be None if there are no sensitivities in this soluion. Otherwise, this is a + dict of parameter names to their calcululated sensitivities """ @@ -63,12 +66,8 @@ def __init__( all_models = [all_models] self._all_ts = all_ts self._all_ys = all_ys + self._all_ys_and_sens = all_ys self._all_models = all_models - self._sensitivities = sensitivities - - self._t_event = t_event - self._y_event = y_event - self._termination = termination # Set up inputs if not isinstance(all_inputs, list): @@ -80,6 +79,29 @@ def __init__( else: self.all_inputs = all_inputs + # sensitivities + if sensitivities is None: + self._sensitivities = {} + # if solution consists of explicit sensitivity equations, extract them + if ( + all_models[0] is not None + and not isinstance(all_ys[0], casadi.Function) + and all_models[0].len_rhs_and_alg != all_ys[0].shape[0] + and all_models[0].len_rhs_and_alg != 0 # for the dummy solver + ): + # save original ys[0] and replace with separated soln + self._all_ys_and_sens = [self._all_ys[0][:]] + self._all_ys[0], self._sensitivities = \ + self._extract_explicit_sensitivities( + all_models[0], all_ys[0], all_ts[0], self.all_inputs[0] + ) + else: + self._sensitivities = sensitivities + + self._t_event = t_event + self._y_event = y_event + self._termination = termination + self.has_symbolic_inputs = any( isinstance(v, casadi.MX) for v in self.all_inputs[0].values() ) @@ -98,82 +120,6 @@ def __init__( for domain, scale in all_models[0].length_scales.items() } - # If the model has been provided, split up y into solution and sensitivity - # Don't do this if the sensitivity equations have not been computed (i.e. if - # y only has the shape or the rhs and alg solution) - # Don't do this if y is symbolic (sensitivities will be calculated a different - # way) - if ( - model is None - or isinstance(all_ys[0], casadi.Function) - or model.len_rhs_and_alg == all_ys[0].shape[0] - or model.len_rhs_and_alg == 0 # for the dummy solver - ): - self.all_ys = all_ys - self.sensitivity = {} - else: - n_states = model.len_rhs_and_alg - n_rhs = model.len_rhs - n_alg = model.len_alg - n_p = self.all_inputs_casadi[0].size - # Get the point where the algebraic equations start - len_rhs_and_sens = (n_p + 1) * model.len_rhs - for idx, y in enumerate(all_ys): - n_t = len(self.all_ts[idx]) - # y gets the part of the solution vector that correspond to the - # actual ODE/DAE solution - all_ys[idx] = np.vstack( - [ - y[: model.len_rhs, :], - y[len_rhs_and_sens : len_rhs_and_sens + model.len_alg, :], - ] - ) - # save sensitivities as a dictionary - # first save the whole sensitivity matrix - # reshape using Fortran order to get the right array: - # t0_x0_p0, t0_x0_p1, ..., t0_x0_pn - # t0_x1_p0, t0_x1_p1, ..., t0_x1_pn - # ... - # t0_xn_p0, t0_xn_p1, ..., t0_xn_pn - # t1_x0_p0, t1_x0_p1, ..., t1_x0_pn - # t1_x1_p0, t1_x1_p1, ..., t1_x1_pn - # ... - # t1_xn_p0, t1_xn_p1, ..., t1_xn_pn - # ... - # tn_x0_p0, tn_x0_p1, ..., tn_x0_pn - # tn_x1_p0, tn_x1_p1, ..., tn_x1_pn - # ... - # tn_xn_p0, tn_xn_p1, ..., tn_xn_pn - # 1, Extract rhs and alg sensitivities and reshape into 3D matrices - # with shape (n_p, n_states, n_t) - if isinstance(y, casadi.DM): - y_full = y.full() - else: - y_full = y - ode_sens = y_full[n_rhs:len_rhs_and_sens, :].reshape(n_p, n_rhs, n_t) - alg_sens = y_full[len_rhs_and_sens + n_alg :, :].reshape( - n_p, n_alg, n_t - ) - # 2. Concatenate into a single 3D matrix with shape (n_p, n_states, n_t) - # i.e. along first axis - full_sens_matrix = np.concatenate([ode_sens, alg_sens], axis=1) - # Transpose and reshape into a (n_states * n_t, n_p) matrix - full_sens_matrix = full_sens_matrix.transpose(2, 1, 0).reshape( - n_t * n_states, n_p - ) - - # Save the full sensitivity matrix - sensitivity = {"all": full_sens_matrix} - # also save the sensitivity wrt each parameter (read the columns of the - # sensitivity matrix) - start = 0 - for i, (name, inp) in enumerate(self.all_inputs[0].items()): - input_size = inp.shape[0] - end = start + input_size - sensitivity[name] = full_sens_matrix[:, start:end] - start = end - self.all_sensitivities[idx] = sensitivity - # Events self._t_event = t_event self._y_event = y_event @@ -194,6 +140,95 @@ def __init__( # Solution now uses CasADi pybamm.citations.register("Andersson2019") + def _extract_explicit_sensitivities(self, model, y, t_eval, inputs): + """ + given a model and a solution y, extracts the sensitivities + + Parameters + -------- + model : :class:`pybamm.BaseModel` + A model that has been already setup by this base solver + y: ndarray + The solution of the full explicit sensitivity equations + t_eval: ndarray + The evaluation times + inputs: dict + parameter inputs + + Returns + ------- + y: ndarray + The solution of the ode/dae in model + sensitivities: dict of (string: ndarray) + A dictionary of parameter names, and the corresponding solution of + the sensitivity equations + """ + + n_states = model.len_rhs_and_alg + n_rhs = model.len_rhs + n_alg = model.len_alg + # Get the point where the algebraic equations start + n_p = model.len_rhs_sens // model.len_rhs + len_rhs_and_sens = model.len_rhs + model.len_rhs_sens + + n_t = len(t_eval) + # y gets the part of the solution vector that correspond to the + # actual ODE/DAE solution + + # save sensitivities as a dictionary + # first save the whole sensitivity matrix + # reshape using Fortran order to get the right array: + # t0_x0_p0, t0_x0_p1, ..., t0_x0_pn + # t0_x1_p0, t0_x1_p1, ..., t0_x1_pn + # ... + # t0_xn_p0, t0_xn_p1, ..., t0_xn_pn + # t1_x0_p0, t1_x0_p1, ..., t1_x0_pn + # t1_x1_p0, t1_x1_p1, ..., t1_x1_pn + # ... + # t1_xn_p0, t1_xn_p1, ..., t1_xn_pn + # ... + # tn_x0_p0, tn_x0_p1, ..., tn_x0_pn + # tn_x1_p0, tn_x1_p1, ..., tn_x1_pn + # ... + # tn_xn_p0, tn_xn_p1, ..., tn_xn_pn + # 1, Extract rhs and alg sensitivities and reshape into 3D matrices + # with shape (n_p, n_states, n_t) + if isinstance(y, casadi.DM): + y_full = y.full() + else: + y_full = y + ode_sens = y_full[n_rhs:len_rhs_and_sens, :].reshape(n_p, n_rhs, n_t) + alg_sens = y_full[len_rhs_and_sens + n_alg :, :].reshape( + n_p, n_alg, n_t + ) + # 2. Concatenate into a single 3D matrix with shape (n_p, n_states, n_t) + # i.e. along first axis + full_sens_matrix = np.concatenate([ode_sens, alg_sens], axis=1) + # Transpose and reshape into a (n_states * n_t, n_p) matrix + full_sens_matrix = full_sens_matrix.transpose(2, 1, 0).reshape( + n_t * n_states, n_p + ) + + # Save the full sensitivity matrix + sensitivity = {"all": full_sens_matrix} + # also save the sensitivity wrt each parameter (read the columns of the + # sensitivity matrix) + start = 0 + for name in model.calculate_sensitivities: + inp = inputs[name] + input_size = inp.shape[0] + end = start + input_size + sensitivity[name] = full_sens_matrix[:, start:end] + start = end + + y_dae = np.vstack( + [ + y[: model.len_rhs, :], + y[len_rhs_and_sens : len_rhs_and_sens + model.len_alg, :], + ] + ) + return y_dae, sensitivity + @property def t(self): """Times at which the solution is evaluated""" @@ -242,6 +277,10 @@ def all_ts(self): def all_ys(self): return self._all_ys + @property + def all_ys_and_sens(self): + return self._all_ys_and_sens + @property def all_models(self): """Model(s) used for solution""" diff --git a/tests/unit/test_solvers/test_idaklu_solver.py b/tests/unit/test_solvers/test_idaklu_solver.py index 52f68a3e57..50b64de2bc 100644 --- a/tests/unit/test_solvers/test_idaklu_solver.py +++ b/tests/unit/test_solvers/test_idaklu_solver.py @@ -51,6 +51,7 @@ def test_ida_roberts_klu_sensitivities(self): # example provided in sundials # see sundials ida examples pdf for form in ["python", "casadi", "jax"]: + print('FORM', form) model = pybamm.BaseModel() model.convert_to_format = form u = pybamm.Variable("u") @@ -95,6 +96,59 @@ def test_ida_roberts_klu_sensitivities(self): dyda_ida, dyda_fd ) + def test_ida_roberts_klu_sensitivities_explicit(self): + # this test implements a python version of the ida Roberts + # example provided in sundials + # see sundials ida examples pdf + model = pybamm.BaseModel() + model.convert_to_format = "casadi" + u = pybamm.Variable("u") + v = pybamm.Variable("v") + a = pybamm.InputParameter("a") + b = pybamm.InputParameter("b") + model.rhs = {u: a * b * v} + model.algebraic = {v: 1 - v} + model.initial_conditions = {u: 0, v: 1} + + disc = pybamm.Discretisation() + disc.process_model(model) + + solver = pybamm.IDAKLUSolver( + sensitivity='explicit forward' + ) + + t_eval = np.linspace(0, 3, 100) + a_value = 0.1 + print('starting solve.....') + sol = solver.solve( + model, t_eval, inputs={"a": a_value, "b": 1}, + calculate_sensitivities=True + ) + + # test that y[1] remains constant + np.testing.assert_array_almost_equal( + sol.y[1, :], np.ones(sol.t.shape) + ) + + # test that y[0] = to true solution + true_solution = a_value * sol.t + np.testing.assert_array_almost_equal(sol.y[0, :], true_solution) + + # evaluate the sensitivities + dyda_ida = sol.sensitivities["a"] + + # evaluate the sensitivities using finite difference + h = 1e-6 + sol_plus = solver.solve(model, t_eval, inputs={"a": a_value + 0.5 * h}) + sol_neg = solver.solve(model, t_eval, inputs={"a": a_value - 0.5 * h}) + dyda_fd = (sol_plus.y - sol_neg.y) / h + + np.testing.assert_array_almost_equal( + dyda_ida, dyda_fd + ) + + + def test_set_atol(self): model = pybamm.lithium_ion.DFN() geometry = model.default_geometry From 52149941881cd97ba041e9fdffd3dfebca9921bc Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Mon, 5 Jul 2021 17:32:32 +0100 Subject: [PATCH 26/73] #1477 generalising 'explicit forward' option so any solver can use it --- pybamm/solvers/base_solver.py | 35 +++++++++++++++++++-------------- pybamm/solvers/casadi_solver.py | 30 +++++++++++++++++++++++----- pybamm/solvers/solution.py | 19 +++++++++++------- 3 files changed, 57 insertions(+), 27 deletions(-) diff --git a/pybamm/solvers/base_solver.py b/pybamm/solvers/base_solver.py index ff3d7223ce..8cd2f097b9 100644 --- a/pybamm/solvers/base_solver.py +++ b/pybamm/solvers/base_solver.py @@ -8,7 +8,7 @@ import numpy as np import sys import itertools -from scipy.linalg import block_diag +from scipy.sparse import block_diag import multiprocessing as mp import warnings @@ -241,6 +241,8 @@ def set_up(self, model, inputs=None, t_eval=None, # save sensitivity parameters so we can identify them later on # (FYI: this is used in the Solution class) model.calculate_sensitivities = calculate_sensitivites + model.len_rhs_sens = model.len_rhs * len(calculate_sensitivites) + model.len_alg_sens = model.len_alg * len(calculate_sensitivites) # Only allow solving explicit sensitivity equations with the casadi format for now if ( @@ -277,8 +279,6 @@ def set_up(self, model, inputs=None, t_eval=None, pS_casadi_stacked = casadi.vertcat( *[p_casadi[name] for name in calculate_sensitivites] ) - model.len_rhs_sens = model.len_rhs * pS_casadi_stacked.shape[0] - model.len_alg_sens = model.len_alg * pS_casadi_stacked.shape[0] S_x = casadi.MX.sym("S_x", model.len_rhs_sens) S_z = casadi.MX.sym("S_z", model.len_alg_sens) y_and_S = casadi.vertcat(y_diff, S_x, y_alg, S_z) @@ -615,6 +615,21 @@ def jacp(*args, **kwargs): interpolant_extrapolation_events_eval ) + # if we have changed the equations to include the explicit sensitivity + # equations, then we also need to update the mass matrix + if self.sensitivity == "explicit forward": + n_inputs = len(calculate_sensitivites) + model.mass_matrix_inv = pybamm.Matrix( + block_diag( + [model.mass_matrix_inv.entries] * (n_inputs + 1), format="csr" + ) + ) + model.mass_matrix = pybamm.Matrix( + block_diag( + [model.mass_matrix.entries] * (n_inputs + 1), format="csr" + ) + ) + # Save CasADi functions for the CasADi solver # Note: when we pass to casadi the ode part of the problem must be in explicit # form so we pre-multiply by the inverse of the mass matrix @@ -623,16 +638,7 @@ def jacp(*args, **kwargs): ): # can use DAE solver to solve model with algebraic equations only if len(model.rhs) > 0: - if self.sensitivity == "explicit forward": - # Copy mass matrix blocks diagonally - single_mass_matrix_inv = model.mass_matrix_inv.entries.toarray() - n_inputs = p_casadi_stacked.shape[0] - block_mass_matrix = block_diag( - *[single_mass_matrix_inv] * (n_inputs + 1) - ) - mass_matrix_inv = casadi.MX(block_mass_matrix) - else: - mass_matrix_inv = casadi.MX(model.mass_matrix_inv.entries) + mass_matrix_inv = casadi.MX(model.mass_matrix_inv.entries) explicit_rhs = mass_matrix_inv @ rhs( t_casadi, y_and_S, p_casadi_stacked ) @@ -754,8 +760,7 @@ def calculate_consistent_state(self, model, time=0, inputs=None): ) pybamm.logger.debug("Found consistent states") - # use all_ys_and_sens in case we are solving the full sensitivity equations - y0 = root_sol.all_ys_and_sens[0] + y0 = root_sol.all_ys[0] if isinstance(y0, np.ndarray): y0 = y0.flatten() return y0 diff --git a/pybamm/solvers/casadi_solver.py b/pybamm/solvers/casadi_solver.py index d2b9fad697..45ad4dbccd 100644 --- a/pybamm/solvers/casadi_solver.py +++ b/pybamm/solvers/casadi_solver.py @@ -131,6 +131,11 @@ def _integrate(self, model, t_eval, inputs_dict=None): inputs_dict : dict, optional Any external variables or input parameters to pass to the model when solving """ + + + # are we solving explicit forward equations? + explicit_sensitivities = self.sensitivity == 'explicit forward' + # Record whether there are any symbolic inputs inputs_dict = inputs_dict or {} @@ -158,14 +163,15 @@ def _integrate(self, model, t_eval, inputs_dict=None): # Create integrator without grid to avoid having to create several times self.create_integrator(model, inputs) solution = self._run_integrator( - model, model.y0, inputs_dict, inputs, t_eval, use_grid=False + model, model.y0, inputs_dict, inputs, t_eval, use_grid=False, ) if self.sensitivity == "casadi" and inputs_dict != {}: # If the solution has already been created, we can reuse it if model in self.y_sols: y_sol = self.y_sols[model] solution = pybamm.Solution( - t_eval, y_sol, model=model, inputs=inputs_dict + t_eval, y_sol, model=model, inputs=inputs_dict, + sensitivities=explicit_sensitivities ) else: # Create integrator without grid, which will be called repeatedly @@ -212,7 +218,10 @@ def _integrate(self, model, t_eval, inputs_dict=None): # to avoid having to create several times self.create_integrator(model, inputs_dict) # Initialize solution - solution = pybamm.Solution(np.array([t]), y0, model, inputs_dict) + solution = pybamm.Solution( + np.array([t]), y0, model, inputs_dict, + sensitivities=explicit_sensitivities + ) solution.solve_time = 0 solution.integration_time = 0 use_grid = False @@ -455,6 +464,7 @@ def integer_bisect(): np.array([t_event]), y_event[:, np.newaxis], "event", + sensitivities=explicit_sensitivities ) solution.integration_time = ( coarse_solution.integration_time + dense_step_sol.integration_time @@ -613,6 +623,10 @@ def create_integrator(self, model, inputs, t_eval=None, use_event_switch=False): def _run_integrator(self, model, y0, inputs_dict, inputs, t_eval, use_grid=True): pybamm.logger.debug("Running CasADi integrator") + + # are we solving explicit forward equations? + explicit_sensitivities = self.sensitivity == 'explicit forward' + if use_grid is True: t_eval_shifted = t_eval - t_eval[0] t_eval_shifted_rounded = np.round(t_eval_shifted, decimals=12).tobytes() @@ -649,7 +663,10 @@ def _run_integrator(self, model, y0, inputs_dict, inputs, t_eval, use_grid=True) ) integration_time = timer.time() y_sol = casadi.vertcat(casadi_sol["xf"], casadi_sol["zf"]) - sol = pybamm.Solution(t_eval, y_sol, model, inputs_dict) + sol = pybamm.Solution( + t_eval, y_sol, model, inputs_dict, + sensitivities=explicit_sensitivities + ) sol.integration_time = integration_time return sol else: @@ -682,7 +699,10 @@ def _run_integrator(self, model, y0, inputs_dict, inputs, t_eval, use_grid=True) # Save the solution, can just reuse and change the inputs self.y_sols[model] = y_sol - sol = pybamm.Solution(t_eval, y_sol, model, inputs_dict) + sol = pybamm.Solution( + t_eval, y_sol, model, inputs_dict, + sensitivities=explicit_sensitivities + ) sol.integration_time = integration_time return sol except RuntimeError as e: diff --git a/pybamm/solvers/solution.py b/pybamm/solvers/solution.py index 961c25651c..a6ec3f6fc4 100644 --- a/pybamm/solvers/solution.py +++ b/pybamm/solvers/solution.py @@ -41,9 +41,11 @@ class Solution(object): the event happens. termination : str String to indicate why the solution terminated - sensitivities: None or dict - Will be None if there are no sensitivities in this soluion. Otherwise, this is a - dict of parameter names to their calcululated sensitivities + + sensitivities: bool or dict + True if sensitivities included as the solution of the explicit forwards + equations. False if no sensitivities included/wanted. Dict if sensitivities are + provided as a dict of {parameter: sensitivities} pairs. """ @@ -56,7 +58,7 @@ def __init__( t_event=None, y_event=None, termination="final time", - sensitivities=None + sensitivities=False ): if not isinstance(all_ts, list): all_ts = [all_ts] @@ -80,11 +82,12 @@ def __init__( self.all_inputs = all_inputs # sensitivities - if sensitivities is None: + if isinstance(sensitivities, bool): self._sensitivities = {} # if solution consists of explicit sensitivity equations, extract them if ( - all_models[0] is not None + sensitivities == True + and all_models[0] is not None and not isinstance(all_ys[0], casadi.Function) and all_models[0].len_rhs_and_alg != all_ys[0].shape[0] and all_models[0].len_rhs_and_alg != 0 # for the dummy solver @@ -95,8 +98,10 @@ def __init__( self._extract_explicit_sensitivities( all_models[0], all_ys[0], all_ts[0], self.all_inputs[0] ) - else: + elif isinstance(sensitivities, dict): self._sensitivities = sensitivities + else: + raise RuntimeError('sensitivities arg needs to be a bool or dict') self._t_event = t_event self._y_event = y_event From 840f073d7f6713803531e2bde3299cee754e9d4c Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Fri, 16 Jul 2021 11:49:27 +0100 Subject: [PATCH 27/73] #1477 going to take out sensitivity=casadi option --- pybamm/solvers/base_solver.py | 89 ++++++++++--------- pybamm/solvers/casadi_solver.py | 18 ++-- pybamm/solvers/idaklu_solver.py | 8 +- pybamm/solvers/processed_variable.py | 2 +- tests/integration/test_solvers/test_idaklu.py | 42 +++++++++ tests/unit/test_solvers/test_idaklu_solver.py | 73 +++------------ 6 files changed, 114 insertions(+), 118 deletions(-) diff --git a/pybamm/solvers/base_solver.py b/pybamm/solvers/base_solver.py index 8cd2f097b9..60ad08fe59 100644 --- a/pybamm/solvers/base_solver.py +++ b/pybamm/solvers/base_solver.py @@ -42,11 +42,7 @@ class BaseSolver(object): the solution instance returned. At the moment this is only implemented for the IDAKLU solver.\ - "explicit forward": explicitly formulate the sensitivity equations for - the chosen input parameters. The formulation is as per - "Park, S., Kato, D., Gima, Z., Klein, R., & Moura, S. (2018).\ - Optimal experimental design for parameterization of an electrochemical - lithium-ion battery model. Journal of The Electrochemical\ - Society, 165(7), A1309.". See #1100 for details. At the moment this is only + the chosen input parameters. . At the moment this is only implemented using convert_to_format = 'casadi'. \ - see individual solvers for other options """ @@ -60,7 +56,6 @@ def __init__( root_tol=1e-6, extrap_tol=0, max_steps="deprecated", - sensitivity=None, ): self._method = method self._rtol = rtol @@ -79,7 +74,6 @@ def __init__( self.name = "Base solver" self.ode_solver = False self.algebraic_solver = False - self.sensitivity = sensitivity @property def method(self): @@ -140,8 +134,6 @@ def copy(self): new_solver.models_set_up = {} return new_solver - - def set_up(self, model, inputs=None, t_eval=None, calculate_sensitivites=False): """Unpack model, perform checks, and calculate jacobian. @@ -238,21 +230,17 @@ def set_up(self, model, inputs=None, t_eval=None, calculate_sensitivites = [p for p in inputs.keys()] else: calculate_sensitivites = [] + + calculate_sensitivites_explicit = False + if calculate_sensitivites and not isinstance(self, pybamm.IDAKLUSolver): + calculate_sensitivites_explicit = True + # save sensitivity parameters so we can identify them later on # (FYI: this is used in the Solution class) model.calculate_sensitivities = calculate_sensitivites - model.len_rhs_sens = model.len_rhs * len(calculate_sensitivites) - model.len_alg_sens = model.len_alg * len(calculate_sensitivites) - - # Only allow solving explicit sensitivity equations with the casadi format for now - if ( - self.sensitivity == "explicit forward" - and model.convert_to_format != "casadi" - ): - raise NotImplementedError( - "model should be converted to casadi format in order to solve " - "explicit sensitivity equations" - ) + if calculate_sensitivites_explicit: + model.len_rhs_sens = model.len_rhs * len(calculate_sensitivites) + model.len_alg_sens = model.len_alg * len(calculate_sensitivites) if model.convert_to_format != "casadi": # Create Jacobian from concatenated rhs and algebraic @@ -275,7 +263,7 @@ def set_up(self, model, inputs=None, t_eval=None, p_casadi[name] = casadi.MX.sym(name, value.shape[0]) p_casadi_stacked = casadi.vertcat(*[p for p in p_casadi.values()]) # sensitivity vectors - if self.sensitivity == "explicit forward": + if calculate_sensitivites_explicit: pS_casadi_stacked = casadi.vertcat( *[p_casadi[name] for name in calculate_sensitivites] ) @@ -297,15 +285,19 @@ def report(string): if model.convert_to_format == "jax": report(f"Converting {name} to jax") func = pybamm.EvaluatorJax(func) - if calculate_sensitivites: + jacp = None + if calculate_sensitivites_explicit: + raise NotImplementedError( + "sensitivities using convert_to_format = 'jax' " + "only implemented for IDAKLUSolver" + ) + elif calculate_sensitivites: report(( f"Calculating sensitivities for {name} with respect " f"to parameters {calculate_sensitivites} using jax" )) jacp = func.get_sensitivities() jacp = jacp.evaluate - else: - jacp = None if use_jacobian: report(f"Calculating jacobian for {name} using jax") jac = func.get_jacobian() @@ -319,6 +311,10 @@ def report(string): # Process with pybamm functions, optionally converting # to python evaluator if calculate_sensitivites: + raise NotImplementedError( + "sensitivities only implemented with " + "convert_to_format = 'casadi' or convert_to_format = 'jax'" + ) report(( f"Calculating sensitivities for {name} with respect " f"to parameters {calculate_sensitivites}" @@ -362,9 +358,16 @@ def jacp(*args, **kwargs): report(f"Converting {name} to CasADi") func = func.to_casadi(t_casadi, y_casadi, inputs=p_casadi) # Add sensitivity vectors to the rhs and algebraic equations - if self.sensitivity == "explicit forward": + jacp = None + if calculate_sensitivites_explicit: + # The formulation is as per Park, S., Kato, D., Gima, Z., Klein, R., + # & Moura, S. (2018). Optimal experimental design for + # parameterization of an electrochemical lithium-ion battery model. + # Journal of The Electrochemical Society, 165(7), A1309.". See #1100 + # for details if name == "rhs" and model.len_rhs > 0: - report("Creating sensitivity equations for rhs using CasADi") + report( + "Creating explicit forward sensitivity equations for rhs using CasADi") df_dx = casadi.jacobian(func, y_diff) df_dp = casadi.jacobian(func, pS_casadi_stacked) S_x_mat = S_x.reshape( @@ -383,7 +386,7 @@ def jacp(*args, **kwargs): func = casadi.vertcat(func, S_rhs) if name == "algebraic" and model.len_alg > 0: report( - "Creating sensitivity equations for algebraic using CasADi" + "Creating explicit forward sensitivity equations for algebraic using CasADi" ) dg_dz = casadi.jacobian(func, y_alg) dg_dp = casadi.jacobian(func, pS_casadi_stacked) @@ -401,7 +404,12 @@ def jacp(*args, **kwargs): (-1, 1) ) func = casadi.vertcat(func, S_alg) - elif name == "initial_conditions": + if name == "residuals": + raise NotImplementedError( + "explicit forward equations not implimented for residuals" + ) + + if name == "initial_conditions": if model.len_rhs == 0 or model.len_alg == 0: S_0 = casadi.jacobian(func, pS_casadi_stacked).reshape( (-1, 1) @@ -417,16 +425,7 @@ def jacp(*args, **kwargs): (-1, 1) ) func = casadi.vertcat(x0, Sx_0, z0, Sz_0) - if use_jacobian: - report(f"Calculating jacobian for {name} using CasADi") - jac_casadi = casadi.jacobian(func, y_and_S) - jac = casadi.Function( - name, [t_casadi, y_and_S, p_casadi_stacked], [jac_casadi] - ) - else: - jac = None - - if calculate_sensitivites and self.sensitivity != "explicit forward": + elif calculate_sensitivites: report(( f"Calculating sensitivities for {name} with respect " f"to parameters {calculate_sensitivites} using CasADi" @@ -444,8 +443,14 @@ def jacp(*args, **kwargs): return {k: v(*args, **kwargs) for k, v in jacp_dict.items()} + if use_jacobian: + report(f"Calculating jacobian for {name} using CasADi") + jac_casadi = casadi.jacobian(func, y_and_S) + jac = casadi.Function( + name, [t_casadi, y_and_S, p_casadi_stacked], [jac_casadi] + ) else: - jacp = None + jac = None func = casadi.Function( name, [t_casadi, y_and_S, p_casadi_stacked], [func] @@ -538,7 +543,7 @@ def jacp(*args, **kwargs): )[0] init_eval = InitialConditions(initial_conditions, model) - if self.sensitivity == "explicit forward": + if calculate_sensitivites_explicit: y0_total_size = ( model.len_rhs + model.len_rhs_sens + model.len_alg + model.len_alg_sens @@ -555,7 +560,6 @@ def jacp(*args, **kwargs): # Calculate initial conditions model.y0 = init_eval(inputs) - print('YYYYY', model.y0) casadi_terminate_events = [] terminate_events_eval = [] @@ -726,7 +730,6 @@ def _set_initial_conditions(self, model, inputs, update_rhs): model.y0 = casadi.Function("y0", [symbolic_inputs], [y0]) else: model.y0 = y0 - print('ASDF', model.y0) def calculate_consistent_state(self, model, time=0, inputs=None): """ diff --git a/pybamm/solvers/casadi_solver.py b/pybamm/solvers/casadi_solver.py index 45ad4dbccd..2c7fe990a3 100644 --- a/pybamm/solvers/casadi_solver.py +++ b/pybamm/solvers/casadi_solver.py @@ -62,13 +62,6 @@ class CasadiSolver(pybamm.BaseSolver): Any options to pass to the CasADi integrator when calling the integrator. Please consult `CasADi documentation `_ for details. - sensitivity : str, optional - Whether (and how) to calculate sensitivities when solving. Options are: - - - None: no sensitivities - - "explicit forward": explicitly formulate the sensitivity equations. \ - See :class:`pybamm.BaseSolver` - - "casadi": use casadi to differentiate through the integrator """ def __init__( @@ -83,7 +76,6 @@ def __init__( extrap_tol=0, extra_options_setup=None, extra_options_call=None, - sensitivity=None, ): super().__init__( "problem dependent", @@ -92,7 +84,6 @@ def __init__( root_method, root_tol, extrap_tol, - sensitivity=sensitivity, ) if mode in ["safe", "fast", "fast with events", "safe without grid"]: self.mode = mode @@ -138,6 +129,9 @@ def _integrate(self, model, t_eval, inputs_dict=None): # Record whether there are any symbolic inputs inputs_dict = inputs_dict or {} + has_symbolic_inputs = any( + isinstance(v, casadi.MX) for v in inputs_dict.values() + ) # convert inputs to casadi format inputs = casadi.vertcat(*[x for x in inputs_dict.values()]) @@ -176,7 +170,7 @@ def _integrate(self, model, t_eval, inputs_dict=None): else: # Create integrator without grid, which will be called repeatedly # This is necessary for casadi to compute sensitivities - self.create_integrator(model, inputs_dict) + self.create_integrator(model, inputs) solution = self._run_integrator( model, model.y0, inputs_dict, inputs, t_eval ) @@ -216,7 +210,7 @@ def _integrate(self, model, t_eval, inputs_dict=None): # in "safe without grid" mode, # create integrator once, without grid, # to avoid having to create several times - self.create_integrator(model, inputs_dict) + self.create_integrator(model, inputs) # Initialize solution solution = pybamm.Solution( np.array([t]), y0, model, inputs_dict, @@ -258,7 +252,7 @@ def _integrate(self, model, t_eval, inputs_dict=None): if self.mode == "safe": # update integrator with the grid - self.create_integrator(model, inputs_dict, t_window) + self.create_integrator(model, inputs, t_window) # Try to solve with the current global step, if it fails then # halve the step size and try again. try: diff --git a/pybamm/solvers/idaklu_solver.py b/pybamm/solvers/idaklu_solver.py index 0aaea0c85e..b8bf4ee3da 100644 --- a/pybamm/solvers/idaklu_solver.py +++ b/pybamm/solvers/idaklu_solver.py @@ -62,6 +62,11 @@ def __init__( if idaklu_spec is None: raise ImportError("KLU is not installed") + if sensitivity == "explicit forward": + raise NotImplementedError( + "Cannot use explicit forward equations with IDAKLUSolver" + ) + super().__init__( "ida", rtol, @@ -188,12 +193,9 @@ def _integrate(self, model, t_eval, inputs_dict=None): atol = self._atol y0 = model.y0 - print('idaklu, y0', y0) if isinstance(y0, casadi.DM): y0 = y0.full().flatten() - print('idaklu, y0', y0) - rtol = self._rtol atol = self._check_atol_type(atol, y0.size) diff --git a/pybamm/solvers/processed_variable.py b/pybamm/solvers/processed_variable.py index 4b5c22fb4b..43d682eccd 100644 --- a/pybamm/solvers/processed_variable.py +++ b/pybamm/solvers/processed_variable.py @@ -46,7 +46,7 @@ def __init__(self, base_variables, base_variables_casadi, solution, warn=True): self.auxiliary_domains = base_variables[0].auxiliary_domains self.warn = warn - self.symbolic_inputs = solution._symbolic_inputs + self.symbolic_inputs = solution.has_symbolic_inputs self.u_sol = solution.y self.y_sym = solution._y_sym diff --git a/tests/integration/test_solvers/test_idaklu.py b/tests/integration/test_solvers/test_idaklu.py index d61249a08c..7b128dbee1 100644 --- a/tests/integration/test_solvers/test_idaklu.py +++ b/tests/integration/test_solvers/test_idaklu.py @@ -19,6 +19,47 @@ def test_on_spme(self): solution = pybamm.IDAKLUSolver().solve(model, t_eval) np.testing.assert_array_less(1, solution.t.size) + def test_on_spme_sensitivities(self): + param_name = "Negative electrode conductivity [S.m-1]" + neg_electrode_cond = 100.0 + model = pybamm.lithium_ion.SPMe() + geometry = model.default_geometry + param = model.default_parameter_values + param.update({param_name: "[input]"}) + inputs = {param_name: neg_electrode_cond} + param.process_model(model) + param.process_geometry(geometry) + mesh = pybamm.Mesh(geometry, model.default_submesh_types, model.default_var_pts) + disc = pybamm.Discretisation(mesh, model.default_spatial_methods) + disc.process_model(model) + t_eval = np.linspace(0, 3600, 100) + solver = pybamm.IDAKLUSolver() + solution = solver.solve( + model, t_eval, + inputs=inputs, + calculate_sensitivities=True, + ) + np.testing.assert_array_less(1, solution.t.size) + + # evaluate the sensitivities using idas + dyda_ida = solution.sensitivities[param_name] + + # evaluate the sensitivities using finite difference + h = 1e-6 + sol_plus = solver.solve( + model, t_eval, + inputs={param_name: neg_electrode_cond + 0.5 * h} + ) + sol_neg = solver.solve( + model, t_eval, + inputs={param_name: neg_electrode_cond - 0.5 * h} + ) + dyda_fd = (sol_plus.y - sol_neg.y) / h + + np.testing.assert_array_almost_equal( + dyda_ida, dyda_fd + ) + def test_set_tol_by_variable(self): model = pybamm.lithium_ion.SPMe() geometry = model.default_geometry @@ -68,6 +109,7 @@ def test_changing_grid(self): if __name__ == "__main__": print("Add -v for more debug output") + pybamm.set_logging_level('INFO') if "-v" in sys.argv: debug = True pybamm.settings.debug_mode = True diff --git a/tests/unit/test_solvers/test_idaklu_solver.py b/tests/unit/test_solvers/test_idaklu_solver.py index 50b64de2bc..df08661325 100644 --- a/tests/unit/test_solvers/test_idaklu_solver.py +++ b/tests/unit/test_solvers/test_idaklu_solver.py @@ -51,7 +51,6 @@ def test_ida_roberts_klu_sensitivities(self): # example provided in sundials # see sundials ida examples pdf for form in ["python", "casadi", "jax"]: - print('FORM', form) model = pybamm.BaseModel() model.convert_to_format = form u = pybamm.Variable("u") @@ -68,11 +67,20 @@ def test_ida_roberts_klu_sensitivities(self): t_eval = np.linspace(0, 3, 100) a_value = 0.1 - print('starting solve.....') - sol = solver.solve( - model, t_eval, inputs={"a": a_value}, - calculate_sensitivities=True - ) + + if form == 'python': + with self.assertRaisesRegex( + NotImplementedError, "sensitivities"): + sol = solver.solve( + model, t_eval, inputs={"a": a_value}, + calculate_sensitivities=True + ) + continue + else: + sol = solver.solve( + model, t_eval, inputs={"a": a_value}, + calculate_sensitivities=True + ) # test that y[1] remains constant np.testing.assert_array_almost_equal( @@ -96,59 +104,6 @@ def test_ida_roberts_klu_sensitivities(self): dyda_ida, dyda_fd ) - def test_ida_roberts_klu_sensitivities_explicit(self): - # this test implements a python version of the ida Roberts - # example provided in sundials - # see sundials ida examples pdf - model = pybamm.BaseModel() - model.convert_to_format = "casadi" - u = pybamm.Variable("u") - v = pybamm.Variable("v") - a = pybamm.InputParameter("a") - b = pybamm.InputParameter("b") - model.rhs = {u: a * b * v} - model.algebraic = {v: 1 - v} - model.initial_conditions = {u: 0, v: 1} - - disc = pybamm.Discretisation() - disc.process_model(model) - - solver = pybamm.IDAKLUSolver( - sensitivity='explicit forward' - ) - - t_eval = np.linspace(0, 3, 100) - a_value = 0.1 - print('starting solve.....') - sol = solver.solve( - model, t_eval, inputs={"a": a_value, "b": 1}, - calculate_sensitivities=True - ) - - # test that y[1] remains constant - np.testing.assert_array_almost_equal( - sol.y[1, :], np.ones(sol.t.shape) - ) - - # test that y[0] = to true solution - true_solution = a_value * sol.t - np.testing.assert_array_almost_equal(sol.y[0, :], true_solution) - - # evaluate the sensitivities - dyda_ida = sol.sensitivities["a"] - - # evaluate the sensitivities using finite difference - h = 1e-6 - sol_plus = solver.solve(model, t_eval, inputs={"a": a_value + 0.5 * h}) - sol_neg = solver.solve(model, t_eval, inputs={"a": a_value - 0.5 * h}) - dyda_fd = (sol_plus.y - sol_neg.y) / h - - np.testing.assert_array_almost_equal( - dyda_ida, dyda_fd - ) - - - def test_set_atol(self): model = pybamm.lithium_ion.DFN() geometry = model.default_geometry From 3ebec309da4a2fad795b90a13a147860ed6efea9 Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Fri, 16 Jul 2021 11:53:57 +0100 Subject: [PATCH 28/73] #1477 took out sensitivity=casadi option --- pybamm/solvers/casadi_solver.py | 20 +- pybamm/solvers/processed_variable.py | 73 ------ tests/unit/test_solvers/test_casadi_solver.py | 220 ++---------------- 3 files changed, 18 insertions(+), 295 deletions(-) diff --git a/pybamm/solvers/casadi_solver.py b/pybamm/solvers/casadi_solver.py index 2c7fe990a3..906d127c15 100644 --- a/pybamm/solvers/casadi_solver.py +++ b/pybamm/solvers/casadi_solver.py @@ -159,24 +159,8 @@ def _integrate(self, model, t_eval, inputs_dict=None): solution = self._run_integrator( model, model.y0, inputs_dict, inputs, t_eval, use_grid=False, ) - if self.sensitivity == "casadi" and inputs_dict != {}: - # If the solution has already been created, we can reuse it - if model in self.y_sols: - y_sol = self.y_sols[model] - solution = pybamm.Solution( - t_eval, y_sol, model=model, inputs=inputs_dict, - sensitivities=explicit_sensitivities - ) - else: - # Create integrator without grid, which will be called repeatedly - # This is necessary for casadi to compute sensitivities - self.create_integrator(model, inputs) - solution = self._run_integrator( - model, model.y0, inputs_dict, inputs, t_eval - ) - solution.termination = "final time" - return solution - elif self.mode in ["fast", "fast with events"] or not model.events: + + if self.mode in ["fast", "fast with events"] or not model.events: if not model.events: pybamm.logger.info("No events found, running fast mode") if self.mode == "fast with events": diff --git a/pybamm/solvers/processed_variable.py b/pybamm/solvers/processed_variable.py index 43d682eccd..323d6fac07 100644 --- a/pybamm/solvers/processed_variable.py +++ b/pybamm/solvers/processed_variable.py @@ -49,7 +49,6 @@ def __init__(self, base_variables, base_variables_casadi, solution, warn=True): self.symbolic_inputs = solution.has_symbolic_inputs self.u_sol = solution.y - self.y_sym = solution._y_sym # Sensitivity starts off uninitialized, only set when called self._sensitivity = None @@ -512,8 +511,6 @@ def sensitivity(self): if self._sensitivity is None: if self.solution_sensitivity != {}: self.initialise_sensitivity_explicit_forward() - elif self.y_sym is not None: - self.initialise_sensitivity_casadi() else: raise ValueError( "Cannot compute sensitivities. The 'sensitivity' argument of the " @@ -576,76 +573,6 @@ def initialise_sensitivity_explicit_forward(self): # Save attribute self._sensitivity = sensitivity - def initialise_sensitivity_casadi(self): - def initialise_0D_symbolic(): - "Create a 0D symbolic variable" - # Evaluate the base_variable index-by-index - for idx in range(len(self.t_sol)): - t = self.t_sol[idx] - u = self.y_sym[:, idx] - next_entries = self.base_variable_casadi(t, u, self.symbolic_inputs) - if idx == 0: - entries = next_entries - else: - entries = casadi.horzcat(entries, next_entries) - - return entries - - def initialise_1D_symbolic(): - "Create a 1D symbolic variable" - # Evaluate the base_variable index-by-index - for idx in range(len(self.t_sol)): - t = self.t_sol[idx] - u = self.y_sym[:, idx] - next_entries = self.base_variable_casadi(t, u, self.symbolic_inputs) - if idx == 0: - entries = next_entries - else: - entries = casadi.vertcat(entries, next_entries) - - return entries - - inputs_stacked = casadi.vertcat(*self.inputs.values()) - self.base_eval = self.base_variable_casadi( - self.t_sol[0], self.u_sol[:, 0], inputs_stacked - ) - if ( - isinstance(self.base_eval, numbers.Number) - or len(self.base_eval.shape) == 0 - or self.base_eval.shape[0] == 1 - ): - entries_MX = initialise_0D_symbolic() - else: - n = self.mesh.npts - base_shape = self.base_eval.shape[0] - # Try shape that could make the variable a 1D variable - if base_shape == n: - entries_MX = initialise_1D_symbolic() - else: - # Raise error for 2D variable - raise NotImplementedError( - "Shape not recognized for {} ".format(self.base_variable) - + "(note processing of 2D and 3D variables is not yet " - + "implemented)" - ) - - # Compute jacobian - sens_MX = casadi.jacobian(entries_MX, self.symbolic_inputs) - casadi_sens_fn = casadi.Function("variable", [self.symbolic_inputs], [sens_MX]) - - sens_eval = casadi_sens_fn(inputs_stacked) - sensitivity = {"all": sens_eval} - - # Add the individual sensitivity - start = 0 - for name, inp in self.inputs.items(): - end = start + inp.shape[0] - sensitivity[name] = sens_eval[:, start:end] - start = end - - self._sensitivity = sensitivity - - class Interpolant1D: def __init__(self, pts_for_interp, entries_for_interp): self.interpolant = interp.interp1d( diff --git a/tests/unit/test_solvers/test_casadi_solver.py b/tests/unit/test_solvers/test_casadi_solver.py index 7eda81e330..c1abc57496 100644 --- a/tests/unit/test_solvers/test_casadi_solver.py +++ b/tests/unit/test_solvers/test_casadi_solver.py @@ -537,199 +537,6 @@ def test_casadi_safe_no_termination(self): solver.solve(model, t_eval=[0, 1]) -class TestCasadiSolverSensitivity(unittest.TestCase): - def test_solve_with_symbolic_input(self): - # Simple system: a single differential equation - var = pybamm.Variable("var") - model = pybamm.BaseModel() - model.rhs = {var: pybamm.InputParameter("param")} - model.initial_conditions = {var: 2} - model.variables = {"var": var} - - # create discretisation - disc = pybamm.Discretisation() - disc.process_model(model) - - # Solve - solver = pybamm.CasadiSolver(sensitivity="casadi") - t_eval = np.linspace(0, 1) - solution = solver.solve(model, t_eval, inputs={"param": 7}) - np.testing.assert_array_almost_equal(solution["var"].data, 2 + 7 * t_eval) - np.testing.assert_array_almost_equal( - solution["var"].sensitivity["param"], t_eval[:, np.newaxis] - ) - - solution = solver.solve(model, t_eval, inputs={"param": -3}) - np.testing.assert_array_almost_equal(solution["var"].data, 2 - 3 * t_eval) - np.testing.assert_array_almost_equal( - solution["var"].sensitivity["param"], t_eval[:, np.newaxis] - ) - - # def test_least_squares_fit(self): - # # Simple system: a single algebraic equation - # var1 = pybamm.Variable("var1", domain="negative electrode") - # var2 = pybamm.Variable("var2", domain="negative electrode") - # model = pybamm.BaseModel() - # p = pybamm.InputParameter("p") - # q = pybamm.InputParameter("q") - # model.rhs = {var1: -var1} - # model.algebraic = {var2: (var2 - p)} - # model.initial_conditions = {var1: 1, var2: 3} - # model.variables = {"objective": (var2 - q) ** 2 + (p - 3) ** 2} - - # # create discretisation - # disc = get_discretisation_for_testing() - # disc.process_model(model) - - # # Solve - # solver = pybamm.CasadiSolver() - # solution = solver.solve(model, np.linspace(0, 1)) - # sol_var = solution["objective"] - - # def objective(x): - # return sol_var.value({"p": x[0], "q": x[1]}).full().flatten() - - # # without jacobian - # lsq_sol = least_squares(objective, [2, 2], method="lm") - # np.testing.assert_array_almost_equal(lsq_sol.x, [3, 3], decimal=3) - - # def jac(x): - # return sol_var.sensitivity({"p": x[0], "q": x[1]}) - - # # with jacobian - # lsq_sol = least_squares(objective, [2, 2], jac=jac, method="lm") - # np.testing.assert_array_almost_equal(lsq_sol.x, [3, 3], decimal=3) - - def test_solve_with_symbolic_input_vector_output_scalar_input(self): - var = pybamm.Variable("var", "negative electrode") - model = pybamm.BaseModel() - # Set length scale to avoid warning - model.length_scales = {"negative electrode": 1} - - param = pybamm.InputParameter("param") - model.rhs = {var: -param * var} - model.initial_conditions = {var: 2} - model.variables = {"var": var} - - # create discretisation - disc = get_discretisation_for_testing() - disc.process_model(model) - n = disc.mesh["negative electrode"].npts - - # Solve - scalar input - solver = pybamm.CasadiSolver(sensitivity="casadi") - t_eval = np.linspace(0, 1) - solution = solver.solve(model, t_eval, inputs={"param": 7}) - np.testing.assert_array_almost_equal( - solution["var"].data, np.tile(2 * np.exp(-7 * t_eval), (n, 1)), decimal=4, - ) - - solution = solver.solve(model, t_eval, inputs={"param": 3}) - np.testing.assert_array_almost_equal( - solution["var"].data, np.tile(2 * np.exp(-3 * t_eval), (n, 1)), decimal=4, - ) - np.testing.assert_array_almost_equal( - solution["var"].sensitivity["param"], - np.repeat( - -2 * t_eval * np.exp(-3 * t_eval), disc.mesh["negative electrode"].npts - )[:, np.newaxis], - decimal=4, - ) - - def test_solve_with_symbolic_input_vector_output_vector_input(self): - var = pybamm.Variable("var", "negative electrode") - model = pybamm.BaseModel() - # Set length scale to avoid warning - model.length_scales = {"negative electrode": 1} - - param = pybamm.InputParameter("param", "negative electrode") - model.rhs = {var: -param * var} - model.initial_conditions = {var: 2} - model.variables = {"var": var} - - # create discretisation - disc = get_discretisation_for_testing() - disc.process_model(model) - n = disc.mesh["negative electrode"].npts - - solver = pybamm.CasadiSolver(sensitivity="casadi") - t_eval = np.linspace(0, 1) - solution = solver.solve(model, t_eval, inputs={"param": 3 * np.ones(n)}) - np.testing.assert_array_almost_equal( - solution["var"].data, np.tile(2 * np.exp(-3 * t_eval), (n, 1)), decimal=4, - ) - np.testing.assert_array_almost_equal( - solution["var"].sensitivity["param"], - np.kron(-2 * t_eval * np.exp(-3 * t_eval), np.eye(n)).T, - decimal=4, - ) - - p = np.linspace(0, 1, n)[:, np.newaxis] - solution = solver.solve(model, t_eval, inputs={"param": 2 * p}) - np.testing.assert_array_almost_equal( - solution["var"].data, 2 * np.exp(-2 * p * t_eval), decimal=4, - ) - - sens = solution["var"].sensitivity["param"] - for idx in range(len(t_eval)): - np.testing.assert_array_almost_equal( - sens[40 * idx : 40 * (idx + 1), :], - -2 * t_eval[idx] * np.exp(-2 * p * t_eval[idx]) * np.eye(40), - decimal=4, - ) - - def test_solve_with_symbolic_input_in_initial_conditions(self): - # Simple system: a single algebraic equation - var = pybamm.Variable("var") - model = pybamm.BaseModel() - model.rhs = {var: -var} - model.initial_conditions = {var: pybamm.InputParameter("param")} - model.variables = {"var": var} - - # create discretisation - disc = pybamm.Discretisation() - disc.process_model(model) - - # Solve - solver = pybamm.CasadiSolver(sensitivity="casadi", atol=1e-10, rtol=1e-10) - t_eval = np.linspace(0, 1) - solution = solver.solve(model, t_eval, inputs={"param": 7}) - np.testing.assert_array_almost_equal(solution["var"].data, 7 * np.exp(-t_eval)) - - solution = solver.solve(model, t_eval, inputs={"param": 3}) - np.testing.assert_array_almost_equal(solution["var"].data, 3 * np.exp(-t_eval)) - np.testing.assert_array_almost_equal( - solution["var"].sensitivity["param"], np.exp(-t_eval)[:, np.newaxis] - ) - - # def test_least_squares_fit_input_in_initial_conditions(self): - # # Simple system: a single algebraic equation - # var1 = pybamm.Variable("var1", domain="negative electrode") - # var2 = pybamm.Variable("var2", domain="negative electrode") - # model = pybamm.BaseModel() - # p = pybamm.InputParameter("p") - # q = pybamm.InputParameter("q") - # model.rhs = {var1: -var1} - # model.algebraic = {var2: (var2 - p)} - # model.initial_conditions = {var1: 1, var2: p} - # model.variables = {"objective": (var2 - q) ** 2 + (p - 3) ** 2} - - # # create discretisation - # disc = get_discretisation_for_testing() - # disc.process_model(model) - - # # Solve - # solver = pybamm.CasadiSolver() - # solution = solver.solve(model, np.linspace(0, 1)) - # sol_var = solution["objective"] - - # def objective(x): - # return sol_var.value({"p": x[0], "q": x[1]}).full().flatten() - - # # without jacobian - # lsq_sol = least_squares(objective, [2, 2], method="lm") - # np.testing.assert_array_almost_equal(lsq_sol.x, [3, 3], decimal=3) - class TestCasadiSolverODEsWithForwardSensitivityEquations(unittest.TestCase): def test_solve_sensitivity_scalar_var_scalar_input(self): @@ -744,10 +551,10 @@ def test_solve_sensitivity_scalar_var_scalar_input(self): # Solve # Make sure that passing in extra options works solver = pybamm.CasadiSolver( - mode="fast", rtol=1e-10, atol=1e-10, sensitivity="explicit forward" + mode="fast", rtol=1e-10, atol=1e-10 ) t_eval = np.linspace(0, 1, 80) - solution = solver.solve(model, t_eval, inputs={"p": 0.1}) + solution = solver.solve(model, t_eval, inputs={"p": 0.1}, sensitivity=True) np.testing.assert_array_equal(solution.t, t_eval) np.testing.assert_allclose(solution.y[0], np.exp(0.1 * solution.t)) np.testing.assert_allclose( @@ -779,11 +586,12 @@ def test_solve_sensitivity_scalar_var_scalar_input(self): # Solve # Make sure that passing in extra options works solver = pybamm.CasadiSolver( - rtol=1e-10, atol=1e-10, sensitivity="explicit forward" + rtol=1e-10, atol=1e-10 ) t_eval = np.linspace(0, 1, 80) solution = solver.solve( - model, t_eval, inputs={"p": 0.1, "q": 2, "r": -1, "s": 0.5} + model, t_eval, inputs={"p": 0.1, "q": 2, "r": -1, "s": 0.5}, + sensitivity=True, ) np.testing.assert_allclose(solution.y[0], -1 + 0.2 * solution.t) np.testing.assert_allclose( @@ -849,9 +657,10 @@ def test_solve_sensitivity_vector_var_scalar_input(self): n = disc.mesh["negative electrode"].npts # Solve - scalar input - solver = pybamm.CasadiSolver(sensitivity="explicit forward") + solver = pybamm.CasadiSolver() t_eval = np.linspace(0, 1) - solution = solver.solve(model, t_eval, inputs={"param": 7}) + solution = solver.solve(model, t_eval, inputs={"param": 7}, + sensitivity=["param"]) np.testing.assert_array_almost_equal( solution["var"].data, np.tile(2 * np.exp(-7 * t_eval), (n, 1)), decimal=4, ) @@ -881,11 +690,12 @@ def test_solve_sensitivity_vector_var_scalar_input(self): # Solve # Make sure that passing in extra options works solver = pybamm.CasadiSolver( - rtol=1e-10, atol=1e-10, sensitivity="explicit forward" + rtol=1e-10, atol=1e-10, ) t_eval = np.linspace(0, 1, 80) solution = solver.solve( - model, t_eval, inputs={"p": 0.1, "q": 2, "r": -1, "s": 0.5} + model, t_eval, inputs={"p": 0.1, "q": 2, "r": -1, "s": 0.5}, + sensitivities=True, ) np.testing.assert_allclose(solution.y, np.tile(-1 + 0.2 * solution.t, (n, 1))) np.testing.assert_allclose( @@ -958,10 +768,11 @@ def test_solve_sensitivity_scalar_var_vector_input(self): # Solve - constant input solver = pybamm.CasadiSolver( - mode="fast", rtol=1e-10, atol=1e-10, sensitivity="explicit forward" + mode="fast", rtol=1e-10, atol=1e-10 ) t_eval = np.linspace(0, 1) - solution = solver.solve(model, t_eval, inputs={"param": 7 * np.ones(n)}) + solution = solver.solve(model, t_eval, inputs={"param": 7 * np.ones(n)}, + sensitivities=True) l_n = mesh["negative electrode"].edges[-1] np.testing.assert_array_almost_equal( solution["var"].data, np.tile(2 * np.exp(-7 * t_eval), (n, 1)), decimal=4, @@ -981,7 +792,8 @@ def test_solve_sensitivity_scalar_var_vector_input(self): # Solve - linspace input p_eval = np.linspace(1, 2, n) - solution = solver.solve(model, t_eval, inputs={"param": p_eval}) + solution = solver.solve(model, t_eval, inputs={"param": p_eval}, + sensitivities=True) l_n = mesh["negative electrode"].edges[-1] np.testing.assert_array_almost_equal( solution["var"].data, 2 * np.exp(-p_eval[:, np.newaxis] * t_eval), decimal=4 From ac94921b31f124bcaf653b66818da322d4900df7 Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Fri, 16 Jul 2021 11:57:57 +0100 Subject: [PATCH 29/73] #1477 took out sensitivity=casadi option, take 2 --- pybamm/solvers/casadi_algebraic_solver.py | 54 +++---------------- tests/unit/test_solvers/test_casadi_solver.py | 2 +- 2 files changed, 7 insertions(+), 49 deletions(-) diff --git a/pybamm/solvers/casadi_algebraic_solver.py b/pybamm/solvers/casadi_algebraic_solver.py index 2db859a858..d9ad073d9b 100644 --- a/pybamm/solvers/casadi_algebraic_solver.py +++ b/pybamm/solvers/casadi_algebraic_solver.py @@ -21,18 +21,11 @@ class CasadiAlgebraicSolver(pybamm.BaseSolver): Any options to pass to the CasADi rootfinder. Please consult `CasADi documentation `_ for details. - sensitivity : str, optional - Whether (and how) to calculate sensitivities when solving. Options are: - - - None: no sensitivities - - "explicit forward": explicitly formulate the sensitivity equations. \ - See :class:`pybamm.BaseSolver` - - "casadi": use casadi to differentiate through the rootfinding operator """ - def __init__(self, tol=1e-6, extra_options=None, sensitivity=None): - super().__init__(sensitivity=sensitivity) + def __init__(self, tol=1e-6, extra_options=None): + super().__init__() self.tol = tol self.name = "CasADi algebraic solver" self.algebraic_solver = True @@ -76,18 +69,6 @@ def _integrate(self, model, t_eval, inputs_dict=None): inputs = casadi.vertcat(*[v for v in inputs_dict.values()]) y0 = model.y0 - print('algebraic', y0) - - # If y0 already satisfies the tolerance for all t then keep it - if self.sensitivity != "casadi" and all( - np.all(abs(model.casadi_algebraic(t, y0, inputs).full()) < self.tol) - for t in t_eval - ): - print('keeping soln', y0.full()) - pybamm.logger.debug("Keeping same solution at all times") - return pybamm.Solution( - t_eval, y0, model, inputs_dict, termination="success" - ) # 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 @@ -139,16 +120,6 @@ def _integrate(self, model, t_eval, inputs_dict=None): ) if model in self.rootfinders: - if self.sensitivity == "casadi": - # Reuse (symbolic) solution with new inputs - y_sol = self.y_sols[model] - return pybamm.Solution( - t_eval, - y_sol, - termination="success", - model=model, - inputs=inputs_dict, - ) roots = self.rootfinders[model] else: # Set up @@ -188,8 +159,7 @@ def _integrate(self, model, t_eval, inputs_dict=None): for idx, t in enumerate(t_eval): # Evaluate algebraic with new t and previous y0, if it's already close # enough then keep it - # We can't do this if also doing sensitivity - if self.sensitivity != "casadi" and np.all( + if np.all( abs(model.casadi_algebraic(t, y0, inputs).full()) < self.tol ): pybamm.logger.debug( @@ -201,12 +171,7 @@ def _integrate(self, model, t_eval, inputs_dict=None): y_alg = casadi.horzcat(y_alg, y0_alg) # Otherwise calculate new y_sol else: - # If doing sensitivity with casadi, evaluate with symbolic inputs - # Otherwise, evaluate with actual inputs - if self.sensitivity == "casadi": - t_y0_diff_inputs = casadi.vertcat(t, y0_diff, symbolic_inputs) - else: - t_y0_diff_inputs = casadi.vertcat(t, y0_diff, inputs) + t_y0_diff_inputs = casadi.vertcat(t, y0_diff, inputs) # Solve try: timer.reset() @@ -222,11 +187,9 @@ def _integrate(self, model, t_eval, inputs_dict=None): message = err.args[0] fun = None - # If there are no symbolic inputs, check the function is below the tol - # Skip this check if also doing sensitivity + # check the function is below the tol if success and ( - self.sensitivity == "casadi" - or (not any(np.isnan(fun)) and np.all(casadi.fabs(fun) < self.tol)) + not any(np.isnan(fun)) and np.all(casadi.fabs(fun) < self.tol) ): # update initial guess for the next iteration y0_alg = y_alg_sol @@ -259,11 +222,6 @@ def _integrate(self, model, t_eval, inputs_dict=None): y_diff = casadi.horzcat(*[y0_diff] * len(t_eval)) y_sol = casadi.vertcat(y_diff, y_alg) - # If doing sensitivity, return the solution as a function of the inputs - if self.sensitivity == "casadi": - y_sol = casadi.Function("y_sol", [symbolic_inputs], [y_sol]) - # Save the solution, can just reuse and change the inputs - self.y_sols[model] = y_sol # Return solution object (no events, so pass None to t_event, y_event) sol = pybamm.Solution( [t_eval], y_sol, model, inputs_dict, termination="success" diff --git a/tests/unit/test_solvers/test_casadi_solver.py b/tests/unit/test_solvers/test_casadi_solver.py index c1abc57496..795e369d0a 100644 --- a/tests/unit/test_solvers/test_casadi_solver.py +++ b/tests/unit/test_solvers/test_casadi_solver.py @@ -660,7 +660,7 @@ def test_solve_sensitivity_vector_var_scalar_input(self): solver = pybamm.CasadiSolver() t_eval = np.linspace(0, 1) solution = solver.solve(model, t_eval, inputs={"param": 7}, - sensitivity=["param"]) + sensitivities=["param"]) np.testing.assert_array_almost_equal( solution["var"].data, np.tile(2 * np.exp(-7 * t_eval), (n, 1)), decimal=4, ) From f5699c43af10d3632ec1c4c33542890704049cc8 Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Fri, 16 Jul 2021 12:47:44 +0100 Subject: [PATCH 30/73] #1477 sorting out processed variable --- pybamm/solvers/base_solver.py | 34 ++----- pybamm/solvers/casadi_solver.py | 12 +-- pybamm/solvers/processed_variable.py | 62 ++++++------ tests/unit/test_solvers/test_casadi_solver.py | 98 ++++++++++--------- 4 files changed, 94 insertions(+), 112 deletions(-) diff --git a/pybamm/solvers/base_solver.py b/pybamm/solvers/base_solver.py index 60ad08fe59..a865c3f998 100644 --- a/pybamm/solvers/base_solver.py +++ b/pybamm/solvers/base_solver.py @@ -35,16 +35,6 @@ class BaseSolver(object): The tolerance for the initial-condition solver (default is 1e-6). extrap_tol : float, optional The tolerance to assert whether extrapolation occurs or not. Default is 0. - sensitivity : str, optional - Whether (and how) to calculate sensitivities when solving. Options are: - - None (default): the individual solver is responsible for - calculating the sensitivity wrt these parameters, and providing the result in - the solution instance returned. At the moment this is only implemented for the - IDAKLU solver.\ - - "explicit forward": explicitly formulate the sensitivity equations for - the chosen input parameters. . At the moment this is only - implemented using convert_to_format = 'casadi'. \ - - see individual solvers for other options """ def __init__( @@ -231,6 +221,8 @@ def set_up(self, model, inputs=None, t_eval=None, else: calculate_sensitivites = [] + self.calculate_sensitivites = calculate_sensitivites + calculate_sensitivites_explicit = False if calculate_sensitivites and not isinstance(self, pybamm.IDAKLUSolver): calculate_sensitivites_explicit = True @@ -360,12 +352,13 @@ def jacp(*args, **kwargs): # Add sensitivity vectors to the rhs and algebraic equations jacp = None if calculate_sensitivites_explicit: + print('CASADI EXPLICIT', name, model.len_rhs) # The formulation is as per Park, S., Kato, D., Gima, Z., Klein, R., # & Moura, S. (2018). Optimal experimental design for # parameterization of an electrochemical lithium-ion battery model. # Journal of The Electrochemical Society, 165(7), A1309.". See #1100 # for details - if name == "rhs" and model.len_rhs > 0: + if name == "RHS" and model.len_rhs > 0: report( "Creating explicit forward sensitivity equations for rhs using CasADi") df_dx = casadi.jacobian(func, y_diff) @@ -621,7 +614,7 @@ def jacp(*args, **kwargs): # if we have changed the equations to include the explicit sensitivity # equations, then we also need to update the mass matrix - if self.sensitivity == "explicit forward": + if calculate_sensitivites_explicit: n_inputs = len(calculate_sensitivites) model.mass_matrix_inv = pybamm.Matrix( block_diag( @@ -693,27 +686,21 @@ def _set_initial_conditions(self, model, inputs, update_rhs): Whether to update the rhs. True for 'solve', False for 'step'. """ - # Make inputs symbolic if calculating sensitivities with casadi - if self.sensitivity == "casadi": - symbolic_inputs = casadi.MX.sym( - "inputs", casadi.vertcat(*inputs.values()).shape[0] - ) - else: - symbolic_inputs = inputs + if self.algebraic_solver is True: # Don't update model.y0 return None elif len(model.algebraic) == 0: if update_rhs is True: # Recalculate initial conditions for the rhs equations - y0 = model.init_eval(symbolic_inputs) + y0 = model.init_eval(inputs) else: # Don't update model.y0 return None else: if update_rhs is True: # Recalculate initial conditions for the rhs equations - y0_from_inputs = model.init_eval(symbolic_inputs) + y0_from_inputs = model.init_eval(inputs) # Reuse old solution for algebraic equations y0_from_model = model.y0 len_rhs = model.len_rhs @@ -726,10 +713,7 @@ def _set_initial_conditions(self, model, inputs, update_rhs): ) y0 = self.calculate_consistent_state(model, 0, inputs) # Make y0 a function of inputs if doing symbolic with casadi - if self.sensitivity == "casadi": - model.y0 = casadi.Function("y0", [symbolic_inputs], [y0]) - else: - model.y0 = y0 + model.y0 = y0 def calculate_consistent_state(self, model, time=0, inputs=None): """ diff --git a/pybamm/solvers/casadi_solver.py b/pybamm/solvers/casadi_solver.py index 906d127c15..37bffeedfc 100644 --- a/pybamm/solvers/casadi_solver.py +++ b/pybamm/solvers/casadi_solver.py @@ -125,7 +125,7 @@ def _integrate(self, model, t_eval, inputs_dict=None): # are we solving explicit forward equations? - explicit_sensitivities = self.sensitivity == 'explicit forward' + explicit_sensitivities = bool(self.calculate_sensitivites) # Record whether there are any symbolic inputs inputs_dict = inputs_dict or {} @@ -603,7 +603,7 @@ def _run_integrator(self, model, y0, inputs_dict, inputs, t_eval, use_grid=True) pybamm.logger.debug("Running CasADi integrator") # are we solving explicit forward equations? - explicit_sensitivities = self.sensitivity == 'explicit forward' + explicit_sensitivities = bool(self.calculate_sensitivites) if use_grid is True: t_eval_shifted = t_eval - t_eval[0] @@ -613,12 +613,6 @@ def _run_integrator(self, model, y0, inputs_dict, inputs, t_eval, use_grid=True) integrator = self.integrators[model]["no grid"] symbolic_inputs = casadi.MX.sym("inputs", inputs.shape[0]) - # If doing sensitivity with casadi, evaluate with symbolic inputs - # Otherwise, evaluate with actual inputs - if self.sensitivity == "casadi": - inputs_eval = symbolic_inputs - else: - inputs_eval = inputs len_rhs = model.concatenated_rhs.size @@ -656,7 +650,7 @@ def _run_integrator(self, model, y0, inputs_dict, inputs, t_eval, use_grid=True) for i in range(len(t_eval) - 1): t_min = t_eval[i] t_max = t_eval[i + 1] - inputs_with_tlims = casadi.vertcat(inputs_eval, t_min, t_max) + inputs_with_tlims = casadi.vertcat(inputs, t_min, t_max) timer = pybamm.Timer() casadi_sol = integrator( x0=x, z0=z, p=inputs_with_tlims, **self.extra_options_call diff --git a/pybamm/solvers/processed_variable.py b/pybamm/solvers/processed_variable.py index 323d6fac07..dc6103334c 100644 --- a/pybamm/solvers/processed_variable.py +++ b/pybamm/solvers/processed_variable.py @@ -39,6 +39,7 @@ def __init__(self, base_variables, base_variables_casadi, solution, warn=True): self.all_ts = solution.all_ts self.all_ys = solution.all_ys + self.all_inputs = solution.all_inputs self.all_inputs_casadi = solution.all_inputs_casadi self.mesh = base_variables[0].mesh @@ -51,8 +52,8 @@ def __init__(self, base_variables, base_variables_casadi, solution, warn=True): self.u_sol = solution.y # Sensitivity starts off uninitialized, only set when called - self._sensitivity = None - self.all_sensitivities = solution.all_sensitivities + self._sensitivities = None + self.solution_sensitivities = solution.sensitivities # Set timescale self.timescale = solution.timescale_eval @@ -488,52 +489,44 @@ def data(self): """Same as entries, but different name""" return self.entries - -class Interpolant0D: - def __init__(self, entries): - self.entries = entries - - def __call__(self, t): - return self.entries - @property - def sensitivity(self): + def sensitivities(self): """ - Returns a dictionary of sensitivity for each input parameter. + Returns a dictionary of sensitivities for each input parameter. The keys are the input parameters, and the value is a matrix of size (n_x * n_t, n_p), where n_x is the number of states, n_t is the number of time points, and n_p is the size of the input parameter """ - # No sensitivity if there are no inputs - if len(self.inputs) == 0: + # No sensitivities if there are no inputs + if len(self.all_inputs[0]) == 0: return {} - # Otherwise initialise and return sensitivity - if self._sensitivity is None: - if self.solution_sensitivity != {}: + # Otherwise initialise and return sensitivities + if self._sensitivities is None: + if self.solution_sensitivities != {}: self.initialise_sensitivity_explicit_forward() else: raise ValueError( - "Cannot compute sensitivities. The 'sensitivity' argument of the " - "solver should be changed from 'None' to allow sensitivity " + "Cannot compute sensitivities. The 'sensitivities' argument of the " + "solver.solve should be changed from 'None' to allow sensitivities " "calculations. Check solver documentation for details." ) - return self._sensitivity + return self._sensitivities def initialise_sensitivity_explicit_forward(self): "Set up the sensitivity dictionary" - inputs_stacked = casadi.vertcat(*[p for p in self.inputs.values()]) + inputs_stacked = self.all_inputs_casadi[0] # Set up symbolic variables t_casadi = casadi.MX.sym("t") y_casadi = casadi.MX.sym("y", self.u_sol.shape[0]) p_casadi = { name: casadi.MX.sym(name, value.shape[0]) - for name, value in self.inputs.items() + for name, value in self.all_inputs[0].items() } p_casadi_stacked = casadi.vertcat(*[p for p in p_casadi.values()]) # Convert variable to casadi format for differentiating - var_casadi = self.base_variable.to_casadi(t_casadi, y_casadi, inputs=p_casadi) + var_casadi = self.base_variables[0].to_casadi(t_casadi, y_casadi, inputs=p_casadi) dvar_dy = casadi.jacobian(var_casadi, y_casadi) dvar_dp = casadi.jacobian(var_casadi, p_casadi_stacked) @@ -544,8 +537,8 @@ def initialise_sensitivity_explicit_forward(self): dvar_dp_func = casadi.Function( "dvar_dp", [t_casadi, y_casadi, p_casadi_stacked], [dvar_dp] ) - for idx in range(len(self.t_sol)): - t = self.t_sol[idx] + for idx in range(len(self.all_ts[0])): + t = self.all_ts[0][idx] u = self.u_sol[:, idx] inp = inputs_stacked[:, idx] next_dvar_dy_eval = dvar_dy_func(t, u, inp) @@ -558,20 +551,29 @@ def initialise_sensitivity_explicit_forward(self): dvar_dp_eval = casadi.vertcat(dvar_dp_eval, next_dvar_dp_eval) # Compute sensitivity - dy_dp = self.solution_sensitivity["all"] + dy_dp = self.solution_sensitivities["all"] S_var = dvar_dy_eval @ dy_dp + dvar_dp_eval - sensitivity = {"all": S_var} + sensitivities = {"all": S_var} # Add the individual sensitivity start = 0 - for name, inp in self.inputs.items(): + for name, inp in self.all_inputs[0].items(): end = start + inp.shape[0] - sensitivity[name] = S_var[:, start:end] + sensitivities[name] = S_var[:, start:end] start = end # Save attribute - self._sensitivity = sensitivity + self._sensitivities = sensitivities + + +class Interpolant0D: + def __init__(self, entries): + self.entries = entries + + def __call__(self, t): + return self.entries + class Interpolant1D: def __init__(self, pts_for_interp, entries_for_interp): diff --git a/tests/unit/test_solvers/test_casadi_solver.py b/tests/unit/test_solvers/test_casadi_solver.py index 795e369d0a..31704c2f77 100644 --- a/tests/unit/test_solvers/test_casadi_solver.py +++ b/tests/unit/test_solvers/test_casadi_solver.py @@ -554,18 +554,19 @@ def test_solve_sensitivity_scalar_var_scalar_input(self): mode="fast", rtol=1e-10, atol=1e-10 ) t_eval = np.linspace(0, 1, 80) - solution = solver.solve(model, t_eval, inputs={"p": 0.1}, sensitivity=True) + solution = solver.solve(model, t_eval, inputs={"p": 0.1}, + calculate_sensitivities=True) np.testing.assert_array_equal(solution.t, t_eval) np.testing.assert_allclose(solution.y[0], np.exp(0.1 * solution.t)) np.testing.assert_allclose( - solution.sensitivity["p"], + solution.sensitivities["p"], (solution.t * np.exp(0.1 * solution.t))[:, np.newaxis], ) np.testing.assert_allclose( solution["var squared"].data, np.exp(0.1 * solution.t) ** 2 ) np.testing.assert_allclose( - solution["var squared"].sensitivity["p"], + solution["var squared"].sensitivities["p"], (2 * np.exp(0.1 * solution.t) * solution.t * np.exp(0.1 * solution.t))[ :, np.newaxis ], @@ -595,21 +596,21 @@ def test_solve_sensitivity_scalar_var_scalar_input(self): ) np.testing.assert_allclose(solution.y[0], -1 + 0.2 * solution.t) np.testing.assert_allclose( - solution.sensitivity["p"], (2 * solution.t)[:, np.newaxis], + solution.sensitivities["p"], (2 * solution.t)[:, np.newaxis], ) np.testing.assert_allclose( - solution.sensitivity["q"], (0.1 * solution.t)[:, np.newaxis], + solution.sensitivities["q"], (0.1 * solution.t)[:, np.newaxis], ) - np.testing.assert_allclose(solution.sensitivity["r"], 1) - np.testing.assert_allclose(solution.sensitivity["s"], 0) + np.testing.assert_allclose(solution.sensitivities["r"], 1) + np.testing.assert_allclose(solution.sensitivities["s"], 0) np.testing.assert_allclose( - solution.sensitivity["all"], + solution.sensitivities["all"], np.hstack( [ - solution.sensitivity["p"], - solution.sensitivity["q"], - solution.sensitivity["r"], - solution.sensitivity["s"], + solution.sensitivities["p"], + solution.sensitivities["q"], + solution.sensitivities["r"], + solution.sensitivities["s"], ] ), ) @@ -617,26 +618,26 @@ def test_solve_sensitivity_scalar_var_scalar_input(self): solution["var times s"].data, 0.5 * (-1 + 0.2 * solution.t) ) np.testing.assert_allclose( - solution["var times s"].sensitivity["p"], + solution["var times s"].sensitivities["p"], 0.5 * (2 * solution.t)[:, np.newaxis], ) np.testing.assert_allclose( - solution["var times s"].sensitivity["q"], + solution["var times s"].sensitivities["q"], 0.5 * (0.1 * solution.t)[:, np.newaxis], ) - np.testing.assert_allclose(solution["var times s"].sensitivity["r"], 0.5) + np.testing.assert_allclose(solution["var times s"].sensitivities["r"], 0.5) np.testing.assert_allclose( - solution["var times s"].sensitivity["s"], + solution["var times s"].sensitivities["s"], (-1 + 0.2 * solution.t)[:, np.newaxis], ) np.testing.assert_allclose( - solution["var times s"].sensitivity["all"], + solution["var times s"].sensitivities["all"], np.hstack( [ - solution["var times s"].sensitivity["p"], - solution["var times s"].sensitivity["q"], - solution["var times s"].sensitivity["r"], - solution["var times s"].sensitivity["s"], + solution["var times s"].sensitivities["p"], + solution["var times s"].sensitivities["q"], + solution["var times s"].sensitivities["r"], + solution["var times s"].sensitivities["s"], ] ), ) @@ -660,12 +661,12 @@ def test_solve_sensitivity_vector_var_scalar_input(self): solver = pybamm.CasadiSolver() t_eval = np.linspace(0, 1) solution = solver.solve(model, t_eval, inputs={"param": 7}, - sensitivities=["param"]) + calculate_sensitivities=["param"]) np.testing.assert_array_almost_equal( solution["var"].data, np.tile(2 * np.exp(-7 * t_eval), (n, 1)), decimal=4, ) np.testing.assert_array_almost_equal( - solution["var"].sensitivity["param"], + solution["var"].sensitivities["param"], np.repeat(-2 * t_eval * np.exp(-7 * t_eval), n)[:, np.newaxis], decimal=4, ) @@ -695,25 +696,25 @@ def test_solve_sensitivity_vector_var_scalar_input(self): t_eval = np.linspace(0, 1, 80) solution = solver.solve( model, t_eval, inputs={"p": 0.1, "q": 2, "r": -1, "s": 0.5}, - sensitivities=True, + calculate_sensitivities=True, ) np.testing.assert_allclose(solution.y, np.tile(-1 + 0.2 * solution.t, (n, 1))) np.testing.assert_allclose( - solution.sensitivity["p"], np.repeat(2 * solution.t, n)[:, np.newaxis], + solution.sensitivities["p"], np.repeat(2 * solution.t, n)[:, np.newaxis], ) np.testing.assert_allclose( - solution.sensitivity["q"], np.repeat(0.1 * solution.t, n)[:, np.newaxis], + solution.sensitivities["q"], np.repeat(0.1 * solution.t, n)[:, np.newaxis], ) - np.testing.assert_allclose(solution.sensitivity["r"], 1) - np.testing.assert_allclose(solution.sensitivity["s"], 0) + np.testing.assert_allclose(solution.sensitivities["r"], 1) + np.testing.assert_allclose(solution.sensitivities["s"], 0) np.testing.assert_allclose( - solution.sensitivity["all"], + solution.sensitivities["all"], np.hstack( [ - solution.sensitivity["p"], - solution.sensitivity["q"], - solution.sensitivity["r"], - solution.sensitivity["s"], + solution.sensitivities["p"], + solution.sensitivities["q"], + solution.sensitivities["r"], + solution.sensitivities["s"], ] ), ) @@ -721,26 +722,26 @@ def test_solve_sensitivity_vector_var_scalar_input(self): solution["var times s"].data, np.tile(0.5 * (-1 + 0.2 * solution.t), (n, 1)) ) np.testing.assert_allclose( - solution["var times s"].sensitivity["p"], + solution["var times s"].sensitivities["p"], np.repeat(0.5 * (2 * solution.t), n)[:, np.newaxis], ) np.testing.assert_allclose( - solution["var times s"].sensitivity["q"], + solution["var times s"].sensitivities["q"], np.repeat(0.5 * (0.1 * solution.t), n)[:, np.newaxis], ) - np.testing.assert_allclose(solution["var times s"].sensitivity["r"], 0.5) + np.testing.assert_allclose(solution["var times s"].sensitivities["r"], 0.5) np.testing.assert_allclose( - solution["var times s"].sensitivity["s"], + solution["var times s"].sensitivities["s"], np.repeat(-1 + 0.2 * solution.t, n)[:, np.newaxis], ) np.testing.assert_allclose( - solution["var times s"].sensitivity["all"], + solution["var times s"].sensitivities["all"], np.hstack( [ - solution["var times s"].sensitivity["p"], - solution["var times s"].sensitivity["q"], - solution["var times s"].sensitivity["r"], - solution["var times s"].sensitivity["s"], + solution["var times s"].sensitivities["p"], + solution["var times s"].sensitivities["q"], + solution["var times s"].sensitivities["r"], + solution["var times s"].sensitivities["s"], ] ), ) @@ -772,34 +773,34 @@ def test_solve_sensitivity_scalar_var_vector_input(self): ) t_eval = np.linspace(0, 1) solution = solver.solve(model, t_eval, inputs={"param": 7 * np.ones(n)}, - sensitivities=True) + calculate_sensitivities=True) l_n = mesh["negative electrode"].edges[-1] np.testing.assert_array_almost_equal( solution["var"].data, np.tile(2 * np.exp(-7 * t_eval), (n, 1)), decimal=4, ) np.testing.assert_array_almost_equal( - solution["var"].sensitivity["param"], + solution["var"].sensitivities["param"], np.vstack([np.eye(n) * -2 * t * np.exp(-7 * t) for t in t_eval]), ) np.testing.assert_array_almost_equal( solution["integral of var"].data, 2 * np.exp(-7 * t_eval) * l_n, decimal=4, ) np.testing.assert_array_almost_equal( - solution["integral of var"].sensitivity["param"], + solution["integral of var"].sensitivities["param"], np.tile(-2 * t_eval * np.exp(-7 * t_eval) * l_n / n, (n, 1)).T, ) # Solve - linspace input p_eval = np.linspace(1, 2, n) solution = solver.solve(model, t_eval, inputs={"param": p_eval}, - sensitivities=True) + calculate_sensitivities=True) l_n = mesh["negative electrode"].edges[-1] np.testing.assert_array_almost_equal( solution["var"].data, 2 * np.exp(-p_eval[:, np.newaxis] * t_eval), decimal=4 ) np.testing.assert_array_almost_equal( - solution["var"].sensitivity["param"], + solution["var"].sensitivities["param"], np.vstack([np.diag(-2 * t * np.exp(-p_eval * t)) for t in t_eval]), ) @@ -813,7 +814,7 @@ def test_solve_sensitivity_scalar_var_vector_input(self): ), ) np.testing.assert_array_almost_equal( - solution["integral of var"].sensitivity["param"], + solution["integral of var"].sensitivities["param"], np.vstack([-2 * t * np.exp(-p_eval * t) * l_n / n for t in t_eval]), ) @@ -822,6 +823,7 @@ def test_solve_sensitivity_scalar_var_vector_input(self): print("Add -v for more debug output") import sys + pybamm.set_logging_level('DEBUG') if "-v" in sys.argv: debug = True pybamm.settings.debug_mode = True From 72560c55d4012716233dfb733dcdf47f96e5abda Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Fri, 16 Jul 2021 13:11:03 +0100 Subject: [PATCH 31/73] #1477 got some more casadi tests working --- pybamm/solvers/base_solver.py | 14 +++++++++++--- pybamm/solvers/processed_variable.py | 11 ++++------- tests/unit/test_solvers/test_casadi_solver.py | 1 - 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/pybamm/solvers/base_solver.py b/pybamm/solvers/base_solver.py index a865c3f998..e1c2b6a6a3 100644 --- a/pybamm/solvers/base_solver.py +++ b/pybamm/solvers/base_solver.py @@ -11,6 +11,7 @@ from scipy.sparse import block_diag import multiprocessing as mp import warnings +import numbers class BaseSolver(object): @@ -231,8 +232,15 @@ def set_up(self, model, inputs=None, t_eval=None, # (FYI: this is used in the Solution class) model.calculate_sensitivities = calculate_sensitivites if calculate_sensitivites_explicit: - model.len_rhs_sens = model.len_rhs * len(calculate_sensitivites) - model.len_alg_sens = model.len_alg * len(calculate_sensitivites) + num_parameters = 0 + for name in calculate_sensitivites: + # if not a number, assume its a vector + if isinstance(inputs[name], numbers.Number): + num_parameters += 1 + else: + num_parameters += len(inputs[name]) + model.len_rhs_sens = model.len_rhs * num_parameters + model.len_alg_sens = model.len_alg * num_parameters if model.convert_to_format != "casadi": # Create Jacobian from concatenated rhs and algebraic @@ -615,7 +623,7 @@ def jacp(*args, **kwargs): # if we have changed the equations to include the explicit sensitivity # equations, then we also need to update the mass matrix if calculate_sensitivites_explicit: - n_inputs = len(calculate_sensitivites) + n_inputs = model.len_rhs_sens // model.len_rhs model.mass_matrix_inv = pybamm.Matrix( block_diag( [model.mass_matrix_inv.entries] * (n_inputs + 1), format="csr" diff --git a/pybamm/solvers/processed_variable.py b/pybamm/solvers/processed_variable.py index dc6103334c..970b1f472a 100644 --- a/pybamm/solvers/processed_variable.py +++ b/pybamm/solvers/processed_variable.py @@ -49,8 +49,6 @@ def __init__(self, base_variables, base_variables_casadi, solution, warn=True): self.symbolic_inputs = solution.has_symbolic_inputs - self.u_sol = solution.y - # Sensitivity starts off uninitialized, only set when called self._sensitivities = None self.solution_sensitivities = solution.sensitivities @@ -518,7 +516,7 @@ def initialise_sensitivity_explicit_forward(self): # Set up symbolic variables t_casadi = casadi.MX.sym("t") - y_casadi = casadi.MX.sym("y", self.u_sol.shape[0]) + y_casadi = casadi.MX.sym("y", self.all_ys[0].shape[0]) p_casadi = { name: casadi.MX.sym(name, value.shape[0]) for name, value in self.all_inputs[0].items() @@ -539,10 +537,9 @@ def initialise_sensitivity_explicit_forward(self): ) for idx in range(len(self.all_ts[0])): t = self.all_ts[0][idx] - u = self.u_sol[:, idx] - inp = inputs_stacked[:, idx] - next_dvar_dy_eval = dvar_dy_func(t, u, inp) - next_dvar_dp_eval = dvar_dp_func(t, u, inp) + u = self.all_ys[0][:, idx] + next_dvar_dy_eval = dvar_dy_func(t, u, inputs_stacked) + next_dvar_dp_eval = dvar_dp_func(t, u, inputs_stacked) if idx == 0: dvar_dy_eval = next_dvar_dy_eval dvar_dp_eval = next_dvar_dp_eval diff --git a/tests/unit/test_solvers/test_casadi_solver.py b/tests/unit/test_solvers/test_casadi_solver.py index 31704c2f77..a2c1731c89 100644 --- a/tests/unit/test_solvers/test_casadi_solver.py +++ b/tests/unit/test_solvers/test_casadi_solver.py @@ -823,7 +823,6 @@ def test_solve_sensitivity_scalar_var_vector_input(self): print("Add -v for more debug output") import sys - pybamm.set_logging_level('DEBUG') if "-v" in sys.argv: debug = True pybamm.settings.debug_mode = True From d9ff546e2e094ba3d217a019fb74fe87c8b1e964 Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Fri, 16 Jul 2021 14:20:49 +0100 Subject: [PATCH 32/73] #1477 fix algebraic solver --- pybamm/solvers/base_solver.py | 1 - pybamm/solvers/casadi_algebraic_solver.py | 27 ++++++++++++++----- pybamm/solvers/casadi_solver.py | 7 +---- tests/unit/test_solvers/test_casadi_solver.py | 2 +- 4 files changed, 22 insertions(+), 15 deletions(-) diff --git a/pybamm/solvers/base_solver.py b/pybamm/solvers/base_solver.py index e1c2b6a6a3..79b0c4bb59 100644 --- a/pybamm/solvers/base_solver.py +++ b/pybamm/solvers/base_solver.py @@ -360,7 +360,6 @@ def jacp(*args, **kwargs): # Add sensitivity vectors to the rhs and algebraic equations jacp = None if calculate_sensitivites_explicit: - print('CASADI EXPLICIT', name, model.len_rhs) # The formulation is as per Park, S., Kato, D., Gima, Z., Klein, R., # & Moura, S. (2018). Optimal experimental design for # parameterization of an electrochemical lithium-ion battery model. diff --git a/pybamm/solvers/casadi_algebraic_solver.py b/pybamm/solvers/casadi_algebraic_solver.py index d9ad073d9b..4258b50b5d 100644 --- a/pybamm/solvers/casadi_algebraic_solver.py +++ b/pybamm/solvers/casadi_algebraic_solver.py @@ -61,6 +61,9 @@ def _integrate(self, model, t_eval, inputs_dict=None): """ # Record whether there are any symbolic inputs inputs_dict = inputs_dict or {} + has_symbolic_inputs = any( + isinstance(v, casadi.MX) for v in inputs_dict.values() + ) symbolic_inputs = casadi.vertcat( *[v for v in inputs_dict.values() if isinstance(v, casadi.MX)] ) @@ -70,22 +73,29 @@ def _integrate(self, model, t_eval, inputs_dict=None): y0 = model.y0 + # If y0 already satisfies the tolerance for all t then keep it + if has_symbolic_inputs is False and all( + np.all(abs(model.casadi_algebraic(t, y0, inputs).full()) < self.tol) + for t in t_eval + ): + pybamm.logger.debug("Keeping same solution at all times") + return pybamm.Solution( + t_eval, y0, model, inputs_dict, termination="success" + ) + # 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 if model.rhs == {}: - print('no rhs') len_rhs = 0 y0_diff = casadi.DM() y0_alg = y0 else: # Check y0 to see if it includes sensitivities if model.len_rhs_and_alg == y0.shape[0]: - print('doesnt include sens') len_rhs = model.len_rhs else: - print('includes sens', inputs.shape[0]) len_rhs = model.len_rhs * (inputs.shape[0] + 1) y0_diff = y0[:len_rhs] y0_alg = y0[len_rhs:] @@ -159,7 +169,8 @@ def _integrate(self, model, t_eval, inputs_dict=None): for idx, t in enumerate(t_eval): # Evaluate algebraic with new t and previous y0, if it's already close # enough then keep it - if np.all( + # We can't do this if there are symbolic inputs + if has_symbolic_inputs is False and np.all( abs(model.casadi_algebraic(t, y0, inputs).full()) < self.tol ): pybamm.logger.debug( @@ -171,7 +182,7 @@ def _integrate(self, model, t_eval, inputs_dict=None): y_alg = casadi.horzcat(y_alg, y0_alg) # Otherwise calculate new y_sol else: - t_y0_diff_inputs = casadi.vertcat(t, y0_diff, inputs) + t_y0_diff_inputs = casadi.vertcat(t, y0_diff, symbolic_inputs) # Solve try: timer.reset() @@ -187,9 +198,11 @@ def _integrate(self, model, t_eval, inputs_dict=None): message = err.args[0] fun = None - # check the function is below the tol + # If there are no symbolic inputs, check the function is below the tol + # Skip this check if there are symbolic inputs if success and ( - not any(np.isnan(fun)) and np.all(casadi.fabs(fun) < self.tol) + has_symbolic_inputs is True + or (not any(np.isnan(fun)) and np.all(casadi.fabs(fun) < self.tol)) ): # update initial guess for the next iteration y0_alg = y_alg_sol diff --git a/pybamm/solvers/casadi_solver.py b/pybamm/solvers/casadi_solver.py index 37bffeedfc..c6251b3a52 100644 --- a/pybamm/solvers/casadi_solver.py +++ b/pybamm/solvers/casadi_solver.py @@ -442,7 +442,7 @@ def integer_bisect(): np.array([t_event]), y_event[:, np.newaxis], "event", - sensitivities=explicit_sensitivities + sensitivities=bool(self.calculate_sensitivites) ) solution.integration_time = ( coarse_solution.integration_time + dense_step_sol.integration_time @@ -665,11 +665,6 @@ def _run_integrator(self, model, y0, inputs_dict, inputs, t_eval, use_grid=True) y_sol = y_diff else: y_sol = casadi.vertcat(y_diff, y_alg) - # If doing sensitivity, return the solution as a function of the inputs - if self.sensitivity == "casadi": - y_sol = casadi.Function("y_sol", [symbolic_inputs], [y_sol]) - # Save the solution, can just reuse and change the inputs - self.y_sols[model] = y_sol sol = pybamm.Solution( t_eval, y_sol, model, inputs_dict, diff --git a/tests/unit/test_solvers/test_casadi_solver.py b/tests/unit/test_solvers/test_casadi_solver.py index a2c1731c89..3323644d8c 100644 --- a/tests/unit/test_solvers/test_casadi_solver.py +++ b/tests/unit/test_solvers/test_casadi_solver.py @@ -592,7 +592,7 @@ def test_solve_sensitivity_scalar_var_scalar_input(self): t_eval = np.linspace(0, 1, 80) solution = solver.solve( model, t_eval, inputs={"p": 0.1, "q": 2, "r": -1, "s": 0.5}, - sensitivity=True, + calculate_sensitivities=True, ) np.testing.assert_allclose(solution.y[0], -1 + 0.2 * solution.t) np.testing.assert_allclose( From 6e913358e3f55ebd612e2217e07668b8d56c0686 Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Fri, 16 Jul 2021 15:37:45 +0100 Subject: [PATCH 33/73] #1477 unit tests pass --- pybamm/__init__.py | 1 + pybamm/solvers/base_solver.py | 29 ++-- pybamm/solvers/casadi_algebraic_solver.py | 73 +++++----- pybamm/solvers/casadi_solver.py | 3 - pybamm/solvers/idaklu_solver.py | 17 +-- pybamm/solvers/processed_variable.py | 4 +- pybamm/solvers/scipy_solver.py | 11 +- pybamm/solvers/solution.py | 4 +- tests/unit/test_solvers/test_base_solver.py | 4 +- .../test_casadi_algebraic_solver.py | 133 ------------------ tests/unit/test_solvers/test_idaklu_solver.py | 2 +- .../test_solvers/test_processed_variable.py | 1 - tests/unit/test_solvers/test_scipy_solver.py | 116 +++++++-------- tests/unit/test_solvers/test_solution.py | 1 + 14 files changed, 127 insertions(+), 272 deletions(-) diff --git a/pybamm/__init__.py b/pybamm/__init__.py index 5a2e502aea..955511150e 100644 --- a/pybamm/__init__.py +++ b/pybamm/__init__.py @@ -209,6 +209,7 @@ def version(formatted=False): # from .solvers.solution import Solution from .solvers.processed_variable import ProcessedVariable +from .solvers.processed_symbolic_variable import ProcessedSymbolicVariable from .solvers.base_solver import BaseSolver from .solvers.dummy_solver import DummySolver from .solvers.algebraic_solver import AlgebraicSolver diff --git a/pybamm/solvers/base_solver.py b/pybamm/solvers/base_solver.py index 79b0c4bb59..51e85d94ea 100644 --- a/pybamm/solvers/base_solver.py +++ b/pybamm/solvers/base_solver.py @@ -11,7 +11,6 @@ from scipy.sparse import block_diag import multiprocessing as mp import warnings -import numbers class BaseSolver(object): @@ -228,6 +227,13 @@ def set_up(self, model, inputs=None, t_eval=None, if calculate_sensitivites and not isinstance(self, pybamm.IDAKLUSolver): calculate_sensitivites_explicit = True + if calculate_sensitivites_explicit and model.convert_to_format != 'casadi': + raise NotImplementedError( + "Sensitivities only supported for:\n" + " - model.convert_to_format = 'casadi'\n" + " - IDAKLUSolver (any convert_to_format)" + ) + # save sensitivity parameters so we can identify them later on # (FYI: this is used in the Solution class) model.calculate_sensitivities = calculate_sensitivites @@ -288,8 +294,8 @@ def report(string): jacp = None if calculate_sensitivites_explicit: raise NotImplementedError( - "sensitivities using convert_to_format = 'jax' " - "only implemented for IDAKLUSolver" + "explicit sensitivity equations not supported for " + "convert_to_format='jax'" ) elif calculate_sensitivites: report(( @@ -310,11 +316,12 @@ def report(string): elif model.convert_to_format != "casadi": # Process with pybamm functions, optionally converting # to python evaluator - if calculate_sensitivites: + if calculate_sensitivites_explicit: raise NotImplementedError( - "sensitivities only implemented with " - "convert_to_format = 'casadi' or convert_to_format = 'jax'" + "explicit sensitivity equations not supported for " + "convert_to_format='{}'".format(model.convert_to_format) ) + elif calculate_sensitivites: report(( f"Calculating sensitivities for {name} with respect " f"to parameters {calculate_sensitivites}" @@ -367,7 +374,9 @@ def jacp(*args, **kwargs): # for details if name == "RHS" and model.len_rhs > 0: report( - "Creating explicit forward sensitivity equations for rhs using CasADi") + "Creating explicit forward sensitivity equations " + "for rhs using CasADi" + ) df_dx = casadi.jacobian(func, y_diff) df_dp = casadi.jacobian(func, pS_casadi_stacked) S_x_mat = S_x.reshape( @@ -386,7 +395,8 @@ def jacp(*args, **kwargs): func = casadi.vertcat(func, S_rhs) if name == "algebraic" and model.len_alg > 0: report( - "Creating explicit forward sensitivity equations for algebraic using CasADi" + "Creating explicit forward sensitivity equations " + "for algebraic using CasADi" ) dg_dz = casadi.jacobian(func, y_alg) dg_dp = casadi.jacobian(func, pS_casadi_stacked) @@ -812,6 +822,7 @@ def solve( """ pybamm.logger.info("Start solving {} with {}".format(model.name, self.name)) + self.calculate_sensitivites = calculate_sensitivities # Make sure model isn't empty if len(model.rhs) == 0 and len(model.algebraic) == 0: @@ -1401,7 +1412,7 @@ def _set_up_ext_and_inputs(self, model, external_variables, inputs): name = input_param.name if name not in inputs: # Don't allow symbolic inputs if using `sensitivity` - if self.sensitivity == "explicit forward": + if self.calculate_sensitivites: raise pybamm.SolverError( "Cannot have symbolic inputs if explicitly solving forward" "sensitivity equations" diff --git a/pybamm/solvers/casadi_algebraic_solver.py b/pybamm/solvers/casadi_algebraic_solver.py index 4258b50b5d..085bc16d9c 100644 --- a/pybamm/solvers/casadi_algebraic_solver.py +++ b/pybamm/solvers/casadi_algebraic_solver.py @@ -32,9 +32,6 @@ def __init__(self, tol=1e-6, extra_options=None): self.extra_options = extra_options or {} pybamm.citations.register("Andersson2019") - self.rootfinders = {} - self.y_sols = {} - @property def tol(self): return self._tol @@ -102,6 +99,14 @@ def _integrate(self, model, t_eval, inputs_dict=None): y_alg = None + # Set up + t_sym = casadi.MX.sym("t") + y_alg_sym = casadi.MX.sym("y_alg", y0_alg.shape[0]) + y_sym = casadi.vertcat(y0_diff, y_alg_sym) + + t_and_inputs_sym = casadi.vertcat(t_sym, symbolic_inputs) + alg = model.casadi_algebraic(t_sym, y_sym, inputs) + # Check interpolant extrapolation if model.interpolant_extrapolation_events_eval: extrap_event = [ @@ -116,7 +121,9 @@ def _integrate(self, model, t_eval, inputs_dict=None): event.event_type == pybamm.EventType.INTERPOLANT_EXTRAPOLATION and ( - event.expression.evaluate(0, y0.full(), inputs=inputs) + event.expression.evaluate( + 0, y0.full(), inputs=inputs_dict + ) < self.extrap_tol ) ): @@ -129,40 +136,26 @@ def _integrate(self, model, t_eval, inputs_dict=None): "outside these bounds.".format(extrap_event_names) ) - if model in self.rootfinders: - roots = self.rootfinders[model] - else: - # Set up - t_sym = casadi.MX.sym("t") - y0_diff_sym = casadi.MX.sym("y0_diff", y0_diff.shape[0]) - y_alg_sym = casadi.MX.sym("y_alg", y0_alg.shape[0]) - y_sym = casadi.vertcat(y0_diff_sym, y_alg_sym) - - t_y0diff_inputs_sym = casadi.vertcat(t_sym, y0_diff_sym, symbolic_inputs) - alg = model.casadi_algebraic(t_sym, y_sym, symbolic_inputs) - - # Set constraints vector in the casadi format - # Constrain the unknowns. 0 (default): no constraint on ui, 1: ui >= 0.0, - # -1: ui <= 0.0, 2: ui > 0.0, -2: ui < 0.0. - constraints = np.zeros_like(model.bounds[0], dtype=int) - # If the lower bound is positive then the variable must always be positive - constraints[model.bounds[0] >= 0] = 1 - # If the upper bound is negative then the variable must always be negative - constraints[model.bounds[1] <= 0] = -1 - - # Set up rootfinder - roots = casadi.rootfinder( - "roots", - "newton", - dict(x=y_alg_sym, p=t_y0diff_inputs_sym, g=alg), - { - **self.extra_options, - "abstol": self.tol, - "constraints": list(constraints[len_rhs:]), - }, - ) - - self.rootfinders[model] = roots + # Set constraints vector in the casadi format + # Constrain the unknowns. 0 (default): no constraint on ui, 1: ui >= 0.0, + # -1: ui <= 0.0, 2: ui > 0.0, -2: ui < 0.0. + constraints = np.zeros_like(model.bounds[0], dtype=int) + # If the lower bound is positive then the variable must always be positive + constraints[model.bounds[0] >= 0] = 1 + # If the upper bound is negative then the variable must always be negative + constraints[model.bounds[1] <= 0] = -1 + + # Set up rootfinder + roots = casadi.rootfinder( + "roots", + "newton", + dict(x=y_alg_sym, p=t_and_inputs_sym, g=alg), + { + **self.extra_options, + "abstol": self.tol, + "constraints": list(constraints[len_rhs:]), + }, + ) timer = pybamm.Timer() integration_time = 0 @@ -182,11 +175,11 @@ def _integrate(self, model, t_eval, inputs_dict=None): y_alg = casadi.horzcat(y_alg, y0_alg) # Otherwise calculate new y_sol else: - t_y0_diff_inputs = casadi.vertcat(t, y0_diff, symbolic_inputs) + t_eval_inputs_sym = casadi.vertcat(t, symbolic_inputs) # Solve try: timer.reset() - y_alg_sol = roots(y0_alg, t_y0_diff_inputs) + y_alg_sol = roots(y0_alg, t_eval_inputs_sym) integration_time += timer.time() success = True message = None diff --git a/pybamm/solvers/casadi_solver.py b/pybamm/solvers/casadi_solver.py index c6251b3a52..965b5ebc7a 100644 --- a/pybamm/solvers/casadi_solver.py +++ b/pybamm/solvers/casadi_solver.py @@ -123,7 +123,6 @@ def _integrate(self, model, t_eval, inputs_dict=None): Any external variables or input parameters to pass to the model when solving """ - # are we solving explicit forward equations? explicit_sensitivities = bool(self.calculate_sensitivites) @@ -612,8 +611,6 @@ def _run_integrator(self, model, y0, inputs_dict, inputs, t_eval, use_grid=True) else: integrator = self.integrators[model]["no grid"] - symbolic_inputs = casadi.MX.sym("inputs", inputs.shape[0]) - len_rhs = model.concatenated_rhs.size # Check y0 to see if it includes sensitivities diff --git a/pybamm/solvers/idaklu_solver.py b/pybamm/solvers/idaklu_solver.py index b8bf4ee3da..4725d93131 100644 --- a/pybamm/solvers/idaklu_solver.py +++ b/pybamm/solvers/idaklu_solver.py @@ -38,14 +38,6 @@ class IDAKLUSolver(pybamm.BaseSolver): The tolerance for the initial-condition solver (default is 1e-6). extrap_tol : float, optional The tolerance to assert whether extrapolation occurs or not (default is 0). - sensitivity : str, optional - Whether (and how) to calculate sensitivities when solving. Options are: - - "explicit forward": explicitly formulate the sensitivity equations. \ - The formulation is as per "Park, S., Kato, D., Gima, Z., \ - Klein, R., & Moura, S. (2018). Optimal experimental design for parameterization\ - of an electrochemical lithium-ion battery model. Journal of The Electrochemical\ - Society, 165(7), A1309.". See #1100 for details \ - """ def __init__( @@ -56,17 +48,11 @@ def __init__( root_tol=1e-6, extrap_tol=0, max_steps="deprecated", - sensitivity="idas" ): if idaklu_spec is None: raise ImportError("KLU is not installed") - if sensitivity == "explicit forward": - raise NotImplementedError( - "Cannot use explicit forward equations with IDAKLUSolver" - ) - super().__init__( "ida", rtol, @@ -75,7 +61,6 @@ def __init__( root_tol, extrap_tol, max_steps, - sensitivity=sensitivity, ) self.name = "IDA KLU solver" @@ -339,7 +324,7 @@ def sensfn(resvalS, t, y, yp, yS, ypS): name: sol.yS[i].transpose() for i, name in enumerate(sens0.keys()) } else: - yS_out = None + yS_out = False if sol.flag in [0, 2]: # 0 = solved for all t_eval if sol.flag == 0: diff --git a/pybamm/solvers/processed_variable.py b/pybamm/solvers/processed_variable.py index 970b1f472a..f9fca1b938 100644 --- a/pybamm/solvers/processed_variable.py +++ b/pybamm/solvers/processed_variable.py @@ -524,7 +524,9 @@ def initialise_sensitivity_explicit_forward(self): p_casadi_stacked = casadi.vertcat(*[p for p in p_casadi.values()]) # Convert variable to casadi format for differentiating - var_casadi = self.base_variables[0].to_casadi(t_casadi, y_casadi, inputs=p_casadi) + var_casadi = self.base_variables[0].to_casadi( + t_casadi, y_casadi, inputs=p_casadi + ) dvar_dy = casadi.jacobian(var_casadi, y_casadi) dvar_dp = casadi.jacobian(var_casadi, p_casadi_stacked) diff --git a/pybamm/solvers/scipy_solver.py b/pybamm/solvers/scipy_solver.py index b7ab035110..fe1ef0feb2 100644 --- a/pybamm/solvers/scipy_solver.py +++ b/pybamm/solvers/scipy_solver.py @@ -25,12 +25,6 @@ class ScipySolver(pybamm.BaseSolver): Any options to pass to the solver. Please consult `SciPy documentation `_ for details. - sensitivity : str, optional - Whether (and how) to calculate sensitivities when solving. Options are: - - - None: no sensitivities - - "explicit forward": explicitly formulate the sensitivity equations. \ - See :class:`pybamm.BaseSolver` """ def __init__( @@ -40,14 +34,12 @@ def __init__( atol=1e-6, extrap_tol=0, extra_options=None, - sensitivity=None, ): super().__init__( method=method, rtol=rtol, atol=atol, extrap_tol=extrap_tol, - sensitivity=sensitivity, ) self.ode_solver = True self.extra_options = extra_options or {} @@ -136,7 +128,8 @@ def event_fn(t, y): t_event = None y_event = np.array(None) sol = pybamm.Solution( - sol.t, sol.y, model, inputs_dict, t_event, y_event, termination + sol.t, sol.y, model, inputs_dict, t_event, y_event, termination, + sensitivities=bool(self.calculate_sensitivites) ) sol.integration_time = integration_time return sol diff --git a/pybamm/solvers/solution.py b/pybamm/solvers/solution.py index a6ec3f6fc4..644e9d0488 100644 --- a/pybamm/solvers/solution.py +++ b/pybamm/solvers/solution.py @@ -86,7 +86,7 @@ def __init__( self._sensitivities = {} # if solution consists of explicit sensitivity equations, extract them if ( - sensitivities == True + sensitivities is True and all_models[0] is not None and not isinstance(all_ys[0], casadi.Function) and all_models[0].len_rhs_and_alg != all_ys[0].shape[0] @@ -97,7 +97,7 @@ def __init__( self._all_ys[0], self._sensitivities = \ self._extract_explicit_sensitivities( all_models[0], all_ys[0], all_ts[0], self.all_inputs[0] - ) + ) elif isinstance(sensitivities, dict): self._sensitivities = sensitivities else: diff --git a/tests/unit/test_solvers/test_base_solver.py b/tests/unit/test_solvers/test_base_solver.py index 9e141b8616..ce59ae1f64 100644 --- a/tests/unit/test_solvers/test_base_solver.py +++ b/tests/unit/test_solvers/test_base_solver.py @@ -163,12 +163,12 @@ def __init__(self): ) self.convert_to_format = "casadi" self.bounds = (-np.inf * np.ones(4), np.inf * np.ones(4)) -<<<<<<< HEAD self.len_rhs = 1 self.len_rhs_and_alg = 4 self.interpolant_extrapolation_events_eval = [] ->>>>>>> develop + def rhs_eval(self, t, y, inputs): + return y[0:1] def algebraic_eval(self, t, y, inputs): return (y[1:] - vec[1:]) ** 2 diff --git a/tests/unit/test_solvers/test_casadi_algebraic_solver.py b/tests/unit/test_solvers/test_casadi_algebraic_solver.py index 68cca727dc..501cdfd555 100644 --- a/tests/unit/test_solvers/test_casadi_algebraic_solver.py +++ b/tests/unit/test_solvers/test_casadi_algebraic_solver.py @@ -174,139 +174,6 @@ def test_solve_with_input(self): np.testing.assert_array_equal(solution.y, -7) -class TestCasadiAlgebraicSolverSensitivity(unittest.TestCase): - def test_solve_with_symbolic_input(self): - # Simple system: a single algebraic equation - var = pybamm.Variable("var") - model = pybamm.BaseModel() - model.algebraic = {var: var + pybamm.InputParameter("param")} - model.initial_conditions = {var: 2} - model.variables = {"var": var} - - # create discretisation - disc = pybamm.Discretisation() - disc.process_model(model) - - # Solve - solver = pybamm.CasadiAlgebraicSolver(sensitivity="casadi") - solution = solver.solve(model, [0], inputs={"param": 7}) - np.testing.assert_array_equal(solution["var"].data, -7) - np.testing.assert_array_equal(solution["var"].sensitivity["param"], -1) - np.testing.assert_array_equal(solution["var"].sensitivity["all"], -1) - - solution = solver.solve(model, [0], inputs={"param": 3}) - np.testing.assert_array_equal(solution["var"].data, -3) - np.testing.assert_array_equal(solution["var"].sensitivity["param"], -1) - np.testing.assert_array_equal(solution["var"].sensitivity["all"], -1) - - def test_least_squares_fit(self): - # Simple system: a single algebraic equation - var = pybamm.Variable("var", domain="negative electrode") - model = pybamm.BaseModel() - # Set length scale to avoid warning - model.length_scales = {"negative electrode": 1} - - p = pybamm.InputParameter("p") - q = pybamm.InputParameter("q") - model.algebraic = {var: (var - p)} - model.initial_conditions = {var: 3} - model.variables = {"objective": (var - q) ** 2 + (p - 3) ** 2} - - # create discretisation - disc = tests.get_discretisation_for_testing() - disc.process_model(model) - - # Solve - solver = pybamm.CasadiAlgebraicSolver(sensitivity="casadi") - - def objective(x): - solution = solver.solve(model, [0], inputs={"p": x[0], "q": x[1]}) - return solution["objective"].data.flatten() - - # without jacobian - lsq_sol = least_squares(objective, [2, 2], method="lm") - np.testing.assert_array_almost_equal(lsq_sol.x, [3, 3], decimal=3) - - def jac(x): - solution = solver.solve(model, [0], inputs={"p": x[0], "q": x[1]}) - return solution["objective"].sensitivity["all"] - - # with jacobian - lsq_sol = least_squares(objective, [2, 2], jac=jac, method="lm") - np.testing.assert_array_almost_equal(lsq_sol.x, [3, 3], decimal=3) - - def test_solve_with_symbolic_input_vector_variable_scalar_input(self): - var = pybamm.Variable("var", "negative electrode") - model = pybamm.BaseModel() - # Set length scale to avoid warning - model.length_scales = {"negative electrode": 1} - param = pybamm.InputParameter("param") - model.algebraic = {var: var + param} - model.initial_conditions = {var: 2} - model.variables = {"var": var} - - # create discretisation - disc = tests.get_discretisation_for_testing() - disc.process_model(model) - - # Solve - scalar input - solver = pybamm.CasadiAlgebraicSolver(sensitivity="casadi") - solution = solver.solve(model, [0], inputs={"param": 7}) - np.testing.assert_array_equal(solution["var"].data, -7) - solution = solver.solve(model, [0], inputs={"param": 3}) - np.testing.assert_array_equal(solution["var"].data, -3) - np.testing.assert_array_equal(solution["var"].sensitivity["param"], -1) - - def test_solve_with_symbolic_input_vector_variable_vector_input(self): - var = pybamm.Variable("var", "negative electrode") - model = pybamm.BaseModel() - # Set length scale to avoid warning - model.length_scales = {"negative electrode": 1} - param = pybamm.InputParameter("param", "negative electrode") - model.algebraic = {var: var + param} - model.initial_conditions = {var: 2} - model.variables = {"var": var} - - # create discretisation - disc = tests.get_discretisation_for_testing() - disc.process_model(model) - n = disc.mesh["negative electrode"].npts - - # Solve - vector input - solver = pybamm.CasadiAlgebraicSolver(sensitivity="casadi") - solution = solver.solve(model, [0], inputs={"param": 3 * np.ones(n)}) - - np.testing.assert_array_almost_equal(solution["var"].data, -3) - np.testing.assert_array_almost_equal( - solution["var"].sensitivity["param"], -np.eye(40) - ) - - p = np.linspace(0, 1, n)[:, np.newaxis] - solution = solver.solve(model, [0], inputs={"param": 2 * p}) - np.testing.assert_array_almost_equal(solution["var"].data, -2 * p) - np.testing.assert_array_almost_equal( - solution["var"].sensitivity["param"], -np.eye(40) - ) - - def test_solve_with_symbolic_input_in_initial_conditions(self): - # Simple system: a single algebraic equation - var = pybamm.Variable("var") - model = pybamm.BaseModel() - model.algebraic = {var: var + 2} - model.initial_conditions = {var: pybamm.InputParameter("param")} - model.variables = {"var": var} - - # create discretisation - disc = pybamm.Discretisation() - disc.process_model(model) - - # Solve - solver = pybamm.CasadiAlgebraicSolver(sensitivity="casadi") - solution = solver.solve(model, [0], inputs={"param": 7}) - np.testing.assert_array_equal(solution["var"].data, -2) - np.testing.assert_array_equal(solution["var"].sensitivity["param"], 0) - - if __name__ == "__main__": print("Add -v for more debug output") import sys diff --git a/tests/unit/test_solvers/test_idaklu_solver.py b/tests/unit/test_solvers/test_idaklu_solver.py index df08661325..8204759ad4 100644 --- a/tests/unit/test_solvers/test_idaklu_solver.py +++ b/tests/unit/test_solvers/test_idaklu_solver.py @@ -70,7 +70,7 @@ def test_ida_roberts_klu_sensitivities(self): if form == 'python': with self.assertRaisesRegex( - NotImplementedError, "sensitivities"): + NotImplementedError, "explicit sensitivity"): sol = solver.solve( model, t_eval, inputs={"a": a_value}, calculate_sensitivities=True diff --git a/tests/unit/test_solvers/test_processed_variable.py b/tests/unit/test_solvers/test_processed_variable.py index 1161d19a32..6d692da6ea 100644 --- a/tests/unit/test_solvers/test_processed_variable.py +++ b/tests/unit/test_solvers/test_processed_variable.py @@ -150,7 +150,6 @@ def test_processed_variable_1D_unknown_domain(self): np.linspace(0, 1, 1), np.zeros((var_pts[x])), "test", - model=pybamm.BaseModel(), ) c = pybamm.StateVector(slice(0, var_pts[x]), domain=["SEI layer"]) diff --git a/tests/unit/test_solvers/test_scipy_solver.py b/tests/unit/test_solvers/test_scipy_solver.py index d7e5f6782c..d20c822359 100644 --- a/tests/unit/test_solvers/test_scipy_solver.py +++ b/tests/unit/test_solvers/test_scipy_solver.py @@ -516,21 +516,22 @@ def test_solve_sensitivity_scalar_var_scalar_input(self): # Solve # Make sure that passing in extra options works solver = pybamm.ScipySolver( - rtol=1e-10, atol=1e-10, sensitivity="explicit forward" + rtol=1e-10, atol=1e-10 ) t_eval = np.linspace(0, 1, 80) - solution = solver.solve(model, t_eval, inputs={"p": 0.1}) + solution = solver.solve(model, t_eval, inputs={"p": 0.1}, + calculate_sensitivities=True) np.testing.assert_array_equal(solution.t, t_eval) np.testing.assert_allclose(solution.y[0], np.exp(0.1 * solution.t)) np.testing.assert_allclose( - solution.sensitivity["p"], + solution.sensitivities["p"], (solution.t * np.exp(0.1 * solution.t))[:, np.newaxis], ) np.testing.assert_allclose( solution["var squared"].data, np.exp(0.1 * solution.t) ** 2 ) np.testing.assert_allclose( - solution["var squared"].sensitivity["p"], + solution["var squared"].sensitivities["p"], (2 * np.exp(0.1 * solution.t) * solution.t * np.exp(0.1 * solution.t))[ :, np.newaxis ], @@ -551,31 +552,32 @@ def test_solve_sensitivity_scalar_var_scalar_input(self): # Solve # Make sure that passing in extra options works solver = pybamm.ScipySolver( - rtol=1e-10, atol=1e-10, sensitivity="explicit forward" + rtol=1e-10, atol=1e-10 ) t_eval = np.linspace(0, 1, 80) solution = solver.solve( - model, t_eval, inputs={"p": 0.1, "q": 2, "r": -1, "s": 0.5} + model, t_eval, inputs={"p": 0.1, "q": 2, "r": -1, "s": 0.5}, + calculate_sensitivities=True ) np.testing.assert_allclose(solution.y[0], -1 + 0.2 * solution.t) np.testing.assert_allclose( - solution.sensitivity["p"], + solution.sensitivities["p"], (2 * solution.t)[:, np.newaxis], ) np.testing.assert_allclose( - solution.sensitivity["q"], + solution.sensitivities["q"], (0.1 * solution.t)[:, np.newaxis], ) - np.testing.assert_allclose(solution.sensitivity["r"], 1) - np.testing.assert_allclose(solution.sensitivity["s"], 0) + np.testing.assert_allclose(solution.sensitivities["r"], 1) + np.testing.assert_allclose(solution.sensitivities["s"], 0) np.testing.assert_allclose( - solution.sensitivity["all"], + solution.sensitivities["all"], np.hstack( [ - solution.sensitivity["p"], - solution.sensitivity["q"], - solution.sensitivity["r"], - solution.sensitivity["s"], + solution.sensitivities["p"], + solution.sensitivities["q"], + solution.sensitivities["r"], + solution.sensitivities["s"], ] ), ) @@ -583,26 +585,26 @@ def test_solve_sensitivity_scalar_var_scalar_input(self): solution["var times s"].data, 0.5 * (-1 + 0.2 * solution.t) ) np.testing.assert_allclose( - solution["var times s"].sensitivity["p"], + solution["var times s"].sensitivities["p"], 0.5 * (2 * solution.t)[:, np.newaxis], ) np.testing.assert_allclose( - solution["var times s"].sensitivity["q"], + solution["var times s"].sensitivities["q"], 0.5 * (0.1 * solution.t)[:, np.newaxis], ) - np.testing.assert_allclose(solution["var times s"].sensitivity["r"], 0.5) + np.testing.assert_allclose(solution["var times s"].sensitivities["r"], 0.5) np.testing.assert_allclose( - solution["var times s"].sensitivity["s"], + solution["var times s"].sensitivities["s"], (-1 + 0.2 * solution.t)[:, np.newaxis], ) np.testing.assert_allclose( - solution["var times s"].sensitivity["all"], + solution["var times s"].sensitivities["all"], np.hstack( [ - solution["var times s"].sensitivity["p"], - solution["var times s"].sensitivity["q"], - solution["var times s"].sensitivity["r"], - solution["var times s"].sensitivity["s"], + solution["var times s"].sensitivities["p"], + solution["var times s"].sensitivities["q"], + solution["var times s"].sensitivities["r"], + solution["var times s"].sensitivities["s"], ] ), ) @@ -623,16 +625,17 @@ def test_solve_sensitivity_vector_var_scalar_input(self): n = disc.mesh["negative electrode"].npts # Solve - scalar input - solver = pybamm.ScipySolver(sensitivity="explicit forward") + solver = pybamm.ScipySolver() t_eval = np.linspace(0, 1) - solution = solver.solve(model, t_eval, inputs={"param": 7}) + solution = solver.solve(model, t_eval, inputs={"param": 7}, + calculate_sensitivities=True) np.testing.assert_array_almost_equal( solution["var"].data, np.tile(2 * np.exp(-7 * t_eval), (n, 1)), decimal=4, ) np.testing.assert_array_almost_equal( - solution["var"].sensitivity["param"], + solution["var"].sensitivities["param"], np.repeat(-2 * t_eval * np.exp(-7 * t_eval), n)[:, np.newaxis], decimal=4, ) @@ -657,31 +660,32 @@ def test_solve_sensitivity_vector_var_scalar_input(self): # Solve # Make sure that passing in extra options works solver = pybamm.ScipySolver( - rtol=1e-10, atol=1e-10, sensitivity="explicit forward" + rtol=1e-10, atol=1e-10 ) t_eval = np.linspace(0, 1, 80) solution = solver.solve( - model, t_eval, inputs={"p": 0.1, "q": 2, "r": -1, "s": 0.5} + model, t_eval, inputs={"p": 0.1, "q": 2, "r": -1, "s": 0.5}, + calculate_sensitivities=True ) np.testing.assert_allclose(solution.y, np.tile(-1 + 0.2 * solution.t, (n, 1))) np.testing.assert_allclose( - solution.sensitivity["p"], + solution.sensitivities["p"], np.repeat(2 * solution.t, n)[:, np.newaxis], ) np.testing.assert_allclose( - solution.sensitivity["q"], + solution.sensitivities["q"], np.repeat(0.1 * solution.t, n)[:, np.newaxis], ) - np.testing.assert_allclose(solution.sensitivity["r"], 1) - np.testing.assert_allclose(solution.sensitivity["s"], 0) + np.testing.assert_allclose(solution.sensitivities["r"], 1) + np.testing.assert_allclose(solution.sensitivities["s"], 0) np.testing.assert_allclose( - solution.sensitivity["all"], + solution.sensitivities["all"], np.hstack( [ - solution.sensitivity["p"], - solution.sensitivity["q"], - solution.sensitivity["r"], - solution.sensitivity["s"], + solution.sensitivities["p"], + solution.sensitivities["q"], + solution.sensitivities["r"], + solution.sensitivities["s"], ] ), ) @@ -689,26 +693,26 @@ def test_solve_sensitivity_vector_var_scalar_input(self): solution["var times s"].data, np.tile(0.5 * (-1 + 0.2 * solution.t), (n, 1)) ) np.testing.assert_allclose( - solution["var times s"].sensitivity["p"], + solution["var times s"].sensitivities["p"], np.repeat(0.5 * (2 * solution.t), n)[:, np.newaxis], ) np.testing.assert_allclose( - solution["var times s"].sensitivity["q"], + solution["var times s"].sensitivities["q"], np.repeat(0.5 * (0.1 * solution.t), n)[:, np.newaxis], ) - np.testing.assert_allclose(solution["var times s"].sensitivity["r"], 0.5) + np.testing.assert_allclose(solution["var times s"].sensitivities["r"], 0.5) np.testing.assert_allclose( - solution["var times s"].sensitivity["s"], + solution["var times s"].sensitivities["s"], np.repeat(-1 + 0.2 * solution.t, n)[:, np.newaxis], ) np.testing.assert_allclose( - solution["var times s"].sensitivity["all"], + solution["var times s"].sensitivities["all"], np.hstack( [ - solution["var times s"].sensitivity["p"], - solution["var times s"].sensitivity["q"], - solution["var times s"].sensitivity["r"], - solution["var times s"].sensitivity["s"], + solution["var times s"].sensitivities["p"], + solution["var times s"].sensitivities["q"], + solution["var times s"].sensitivities["r"], + solution["var times s"].sensitivities["s"], ] ), ) @@ -736,10 +740,11 @@ def test_solve_sensitivity_vector_var_vector_input(self): # Solve - constant input solver = pybamm.ScipySolver( - rtol=1e-10, atol=1e-10, sensitivity="explicit forward" + rtol=1e-10, atol=1e-10 ) t_eval = np.linspace(0, 1) - solution = solver.solve(model, t_eval, inputs={"param": 7 * np.ones(n)}) + solution = solver.solve(model, t_eval, inputs={"param": 7 * np.ones(n)}, + calculate_sensitivities=True) l_n = mesh["negative electrode"].edges[-1] np.testing.assert_array_almost_equal( solution["var"].data, @@ -748,7 +753,7 @@ def test_solve_sensitivity_vector_var_vector_input(self): ) np.testing.assert_array_almost_equal( - solution["var"].sensitivity["param"], + solution["var"].sensitivities["param"], np.vstack([np.eye(n) * -2 * t * np.exp(-7 * t) for t in t_eval]), ) np.testing.assert_array_almost_equal( @@ -757,23 +762,24 @@ def test_solve_sensitivity_vector_var_vector_input(self): decimal=4, ) np.testing.assert_array_almost_equal( - solution["integral of var"].sensitivity["param"], + solution["integral of var"].sensitivities["param"], np.tile(-2 * t_eval * np.exp(-7 * t_eval) * l_n / n, (n, 1)).T, ) # Solve - linspace input solver = pybamm.ScipySolver( - rtol=1e-10, atol=1e-10, sensitivity="explicit forward" + rtol=1e-10, atol=1e-10 ) t_eval = np.linspace(0, 1) p_eval = np.linspace(1, 2, n) - solution = solver.solve(model, t_eval, inputs={"param": p_eval}) + solution = solver.solve(model, t_eval, inputs={"param": p_eval}, + calculate_sensitivities=True) l_n = mesh["negative electrode"].edges[-1] np.testing.assert_array_almost_equal( solution["var"].data, 2 * np.exp(-p_eval[:, np.newaxis] * t_eval), decimal=4 ) np.testing.assert_array_almost_equal( - solution["var"].sensitivity["param"], + solution["var"].sensitivities["param"], np.vstack([np.diag(-2 * t * np.exp(-p_eval * t)) for t in t_eval]), ) @@ -787,7 +793,7 @@ def test_solve_sensitivity_vector_var_vector_input(self): ), ) np.testing.assert_array_almost_equal( - solution["integral of var"].sensitivity["param"], + solution["integral of var"].sensitivities["param"], np.vstack([-2 * t * np.exp(-p_eval * t) * l_n / n for t in t_eval]), ) diff --git a/tests/unit/test_solvers/test_solution.py b/tests/unit/test_solvers/test_solution.py index ede3845cad..5ded64e328 100644 --- a/tests/unit/test_solvers/test_solution.py +++ b/tests/unit/test_solvers/test_solution.py @@ -45,6 +45,7 @@ def test_add_solutions(self): y2 = np.tile(t2, (20, 1)) sol2 = pybamm.Solution(t2, y2, pybamm.BaseModel(), {"a": 2}) sol2.solve_time = 1 + sol2.integration_time = 0.5 sol_sum = sol1 + sol2 From 989749439c17e31e7d8b1c58da4438fbb7b2108f Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Fri, 16 Jul 2021 17:31:23 +0100 Subject: [PATCH 34/73] #1477 fix for casadi manual stepper --- pybamm/solvers/base_solver.py | 5 - pybamm/solvers/casadi_solver.py | 38 ++- pybamm/solvers/solution.py | 1 + tests/unit/test_solvers/test_casadi_solver.py | 284 ++++++++++++++++++ 4 files changed, 316 insertions(+), 12 deletions(-) diff --git a/pybamm/solvers/base_solver.py b/pybamm/solvers/base_solver.py index 51e85d94ea..cd12efb3de 100644 --- a/pybamm/solvers/base_solver.py +++ b/pybamm/solvers/base_solver.py @@ -414,11 +414,6 @@ def jacp(*args, **kwargs): (-1, 1) ) func = casadi.vertcat(func, S_alg) - if name == "residuals": - raise NotImplementedError( - "explicit forward equations not implimented for residuals" - ) - if name == "initial_conditions": if model.len_rhs == 0 or model.len_alg == 0: S_0 = casadi.jacobian(func, pS_casadi_stacked).reshape( diff --git a/pybamm/solvers/casadi_solver.py b/pybamm/solvers/casadi_solver.py index 965b5ebc7a..ab010b1962 100644 --- a/pybamm/solvers/casadi_solver.py +++ b/pybamm/solvers/casadi_solver.py @@ -197,7 +197,7 @@ def _integrate(self, model, t_eval, inputs_dict=None): # Initialize solution solution = pybamm.Solution( np.array([t]), y0, model, inputs_dict, - sensitivities=explicit_sensitivities + sensitivities=False, ) solution.solve_time = 0 solution.integration_time = 0 @@ -240,7 +240,8 @@ def _integrate(self, model, t_eval, inputs_dict=None): # halve the step size and try again. try: current_step_sol = self._run_integrator( - model, y0, inputs_dict, inputs, t_window, use_grid=use_grid + model, y0, inputs_dict, inputs, t_window, use_grid=use_grid, + extract_sensitivities_in_solution=False, ) solved = True except pybamm.SolverError: @@ -273,6 +274,20 @@ def _integrate(self, model, t_eval, inputs_dict=None): t = t_window[-1] # update y0 y0 = solution.all_ys[-1][:, -1] + + # now we extract sensitivities from the solution + if (explicit_sensitivities): + # save original ys[0] and replace with separated soln + # TODO: This is a dodgy hack, perhaps re-init the solution object? + solution._all_ys_and_sens = [solution._all_ys[0][:]] + solution._all_ys[0], solution._sensitivities = \ + solution._extract_explicit_sensitivities( + solution.all_models[0], + solution.all_ys[0], + solution.all_ts[0], + solution.all_inputs[0], + ) + return solution def _solve_for_event(self, coarse_solution, init_event_signs): @@ -598,12 +613,20 @@ def create_integrator(self, model, inputs, t_eval=None, use_event_switch=False): return integrator - def _run_integrator(self, model, y0, inputs_dict, inputs, t_eval, use_grid=True): + def _run_integrator(self, model, y0, inputs_dict, + inputs, t_eval, use_grid=True, + extract_sensitivities_in_solution=None, + ): pybamm.logger.debug("Running CasADi integrator") # are we solving explicit forward equations? explicit_sensitivities = bool(self.calculate_sensitivites) + # by default we extract sensitivities in the solution if we + # are calculating the sensitivities + if extract_sensitivities_in_solution is None: + extract_sensitivities_in_solution = explicit_sensitivities + if use_grid is True: t_eval_shifted = t_eval - t_eval[0] t_eval_shifted_rounded = np.round(t_eval_shifted, decimals=12).tobytes() @@ -614,8 +637,9 @@ def _run_integrator(self, model, y0, inputs_dict, inputs, t_eval, use_grid=True) len_rhs = model.concatenated_rhs.size # Check y0 to see if it includes sensitivities - if model.len_rhs_and_alg != y0.shape[0]: - len_rhs = len_rhs * (inputs.shape[0] + 1) + if explicit_sensitivities: + num_parameters = model.len_rhs_sens // model.len_rhs + len_rhs = len_rhs * (num_parameters + 1) y0_diff = y0[:len_rhs] y0_alg = y0[len_rhs:] @@ -634,7 +658,7 @@ def _run_integrator(self, model, y0, inputs_dict, inputs, t_eval, use_grid=True) y_sol = casadi.vertcat(casadi_sol["xf"], casadi_sol["zf"]) sol = pybamm.Solution( t_eval, y_sol, model, inputs_dict, - sensitivities=explicit_sensitivities + sensitivities=extract_sensitivities_in_solution ) sol.integration_time = integration_time return sol @@ -665,7 +689,7 @@ def _run_integrator(self, model, y0, inputs_dict, inputs, t_eval, use_grid=True) sol = pybamm.Solution( t_eval, y_sol, model, inputs_dict, - sensitivities=explicit_sensitivities + sensitivities=extract_sensitivities_in_solution ) sol.integration_time = integration_time return sol diff --git a/pybamm/solvers/solution.py b/pybamm/solvers/solution.py index 644e9d0488..2ffe567ddf 100644 --- a/pybamm/solvers/solution.py +++ b/pybamm/solvers/solution.py @@ -145,6 +145,7 @@ def __init__( # Solution now uses CasADi pybamm.citations.register("Andersson2019") + def _extract_explicit_sensitivities(self, model, y, t_eval, inputs): """ given a model and a solution y, extracts the sensitivities diff --git a/tests/unit/test_solvers/test_casadi_solver.py b/tests/unit/test_solvers/test_casadi_solver.py index 3323644d8c..3c3cb870d9 100644 --- a/tests/unit/test_solvers/test_casadi_solver.py +++ b/tests/unit/test_solvers/test_casadi_solver.py @@ -818,6 +818,290 @@ def test_solve_sensitivity_scalar_var_vector_input(self): np.vstack([-2 * t * np.exp(-p_eval * t) * l_n / n for t in t_eval]), ) +class TestCasadiSolverDAEsWithForwardSensitivityEquations(unittest.TestCase): + def test_solve_sensitivity_scalar_var_scalar_input(self): + # Create model + model = pybamm.BaseModel() + var1 = pybamm.Variable("var1") + p = pybamm.InputParameter("p") + var1 = pybamm.Variable("var1") + var2 = pybamm.Variable("var2") + model.rhs = {var1: 0.1 * var1} + model.algebraic = {var2: 2 * var1 - var2} + model.initial_conditions = {var1: 1, var2: 2} + model.variables = {"var2 squared": var2 ** 2} + + # Solve + # Make sure that passing in extra options works + solver = pybamm.CasadiSolver( + mode="fast", rtol=1e-10, atol=1e-10 + ) + t_eval = np.linspace(0, 1, 80) + solution = solver.solve(model, t_eval, inputs={"p": 0.1}, + calculate_sensitivities=True) + np.testing.assert_array_equal(solution.t, t_eval) + np.testing.assert_allclose(solution.y[0], np.exp(0.1 * solution.t)) + np.testing.assert_allclose( + solution.sensitivities["p"], + (solution.t * np.exp(0.1 * solution.t))[:, np.newaxis], + ) + np.testing.assert_allclose( + solution["var squared"].data, np.exp(0.1 * solution.t) ** 2 + ) + np.testing.assert_allclose( + solution["var squared"].sensitivities["p"], + (2 * np.exp(0.1 * solution.t) * solution.t * np.exp(0.1 * solution.t))[ + :, np.newaxis + ], + ) + + # More complicated model + # Create model + model = pybamm.BaseModel() + var = pybamm.Variable("var") + p = pybamm.InputParameter("p") + q = pybamm.InputParameter("q") + r = pybamm.InputParameter("r") + s = pybamm.InputParameter("s") + model.rhs = {var: p * q} + model.initial_conditions = {var: r} + model.variables = {"var times s": var * s} + + # Solve + # Make sure that passing in extra options works + solver = pybamm.CasadiSolver( + rtol=1e-10, atol=1e-10 + ) + t_eval = np.linspace(0, 1, 80) + solution = solver.solve( + model, t_eval, inputs={"p": 0.1, "q": 2, "r": -1, "s": 0.5}, + calculate_sensitivities=True, + ) + np.testing.assert_allclose(solution.y[0], -1 + 0.2 * solution.t) + np.testing.assert_allclose( + solution.sensitivities["p"], (2 * solution.t)[:, np.newaxis], + ) + np.testing.assert_allclose( + solution.sensitivities["q"], (0.1 * solution.t)[:, np.newaxis], + ) + np.testing.assert_allclose(solution.sensitivities["r"], 1) + np.testing.assert_allclose(solution.sensitivities["s"], 0) + np.testing.assert_allclose( + solution.sensitivities["all"], + np.hstack( + [ + solution.sensitivities["p"], + solution.sensitivities["q"], + solution.sensitivities["r"], + solution.sensitivities["s"], + ] + ), + ) + np.testing.assert_allclose( + solution["var times s"].data, 0.5 * (-1 + 0.2 * solution.t) + ) + np.testing.assert_allclose( + solution["var times s"].sensitivities["p"], + 0.5 * (2 * solution.t)[:, np.newaxis], + ) + np.testing.assert_allclose( + solution["var times s"].sensitivities["q"], + 0.5 * (0.1 * solution.t)[:, np.newaxis], + ) + np.testing.assert_allclose(solution["var times s"].sensitivities["r"], 0.5) + np.testing.assert_allclose( + solution["var times s"].sensitivities["s"], + (-1 + 0.2 * solution.t)[:, np.newaxis], + ) + np.testing.assert_allclose( + solution["var times s"].sensitivities["all"], + np.hstack( + [ + solution["var times s"].sensitivities["p"], + solution["var times s"].sensitivities["q"], + solution["var times s"].sensitivities["r"], + solution["var times s"].sensitivities["s"], + ] + ), + ) + + def test_solve_sensitivity_vector_var_scalar_input(self): + var = pybamm.Variable("var", "negative electrode") + model = pybamm.BaseModel() + # Set length scales to avoid warning + model.length_scales = {"negative electrode": 1} + param = pybamm.InputParameter("param") + model.rhs = {var: -param * var} + model.initial_conditions = {var: 2} + model.variables = {"var": var} + + # create discretisation + disc = get_discretisation_for_testing() + disc.process_model(model) + n = disc.mesh["negative electrode"].npts + + # Solve - scalar input + solver = pybamm.CasadiSolver() + t_eval = np.linspace(0, 1) + solution = solver.solve(model, t_eval, inputs={"param": 7}, + calculate_sensitivities=["param"]) + np.testing.assert_array_almost_equal( + solution["var"].data, np.tile(2 * np.exp(-7 * t_eval), (n, 1)), decimal=4, + ) + np.testing.assert_array_almost_equal( + solution["var"].sensitivities["param"], + np.repeat(-2 * t_eval * np.exp(-7 * t_eval), n)[:, np.newaxis], + decimal=4, + ) + + # More complicated model + # Create model + model = pybamm.BaseModel() + # Set length scales to avoid warning + model.length_scales = {"negative electrode": 1} + var = pybamm.Variable("var", "negative electrode") + p = pybamm.InputParameter("p") + q = pybamm.InputParameter("q") + r = pybamm.InputParameter("r") + s = pybamm.InputParameter("s") + model.rhs = {var: p * q} + model.initial_conditions = {var: r} + model.variables = {"var times s": var * s} + + # Discretise + disc.process_model(model) + + # Solve + # Make sure that passing in extra options works + solver = pybamm.CasadiSolver( + rtol=1e-10, atol=1e-10, + ) + t_eval = np.linspace(0, 1, 80) + solution = solver.solve( + model, t_eval, inputs={"p": 0.1, "q": 2, "r": -1, "s": 0.5}, + calculate_sensitivities=True, + ) + np.testing.assert_allclose(solution.y, np.tile(-1 + 0.2 * solution.t, (n, 1))) + np.testing.assert_allclose( + solution.sensitivities["p"], np.repeat(2 * solution.t, n)[:, np.newaxis], + ) + np.testing.assert_allclose( + solution.sensitivities["q"], np.repeat(0.1 * solution.t, n)[:, np.newaxis], + ) + np.testing.assert_allclose(solution.sensitivities["r"], 1) + np.testing.assert_allclose(solution.sensitivities["s"], 0) + np.testing.assert_allclose( + solution.sensitivities["all"], + np.hstack( + [ + solution.sensitivities["p"], + solution.sensitivities["q"], + solution.sensitivities["r"], + solution.sensitivities["s"], + ] + ), + ) + np.testing.assert_allclose( + solution["var times s"].data, np.tile(0.5 * (-1 + 0.2 * solution.t), (n, 1)) + ) + np.testing.assert_allclose( + solution["var times s"].sensitivities["p"], + np.repeat(0.5 * (2 * solution.t), n)[:, np.newaxis], + ) + np.testing.assert_allclose( + solution["var times s"].sensitivities["q"], + np.repeat(0.5 * (0.1 * solution.t), n)[:, np.newaxis], + ) + np.testing.assert_allclose(solution["var times s"].sensitivities["r"], 0.5) + np.testing.assert_allclose( + solution["var times s"].sensitivities["s"], + np.repeat(-1 + 0.2 * solution.t, n)[:, np.newaxis], + ) + np.testing.assert_allclose( + solution["var times s"].sensitivities["all"], + np.hstack( + [ + solution["var times s"].sensitivities["p"], + solution["var times s"].sensitivities["q"], + solution["var times s"].sensitivities["r"], + solution["var times s"].sensitivities["s"], + ] + ), + ) + + def test_solve_sensitivity_scalar_var_vector_input(self): + var = pybamm.Variable("var", "negative electrode") + model = pybamm.BaseModel() + # Set length scales to avoid warning + model.length_scales = {"negative electrode": 1} + + param = pybamm.InputParameter("param", "negative electrode") + model.rhs = {var: -param * var} + model.initial_conditions = {var: 2} + model.variables = { + "var": var, + "integral of var": pybamm.Integral(var, pybamm.standard_spatial_vars.x_n), + } + + # create discretisation + mesh = get_mesh_for_testing(xpts=5) + spatial_methods = {"macroscale": pybamm.FiniteVolume()} + disc = pybamm.Discretisation(mesh, spatial_methods) + disc.process_model(model) + n = disc.mesh["negative electrode"].npts + + # Solve - constant input + solver = pybamm.CasadiSolver( + mode="fast", rtol=1e-10, atol=1e-10 + ) + t_eval = np.linspace(0, 1) + solution = solver.solve(model, t_eval, inputs={"param": 7 * np.ones(n)}, + calculate_sensitivities=True) + l_n = mesh["negative electrode"].edges[-1] + np.testing.assert_array_almost_equal( + solution["var"].data, np.tile(2 * np.exp(-7 * t_eval), (n, 1)), decimal=4, + ) + + np.testing.assert_array_almost_equal( + solution["var"].sensitivities["param"], + np.vstack([np.eye(n) * -2 * t * np.exp(-7 * t) for t in t_eval]), + ) + np.testing.assert_array_almost_equal( + solution["integral of var"].data, 2 * np.exp(-7 * t_eval) * l_n, decimal=4, + ) + np.testing.assert_array_almost_equal( + solution["integral of var"].sensitivities["param"], + np.tile(-2 * t_eval * np.exp(-7 * t_eval) * l_n / n, (n, 1)).T, + ) + + # Solve - linspace input + p_eval = np.linspace(1, 2, n) + solution = solver.solve(model, t_eval, inputs={"param": p_eval}, + calculate_sensitivities=True) + l_n = mesh["negative electrode"].edges[-1] + np.testing.assert_array_almost_equal( + solution["var"].data, 2 * np.exp(-p_eval[:, np.newaxis] * t_eval), decimal=4 + ) + np.testing.assert_array_almost_equal( + solution["var"].sensitivities["param"], + np.vstack([np.diag(-2 * t * np.exp(-p_eval * t)) for t in t_eval]), + ) + + np.testing.assert_array_almost_equal( + solution["integral of var"].data, + np.sum( + 2 + * np.exp(-p_eval[:, np.newaxis] * t_eval) + * mesh["negative electrode"].d_edges[:, np.newaxis], + axis=0, + ), + ) + np.testing.assert_array_almost_equal( + solution["integral of var"].sensitivities["param"], + np.vstack([-2 * t * np.exp(-p_eval * t) * l_n / n for t in t_eval]), + ) + + if __name__ == "__main__": print("Add -v for more debug output") From 187c8d5c560590df941198c6e3dd56f77f2555f8 Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Fri, 16 Jul 2021 17:33:37 +0100 Subject: [PATCH 35/73] #1477 fix flake8 --- pybamm/solvers/casadi_solver.py | 2 +- pybamm/solvers/solution.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/pybamm/solvers/casadi_solver.py b/pybamm/solvers/casadi_solver.py index ab010b1962..2b38f71fcb 100644 --- a/pybamm/solvers/casadi_solver.py +++ b/pybamm/solvers/casadi_solver.py @@ -286,7 +286,7 @@ def _integrate(self, model, t_eval, inputs_dict=None): solution.all_ys[0], solution.all_ts[0], solution.all_inputs[0], - ) + ) return solution diff --git a/pybamm/solvers/solution.py b/pybamm/solvers/solution.py index 2ffe567ddf..644e9d0488 100644 --- a/pybamm/solvers/solution.py +++ b/pybamm/solvers/solution.py @@ -145,7 +145,6 @@ def __init__( # Solution now uses CasADi pybamm.citations.register("Andersson2019") - def _extract_explicit_sensitivities(self, model, y, t_eval, inputs): """ given a model and a solution y, extracts the sensitivities From 933fdf9b0c4de08c4ee2017ed0ef332819d60819 Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Fri, 16 Jul 2021 17:42:15 +0100 Subject: [PATCH 36/73] #1477 flake8 --- tests/unit/test_solvers/test_casadi_algebraic_solver.py | 2 -- tests/unit/test_solvers/test_casadi_solver.py | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/unit/test_solvers/test_casadi_algebraic_solver.py b/tests/unit/test_solvers/test_casadi_algebraic_solver.py index 501cdfd555..7f3f4aefa3 100644 --- a/tests/unit/test_solvers/test_casadi_algebraic_solver.py +++ b/tests/unit/test_solvers/test_casadi_algebraic_solver.py @@ -5,8 +5,6 @@ import pybamm import unittest import numpy as np -from scipy.optimize import least_squares -import tests class TestCasadiAlgebraicSolver(unittest.TestCase): diff --git a/tests/unit/test_solvers/test_casadi_solver.py b/tests/unit/test_solvers/test_casadi_solver.py index 3c3cb870d9..f5120b5291 100644 --- a/tests/unit/test_solvers/test_casadi_solver.py +++ b/tests/unit/test_solvers/test_casadi_solver.py @@ -537,7 +537,6 @@ def test_casadi_safe_no_termination(self): solver.solve(model, t_eval=[0, 1]) - class TestCasadiSolverODEsWithForwardSensitivityEquations(unittest.TestCase): def test_solve_sensitivity_scalar_var_scalar_input(self): # Create model @@ -818,6 +817,7 @@ def test_solve_sensitivity_scalar_var_vector_input(self): np.vstack([-2 * t * np.exp(-p_eval * t) * l_n / n for t in t_eval]), ) + class TestCasadiSolverDAEsWithForwardSensitivityEquations(unittest.TestCase): def test_solve_sensitivity_scalar_var_scalar_input(self): # Create model @@ -1102,7 +1102,6 @@ def test_solve_sensitivity_scalar_var_vector_input(self): ) - if __name__ == "__main__": print("Add -v for more debug output") import sys From 5b0ef01f1bae67cd8b54746d8614f10cd8244f90 Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Sat, 17 Jul 2021 07:28:37 +0100 Subject: [PATCH 37/73] #1477 flake8 --- pybamm/solvers/base_solver.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pybamm/solvers/base_solver.py b/pybamm/solvers/base_solver.py index fe48d5fdf6..bb34aab105 100644 --- a/pybamm/solvers/base_solver.py +++ b/pybamm/solvers/base_solver.py @@ -689,8 +689,9 @@ def jacp(*args, **kwargs): model.concatenated_rhs, model.concatenated_algebraic ) # Process again, uses caching so should be quick - residuals_eval, jacobian_eval, jacobian_wrtp_eval = process(all_states, "residuals")[ - 1:] + residuals_eval, jacobian_eval, jacobian_wrtp_eval = process( + all_states, "residuals" + )[1:] model.residuals_eval = residuals_eval model.jacobian_eval = jacobian_eval model.sensitivities_eval = jacobian_wrtp_eval From 2b50282980dc282bf55edd64b344b2afd8644a7d Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Mon, 19 Jul 2021 10:40:48 +0100 Subject: [PATCH 38/73] #1477 some minor fixes --- .../expression_tree/operations/evaluate_python.py | 4 +++- pybamm/solvers/base_solver.py | 15 +++++---------- pybamm/solvers/idaklu_solver.py | 2 +- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/pybamm/expression_tree/operations/evaluate_python.py b/pybamm/expression_tree/operations/evaluate_python.py index a7fbaeba84..1ad3939564 100644 --- a/pybamm/expression_tree/operations/evaluate_python.py +++ b/pybamm/expression_tree/operations/evaluate_python.py @@ -582,7 +582,9 @@ def __init__(self, symbol): args = "t=None, y=None, y_dot=None, inputs=None, known_evals=None" if self._arg_list: args = ",".join(self._arg_list) + ", " + args - python_str = "def evaluate_jax({}):\n".format(args) + python_str + python_str = ( + "def evaluate_jax({}):\n".format(args) + python_str + ) # calculate the final variable that will output the result of calling `evaluate` # on `symbol` diff --git a/pybamm/solvers/base_solver.py b/pybamm/solvers/base_solver.py index bb34aab105..e5bcc383be 100644 --- a/pybamm/solvers/base_solver.py +++ b/pybamm/solvers/base_solver.py @@ -497,12 +497,6 @@ def jacp(*args, **kwargs): else: init_eval.y_dummy = np.zeros((model.len_rhs_and_alg, 1)) - # Process rhs, algebraic and event expressions - rhs, rhs_eval, jac_rhs, jacp_rhs = process(model.concatenated_rhs, "RHS") - algebraic, algebraic_eval, jac_algebraic, jacp_algebraic = process( - model.concatenated_algebraic, "algebraic" - ) - # Calculate initial conditions model.y0 = init_eval(inputs) model.init_eval = init_eval @@ -579,8 +573,8 @@ def jacp(*args, **kwargs): ) # Process rhs, algebraic and event expressions - rhs, rhs_eval, jac_rhs = process(model.concatenated_rhs, "RHS") - algebraic, algebraic_eval, jac_algebraic = process( + rhs, rhs_eval, jac_rhs, jacp_rhs = process(model.concatenated_rhs, "RHS") + algebraic, algebraic_eval, jac_algebraic, jacp_algebraic = process( model.concatenated_algebraic, "algebraic" ) casadi_terminate_events = [] @@ -897,7 +891,7 @@ def solve( # is passed to `set_up`. # See https://github.com/pybamm-team/PyBaMM/pull/1261 self.set_up(model, ext_and_inputs_list[0], t_eval, - calculate_sensitivities) + calculate_sensitivites=calculate_sensitivities) self.models_set_up.update( {model: {"initial conditions": model.concatenated_initial_conditions}} ) @@ -907,7 +901,8 @@ def solve( if ics_set_up.id != model.concatenated_initial_conditions.id: # If the new initial conditions are different, set up again self.set_up(model, ext_and_inputs_list[0], t_eval, - calculate_sensitivities, ics_only=True) + calculate_sensitivites=calculate_sensitivities, + ics_only=True) self.models_set_up[model][ "initial conditions" ] = model.concatenated_initial_conditions diff --git a/pybamm/solvers/idaklu_solver.py b/pybamm/solvers/idaklu_solver.py index 3048e18d43..4df4cbf0fb 100644 --- a/pybamm/solvers/idaklu_solver.py +++ b/pybamm/solvers/idaklu_solver.py @@ -250,7 +250,7 @@ def rootfn(t, y): return return_root # get ids of rhs and algebraic variables - rhs_ids = np.ones(model.rhs_eval(0, y0, inputs).shape) + rhs_ids = np.ones(model.rhs_eval(0, y0, inputs).shape[0]) alg_ids = np.zeros(len(y0) - len(rhs_ids)) ids = np.concatenate((rhs_ids, alg_ids)) From 3296ec3cd0146e64413a6b1f03a8c8be5bcc308c Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Tue, 20 Jul 2021 17:52:49 +0100 Subject: [PATCH 39/73] #1477 make sure that model is set up again if calculate sensitivities changes, plus other fixes --- pybamm/solvers/base_solver.py | 110 ++++++++++-------- pybamm/solvers/casadi_solver.py | 6 +- pybamm/solvers/scipy_solver.py | 2 +- pybamm/solvers/solution.py | 1 + .../test_models/standard_model_tests.py | 29 +++++ tests/unit/test_solvers/test_base_solver.py | 4 +- tests/unit/test_solvers/test_casadi_solver.py | 87 ++------------ tests/unit/test_solvers/test_idaklu_solver.py | 37 +++--- tests/unit/test_solvers/test_scipy_solver.py | 1 - 9 files changed, 130 insertions(+), 147 deletions(-) diff --git a/pybamm/solvers/base_solver.py b/pybamm/solvers/base_solver.py index e5bcc383be..1f8f61f4d7 100644 --- a/pybamm/solvers/base_solver.py +++ b/pybamm/solvers/base_solver.py @@ -127,8 +127,7 @@ def copy(self): new_solver.models_set_up = {} return new_solver - def set_up(self, model, inputs=None, t_eval=None, ics_only=False, - calculate_sensitivites=False): + def set_up(self, model, inputs=None, t_eval=None, ics_only=False): """Unpack model, perform checks, and calculate jacobian. Parameters @@ -140,11 +139,6 @@ def set_up(self, model, inputs=None, t_eval=None, ics_only=False, Any input parameters to pass to the model when solving t_eval : numeric type, optional The times (in seconds) at which to compute the solution - calculate_sensitivites : list of str or bool - If true, solver calculates sensitivities of all input parameters. - If only a subset of sensitivities are required, can also pass a - list of input parameter names - """ pybamm.logger.info("Start solver set-up") @@ -217,32 +211,27 @@ def set_up(self, model, inputs=None, t_eval=None, ics_only=False, if isinstance(symbol, pybamm.InputParameter) }) - # from here on, calculate_sensitivites is now only a list - if isinstance(calculate_sensitivites, bool): - if calculate_sensitivites: - calculate_sensitivites = [p for p in inputs.keys()] - else: - calculate_sensitivites = [] - - self.calculate_sensitivites = calculate_sensitivites + # set default calculate sensitivities on model + if not hasattr(model, 'calculate_sensitivities'): + model.calculate_sensitivities = [] - calculate_sensitivites_explicit = False - if calculate_sensitivites and not isinstance(self, pybamm.IDAKLUSolver): - calculate_sensitivites_explicit = True + # see if we need to form the explicit sensitivity equations + calculate_sensitivities_explicit = False + if model.calculate_sensitivities and not isinstance(self, pybamm.IDAKLUSolver): + calculate_sensitivities_explicit = True - if calculate_sensitivites_explicit and model.convert_to_format != 'casadi': + if calculate_sensitivities_explicit and model.convert_to_format != 'casadi': raise NotImplementedError( "Sensitivities only supported for:\n" " - model.convert_to_format = 'casadi'\n" " - IDAKLUSolver (any convert_to_format)" ) - # save sensitivity parameters so we can identify them later on - # (FYI: this is used in the Solution class) - model.calculate_sensitivities = calculate_sensitivites - if calculate_sensitivites_explicit: + # if we are calculating sensitivities explicitly then the number of + # equations will change + if calculate_sensitivities_explicit: num_parameters = 0 - for name in calculate_sensitivites: + for name in model.calculate_sensitivities: # if not a number, assume its a vector if isinstance(inputs[name], numbers.Number): num_parameters += 1 @@ -272,9 +261,9 @@ def set_up(self, model, inputs=None, t_eval=None, ics_only=False, p_casadi[name] = casadi.MX.sym(name, value.shape[0]) p_casadi_stacked = casadi.vertcat(*[p for p in p_casadi.values()]) # sensitivity vectors - if calculate_sensitivites_explicit: + if calculate_sensitivities_explicit: pS_casadi_stacked = casadi.vertcat( - *[p_casadi[name] for name in calculate_sensitivites] + *[p_casadi[name] for name in model.calculate_sensitivities] ) S_x = casadi.MX.sym("S_x", model.len_rhs_sens) S_z = casadi.MX.sym("S_z", model.len_alg_sens) @@ -295,15 +284,15 @@ def report(string): report(f"Converting {name} to jax") func = pybamm.EvaluatorJax(func) jacp = None - if calculate_sensitivites_explicit: + if calculate_sensitivities_explicit: raise NotImplementedError( "explicit sensitivity equations not supported for " "convert_to_format='jax'" ) - elif calculate_sensitivites: + elif model.calculate_sensitivities: report(( f"Calculating sensitivities for {name} with respect " - f"to parameters {calculate_sensitivites} using jax" + f"to parameters {model.calculate_sensitivities} using jax" )) jacp = func.get_sensitivities() jacp = jacp.evaluate @@ -319,19 +308,19 @@ def report(string): elif model.convert_to_format != "casadi": # Process with pybamm functions, optionally converting # to python evaluator - if calculate_sensitivites_explicit: + if calculate_sensitivities_explicit: raise NotImplementedError( "explicit sensitivity equations not supported for " "convert_to_format='{}'".format(model.convert_to_format) ) - elif calculate_sensitivites: + elif model.calculate_sensitivities: report(( f"Calculating sensitivities for {name} with respect " - f"to parameters {calculate_sensitivites}" + f"to parameters {model.calculate_sensitivities}" )) jacp_dict = { p: func.diff(pybamm.InputParameter(p)) - for p in calculate_sensitivites + for p in model.calculate_sensitivities } if model.convert_to_format == "python": report(f"Converting sensitivities for {name} to python") @@ -369,7 +358,7 @@ def jacp(*args, **kwargs): func = func.to_casadi(t_casadi, y_casadi, inputs=p_casadi) # Add sensitivity vectors to the rhs and algebraic equations jacp = None - if calculate_sensitivites_explicit: + if calculate_sensitivities_explicit: # The formulation is as per Park, S., Kato, D., Gima, Z., Klein, R., # & Moura, S. (2018). Optimal experimental design for # parameterization of an electrochemical lithium-ion battery model. @@ -433,13 +422,13 @@ def jacp(*args, **kwargs): (-1, 1) ) func = casadi.vertcat(x0, Sx_0, z0, Sz_0) - elif calculate_sensitivites: + elif model.calculate_sensitivities: report(( f"Calculating sensitivities for {name} with respect " - f"to parameters {calculate_sensitivites} using CasADi" + f"to parameters {model.calculate_sensitivities} using CasADi" )) jacp_dict = {} - for pname in calculate_sensitivites: + for pname in model.calculate_sensitivities: p_diff = casadi.jacobian(func, p_casadi[pname]) jacp_dict[pname] = casadi.Function( name, [t_casadi, y_casadi, p_casadi_stacked], @@ -488,7 +477,7 @@ def jacp(*args, **kwargs): )[0] init_eval = InitialConditions(initial_conditions, model) - if calculate_sensitivites_explicit: + if calculate_sensitivities_explicit: y0_total_size = ( model.len_rhs + model.len_rhs_sens + model.len_alg + model.len_alg_sens @@ -636,7 +625,7 @@ def jacp(*args, **kwargs): # if we have changed the equations to include the explicit sensitivity # equations, then we also need to update the mass matrix - if calculate_sensitivites_explicit: + if calculate_sensitivities_explicit: n_inputs = model.len_rhs_sens // model.len_rhs model.mass_matrix_inv = pybamm.Matrix( block_diag( @@ -659,10 +648,10 @@ def jacp(*args, **kwargs): if len(model.rhs) > 0: mass_matrix_inv = casadi.MX(model.mass_matrix_inv.entries) explicit_rhs = mass_matrix_inv @ rhs( - t_casadi, y_casadi, p_casadi_stacked + t_casadi, y_and_S, p_casadi_stacked ) model.casadi_rhs = casadi.Function( - "rhs", [t_casadi, y_casadi, p_casadi_stacked], [explicit_rhs] + "rhs", [t_casadi, y_and_S, p_casadi_stacked], [explicit_rhs] ) model.casadi_algebraic = algebraic model.casadi_sensitivities_rhs = jacp_rhs @@ -828,7 +817,15 @@ def solve( """ pybamm.logger.info("Start solving {} with {}".format(model.name, self.name)) - self.calculate_sensitivites = calculate_sensitivities + + # get a list-only version of calculate_sensitivities + if isinstance(calculate_sensitivities, bool): + if calculate_sensitivities: + calculate_sensitivities_list = [p for p in inputs.keys()] + else: + calculate_sensitivities_list = [] + else: + calculate_sensitivities_list = calculate_sensitivities # Make sure model isn't empty if len(model.rhs) == 0 and len(model.algebraic) == 0: @@ -871,7 +868,8 @@ def solve( # If "inputs" is a single dict, "inputs_list" is a list of only one dict. inputs_list = inputs if isinstance(inputs, list) else [inputs] ext_and_inputs_list = [ - self._set_up_ext_and_inputs(model, external_variables, inputs) + self._set_up_ext_and_inputs(model, external_variables, inputs, + calculate_sensitivities_list) for inputs in inputs_list ] @@ -885,27 +883,36 @@ def solve( # Set up (if not done already) timer = pybamm.Timer() if model not in self.models_set_up: + # save sensitivity parameters so we can identify them later on + # (FYI: this is used in the Solution class) + model.calculate_sensitivities = calculate_sensitivities_list + # It is assumed that when len(inputs_list) > 1, model set # up (initial condition, time-scale and length-scale) does # not depend on input parameters. Thefore only `ext_and_inputs[0]` # is passed to `set_up`. # See https://github.com/pybamm-team/PyBaMM/pull/1261 - self.set_up(model, ext_and_inputs_list[0], t_eval, - calculate_sensitivites=calculate_sensitivities) + self.set_up(model, ext_and_inputs_list[0], t_eval) self.models_set_up.update( {model: {"initial conditions": model.concatenated_initial_conditions}} ) else: + # Check that calculate_sensitivites have not been updated + calculate_sensitivities_list.sort() + model.calculate_sensitivities.sort() + if (calculate_sensitivities_list != model.calculate_sensitivities): + model.calculate_sensitivities = calculate_sensitivities_list + self.set_up(model, ext_and_inputs_list[0], t_eval) + ics_set_up = self.models_set_up[model]["initial conditions"] # Check that initial conditions have not been updated if ics_set_up.id != model.concatenated_initial_conditions.id: # If the new initial conditions are different, set up again - self.set_up(model, ext_and_inputs_list[0], t_eval, - calculate_sensitivites=calculate_sensitivities, - ics_only=True) + self.set_up(model, ext_and_inputs_list[0], t_eval, ics_only=True) self.models_set_up[model][ "initial conditions" ] = model.concatenated_initial_conditions + set_up_time = timer.time() timer.reset() @@ -1218,6 +1225,7 @@ def step( pybamm.logger.verbose( "Start stepping {} with {}".format(model.name, self.name) ) + self.set_up(model, ext_and_inputs) self.models_set_up.update( {model: {"initial conditions": model.concatenated_initial_conditions}} @@ -1225,6 +1233,7 @@ def step( t = 0.0 elif model not in self.models_set_up: # Run set up if the model has changed + self.set_up(model, ext_and_inputs) self.models_set_up.update( {model: {"initial conditions": model.concatenated_initial_conditions}} @@ -1406,7 +1415,8 @@ def check_extrapolation(self, solution, events): return [k for k, v in extrap_events.items() if v] - def _set_up_ext_and_inputs(self, model, external_variables, inputs): + def _set_up_ext_and_inputs(self, model, external_variables, inputs, + calculate_sensitivities): """Set up external variables and input parameters""" inputs = inputs or {} @@ -1417,7 +1427,7 @@ def _set_up_ext_and_inputs(self, model, external_variables, inputs): name = input_param.name if name not in inputs: # Don't allow symbolic inputs if using `sensitivity` - if self.calculate_sensitivites: + if calculate_sensitivities: raise pybamm.SolverError( "Cannot have symbolic inputs if explicitly solving forward" "sensitivity equations" diff --git a/pybamm/solvers/casadi_solver.py b/pybamm/solvers/casadi_solver.py index 10d8cda0fa..8059c97980 100644 --- a/pybamm/solvers/casadi_solver.py +++ b/pybamm/solvers/casadi_solver.py @@ -124,7 +124,7 @@ def _integrate(self, model, t_eval, inputs_dict=None): """ # are we solving explicit forward equations? - explicit_sensitivities = bool(self.calculate_sensitivites) + explicit_sensitivities = bool(model.calculate_sensitivities) # Record whether there are any symbolic inputs inputs_dict = inputs_dict or {} @@ -456,7 +456,7 @@ def integer_bisect(): np.array([t_event]), y_event[:, np.newaxis], "event", - sensitivities=bool(self.calculate_sensitivites) + sensitivities=bool(model.calculate_sensitivities) ) solution.integration_time = ( coarse_solution.integration_time + dense_step_sol.integration_time @@ -620,7 +620,7 @@ def _run_integrator(self, model, y0, inputs_dict, pybamm.logger.debug("Running CasADi integrator") # are we solving explicit forward equations? - explicit_sensitivities = bool(self.calculate_sensitivites) + explicit_sensitivities = bool(model.calculate_sensitivities) # by default we extract sensitivities in the solution if we # are calculating the sensitivities diff --git a/pybamm/solvers/scipy_solver.py b/pybamm/solvers/scipy_solver.py index fe1ef0feb2..38b30103ac 100644 --- a/pybamm/solvers/scipy_solver.py +++ b/pybamm/solvers/scipy_solver.py @@ -129,7 +129,7 @@ def event_fn(t, y): y_event = np.array(None) sol = pybamm.Solution( sol.t, sol.y, model, inputs_dict, t_event, y_event, termination, - sensitivities=bool(self.calculate_sensitivites) + sensitivities=bool(model.calculate_sensitivities) ) sol.integration_time = integration_time return sol diff --git a/pybamm/solvers/solution.py b/pybamm/solvers/solution.py index 2531a288ec..c9e3f81424 100644 --- a/pybamm/solvers/solution.py +++ b/pybamm/solvers/solution.py @@ -222,6 +222,7 @@ def _extract_explicit_sensitivities(self, model, y, t_eval, inputs): # Save the full sensitivity matrix sensitivity = {"all": full_sens_matrix} + # also save the sensitivity wrt each parameter (read the columns of the # sensitivity matrix) start = 0 diff --git a/tests/integration/test_models/standard_model_tests.py b/tests/integration/test_models/standard_model_tests.py index a6dbf520d5..3ce73e6113 100644 --- a/tests/integration/test_models/standard_model_tests.py +++ b/tests/integration/test_models/standard_model_tests.py @@ -82,6 +82,34 @@ def test_solving(self, solver=None, t_eval=None): self.solution = self.solver.solve(self.model, t_eval) + def test_solving_with_sensitivities(self, solver=None, t_eval=None): + # Overwrite solver if given + if solver is not None: + self.solver = solver + # Use tighter default tolerances for testing + self.solver.rtol = 1e-8 + self.solver.atol = 1e-8 + + Crate = abs( + self.parameter_values["Current function [A]"] + / self.parameter_values["Nominal cell capacity [A.h]"] + ) + # don't allow zero C-rate + if Crate == 0: + Crate = 1 + if t_eval is None: + t_eval = np.linspace(0, 3600 / Crate, 100) + + # replace a parameter with an input param + param_name = "Negative electrode conductivity [S.m-1]" + neg_electrode_cond = 100.0 + self.parameter_values.update({param_name: "[input]"}) + inputs = {param_name: neg_electrode_cond} + + self.solution_sensitivities = self.solver.solve( + self.model, t_eval, inputs=inputs, calculate_sensitivities=True + ) + def test_outputs(self): # run the standard output tests std_out_test = tests.StandardOutputTests( @@ -96,6 +124,7 @@ def test_all( self.test_processing_parameters(param) self.test_processing_disc(disc) self.test_solving(solver, t_eval) + self.test_solving_with_sensitivities(solver, t_eval) if ( isinstance( diff --git a/tests/unit/test_solvers/test_base_solver.py b/tests/unit/test_solvers/test_base_solver.py index ce59ae1f64..eb8ae4a694 100644 --- a/tests/unit/test_solvers/test_base_solver.py +++ b/tests/unit/test_solvers/test_base_solver.py @@ -347,8 +347,8 @@ def exact_diff_b(y, a, b): model.initial_conditions = {v: 1, u: a * 1} model.convert_to_format = convert_to_format solver = pybamm.IDAKLUSolver(root_method='lm') - solver.set_up(model, calculate_sensitivites=True, - inputs={'a': 0, 'b': 0}) + model.calculate_sensitivities = ['a', 'b'] + solver.set_up(model, inputs={'a': 0, 'b': 0}) all_inputs = [] for v_value in [0.1, -0.2, 1.5, 8.4]: for u_value in [0.13, -0.23, 1.3, 13.4]: diff --git a/tests/unit/test_solvers/test_casadi_solver.py b/tests/unit/test_solvers/test_casadi_solver.py index 8bf3908a39..2fd1e4d456 100644 --- a/tests/unit/test_solvers/test_casadi_solver.py +++ b/tests/unit/test_solvers/test_casadi_solver.py @@ -826,7 +826,7 @@ def test_solve_sensitivity_scalar_var_scalar_input(self): p = pybamm.InputParameter("p") var1 = pybamm.Variable("var1") var2 = pybamm.Variable("var2") - model.rhs = {var1: 0.1 * var1} + model.rhs = {var1: p * var1} model.algebraic = {var2: 2 * var1 - var2} model.initial_conditions = {var1: 1, var2: 2} model.variables = {"var2 squared": var2 ** 2} @@ -843,86 +843,19 @@ def test_solve_sensitivity_scalar_var_scalar_input(self): np.testing.assert_allclose(solution.y[0], np.exp(0.1 * solution.t)) np.testing.assert_allclose( solution.sensitivities["p"], - (solution.t * np.exp(0.1 * solution.t))[:, np.newaxis], + np.stack(( + solution.t * np.exp(0.1 * solution.t), + 2 * solution.t * np.exp(0.1 * solution.t), + )).transpose().reshape(-1, 1), + atol=1e-7 ) np.testing.assert_allclose( - solution["var squared"].data, np.exp(0.1 * solution.t) ** 2 + solution["var2 squared"].data, 4 * np.exp(2 * 0.1 * solution.t) ) np.testing.assert_allclose( - solution["var squared"].sensitivities["p"], - (2 * np.exp(0.1 * solution.t) * solution.t * np.exp(0.1 * solution.t))[ - :, np.newaxis - ], - ) - - # More complicated model - # Create model - model = pybamm.BaseModel() - var = pybamm.Variable("var") - p = pybamm.InputParameter("p") - q = pybamm.InputParameter("q") - r = pybamm.InputParameter("r") - s = pybamm.InputParameter("s") - model.rhs = {var: p * q} - model.initial_conditions = {var: r} - model.variables = {"var times s": var * s} - - # Solve - # Make sure that passing in extra options works - solver = pybamm.CasadiSolver( - rtol=1e-10, atol=1e-10 - ) - t_eval = np.linspace(0, 1, 80) - solution = solver.solve( - model, t_eval, inputs={"p": 0.1, "q": 2, "r": -1, "s": 0.5}, - calculate_sensitivities=True, - ) - np.testing.assert_allclose(solution.y[0], -1 + 0.2 * solution.t) - np.testing.assert_allclose( - solution.sensitivities["p"], (2 * solution.t)[:, np.newaxis], - ) - np.testing.assert_allclose( - solution.sensitivities["q"], (0.1 * solution.t)[:, np.newaxis], - ) - np.testing.assert_allclose(solution.sensitivities["r"], 1) - np.testing.assert_allclose(solution.sensitivities["s"], 0) - np.testing.assert_allclose( - solution.sensitivities["all"], - np.hstack( - [ - solution.sensitivities["p"], - solution.sensitivities["q"], - solution.sensitivities["r"], - solution.sensitivities["s"], - ] - ), - ) - np.testing.assert_allclose( - solution["var times s"].data, 0.5 * (-1 + 0.2 * solution.t) - ) - np.testing.assert_allclose( - solution["var times s"].sensitivities["p"], - 0.5 * (2 * solution.t)[:, np.newaxis], - ) - np.testing.assert_allclose( - solution["var times s"].sensitivities["q"], - 0.5 * (0.1 * solution.t)[:, np.newaxis], - ) - np.testing.assert_allclose(solution["var times s"].sensitivities["r"], 0.5) - np.testing.assert_allclose( - solution["var times s"].sensitivities["s"], - (-1 + 0.2 * solution.t)[:, np.newaxis], - ) - np.testing.assert_allclose( - solution["var times s"].sensitivities["all"], - np.hstack( - [ - solution["var times s"].sensitivities["p"], - solution["var times s"].sensitivities["q"], - solution["var times s"].sensitivities["r"], - solution["var times s"].sensitivities["s"], - ] - ), + solution["var2 squared"].sensitivities["p"], + (8 * solution.t * np.exp(2 * 0.1 * solution.t))[:, np.newaxis], + atol=1e-7 ) def test_solve_sensitivity_vector_var_scalar_input(self): diff --git a/tests/unit/test_solvers/test_idaklu_solver.py b/tests/unit/test_solvers/test_idaklu_solver.py index 8204759ad4..2e547846ff 100644 --- a/tests/unit/test_solvers/test_idaklu_solver.py +++ b/tests/unit/test_solvers/test_idaklu_solver.py @@ -51,6 +51,7 @@ def test_ida_roberts_klu_sensitivities(self): # example provided in sundials # see sundials ida examples pdf for form in ["python", "casadi", "jax"]: + print(form) model = pybamm.BaseModel() model.convert_to_format = form u = pybamm.Variable("u") @@ -68,19 +69,29 @@ def test_ida_roberts_klu_sensitivities(self): t_eval = np.linspace(0, 3, 100) a_value = 0.1 - if form == 'python': - with self.assertRaisesRegex( - NotImplementedError, "explicit sensitivity"): - sol = solver.solve( - model, t_eval, inputs={"a": a_value}, - calculate_sensitivities=True - ) - continue - else: - sol = solver.solve( - model, t_eval, inputs={"a": a_value}, - calculate_sensitivities=True - ) + # solve first without sensitivities + sol = solver.solve( + model, t_eval, inputs={"a": a_value}, + ) + + # test that y[1] remains constant + np.testing.assert_array_almost_equal( + sol.y[1, :], np.ones(sol.t.shape) + ) + + # test that y[0] = to true solution + true_solution = a_value * sol.t + np.testing.assert_array_almost_equal(sol.y[0, :], true_solution) + + # should be no sensitivities calculated + with self.assertRaises(KeyError): + sol.sensitivities["a"] + + # now solve with sensitivities (this should cause set_up to be run again) + sol = solver.solve( + model, t_eval, inputs={"a": a_value}, + calculate_sensitivities=True + ) # test that y[1] remains constant np.testing.assert_array_almost_equal( diff --git a/tests/unit/test_solvers/test_scipy_solver.py b/tests/unit/test_solvers/test_scipy_solver.py index d20c822359..889759a0cc 100644 --- a/tests/unit/test_solvers/test_scipy_solver.py +++ b/tests/unit/test_solvers/test_scipy_solver.py @@ -1,4 +1,3 @@ -# # Tests for the Scipy Solver class # import pybamm From 0b56826d1aef2a363e99a2c414280403865c1e41 Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Wed, 21 Jul 2021 10:08:35 +0100 Subject: [PATCH 40/73] #1477 fix problem related to casadi solver caching integrators --- pybamm/solvers/base_solver.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/pybamm/solvers/base_solver.py b/pybamm/solvers/base_solver.py index 1f8f61f4d7..67aab6e3d2 100644 --- a/pybamm/solvers/base_solver.py +++ b/pybamm/solvers/base_solver.py @@ -880,13 +880,25 @@ def solve( 'when model in format "jax".' ) + # Check that calculate_sensitivites have not been updated + calculate_sensitivities_list.sort() + if not hasattr(model, 'calculate_sensitivities'): + model.calculate_sensitivities = [] + model.calculate_sensitivities.sort() + if (calculate_sensitivities_list != model.calculate_sensitivities): + self.models_set_up.pop(model, None) + # CasadiSolver caches its integrators using model, so delete this too + if isinstance(self, pybamm.CasadiSolver): + self.integrators.pop(model, None) + + # save sensitivity parameters so we can identify them later on + # (FYI: this is used in the Solution class) + model.calculate_sensitivities = calculate_sensitivities_list + # Set up (if not done already) + timer = pybamm.Timer() if model not in self.models_set_up: - # save sensitivity parameters so we can identify them later on - # (FYI: this is used in the Solution class) - model.calculate_sensitivities = calculate_sensitivities_list - # It is assumed that when len(inputs_list) > 1, model set # up (initial condition, time-scale and length-scale) does # not depend on input parameters. Thefore only `ext_and_inputs[0]` @@ -897,13 +909,6 @@ def solve( {model: {"initial conditions": model.concatenated_initial_conditions}} ) else: - # Check that calculate_sensitivites have not been updated - calculate_sensitivities_list.sort() - model.calculate_sensitivities.sort() - if (calculate_sensitivities_list != model.calculate_sensitivities): - model.calculate_sensitivities = calculate_sensitivities_list - self.set_up(model, ext_and_inputs_list[0], t_eval) - ics_set_up = self.models_set_up[model]["initial conditions"] # Check that initial conditions have not been updated if ics_set_up.id != model.concatenated_initial_conditions.id: From 29905b781819ecf8ab0bf9af88d638f732e75029 Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Wed, 21 Jul 2021 10:23:41 +0100 Subject: [PATCH 41/73] #1477 add standard output tests to sensitivity soln to make sure it hasn't messed up the solution --- pybamm/solvers/base_solver.py | 1 - tests/integration/test_models/standard_model_tests.py | 9 +++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/pybamm/solvers/base_solver.py b/pybamm/solvers/base_solver.py index 67aab6e3d2..ac2164a12b 100644 --- a/pybamm/solvers/base_solver.py +++ b/pybamm/solvers/base_solver.py @@ -896,7 +896,6 @@ def solve( model.calculate_sensitivities = calculate_sensitivities_list # Set up (if not done already) - timer = pybamm.Timer() if model not in self.models_set_up: # It is assumed that when len(inputs_list) > 1, model set diff --git a/tests/integration/test_models/standard_model_tests.py b/tests/integration/test_models/standard_model_tests.py index 3ce73e6113..e75cf90861 100644 --- a/tests/integration/test_models/standard_model_tests.py +++ b/tests/integration/test_models/standard_model_tests.py @@ -117,6 +117,15 @@ def test_outputs(self): ) std_out_test.test_all() + # also run them on the sensitivity solution to make sure we havn't messed + # up the solution + std_out_sensitivity_test = tests.StandardOutputTests( + self.model, self.parameter_values, self.disc, + self.solution_sensitivities + ) + std_out_sensitivity_test.test_all() + + def test_all( self, param=None, disc=None, solver=None, t_eval=None, skip_output_tests=False ): From e67880f6222f32212956716a1c3aa4acea6a9307 Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Wed, 21 Jul 2021 13:20:48 +0100 Subject: [PATCH 42/73] #1477 fixes after running integration tests --- pybamm/solvers/casadi_solver.py | 12 +--- pybamm/solvers/solution.py | 27 +++++++-- .../test_models/standard_model_tests.py | 57 +++++++------------ .../test_lithium_ion/test_dfn.py | 12 ++++ .../test_lithium_ion/test_spm.py | 8 +++ .../test_lithium_ion/test_spme.py | 8 +++ 6 files changed, 72 insertions(+), 52 deletions(-) diff --git a/pybamm/solvers/casadi_solver.py b/pybamm/solvers/casadi_solver.py index 8059c97980..f6d3a1d0e1 100644 --- a/pybamm/solvers/casadi_solver.py +++ b/pybamm/solvers/casadi_solver.py @@ -277,16 +277,7 @@ def _integrate(self, model, t_eval, inputs_dict=None): # now we extract sensitivities from the solution if (explicit_sensitivities): - # save original ys[0] and replace with separated soln - # TODO: This is a dodgy hack, perhaps re-init the solution object? - solution._all_ys_and_sens = [solution._all_ys[0][:]] - solution._all_ys[0], solution._sensitivities = \ - solution._extract_explicit_sensitivities( - solution.all_models[0], - solution.all_ys[0], - solution.all_ts[0], - solution.all_inputs[0], - ) + solution.extract_explicit_sensitivities() return solution @@ -300,6 +291,7 @@ def _solve_for_event(self, coarse_solution, init_event_signs): so that only the times up to the event are returned """ pybamm.logger.debug("Solving for events") + model = coarse_solution.all_models[-1] inputs_dict = coarse_solution.all_inputs[-1] inputs = casadi.vertcat(*[x for x in inputs_dict.values()]) diff --git a/pybamm/solvers/solution.py b/pybamm/solvers/solution.py index c9e3f81424..32c4d0488c 100644 --- a/pybamm/solvers/solution.py +++ b/pybamm/solvers/solution.py @@ -92,12 +92,8 @@ def __init__( and all_models[0].len_rhs_and_alg != all_ys[0].shape[0] and all_models[0].len_rhs_and_alg != 0 # for the dummy solver ): - # save original ys[0] and replace with separated soln - self._all_ys_and_sens = [self._all_ys[0][:]] - self._all_ys[0], self._sensitivities = \ - self._extract_explicit_sensitivities( - all_models[0], all_ys[0], all_ts[0], self.all_inputs[0] - ) + self.extract_explicit_sensitivities() + elif isinstance(sensitivities, dict): self._sensitivities = sensitivities else: @@ -151,6 +147,25 @@ def __init__( # Solution now uses CasADi pybamm.citations.register("Andersson2019") + def extract_explicit_sensitivities(self): + for index, (model, ys, ts, inputs) in enumerate( + zip(self.all_models, self.all_ys, self.all_ts, + self.all_inputs) + ): + # TODO: only support sensitivities for one solution atm + # but make sure that sensitivities are removed for all + # solutions + if index == 0: + self._all_ys[index], self._sensitivities = \ + self._extract_explicit_sensitivities( + model, ys, ts, inputs + ) + else: + self._all_ys[index], _ = \ + self._extract_explicit_sensitivities( + model, ys, ts, inputs + ) + def _extract_explicit_sensitivities(self, model, y, t_eval, inputs): """ given a model and a solution y, extracts the sensitivities diff --git a/tests/integration/test_models/standard_model_tests.py b/tests/integration/test_models/standard_model_tests.py index e75cf90861..14617db720 100644 --- a/tests/integration/test_models/standard_model_tests.py +++ b/tests/integration/test_models/standard_model_tests.py @@ -62,7 +62,8 @@ def test_processing_disc(self, disc=None): # Model should still be well-posed after processing self.model.check_well_posedness(post_discretisation=True) - def test_solving(self, solver=None, t_eval=None): + def test_solving(self, solver=None, t_eval=None, inputs=None, + calculate_sensitivities=False): # Overwrite solver if given if solver is not None: self.solver = solver @@ -80,35 +81,11 @@ def test_solving(self, solver=None, t_eval=None): if t_eval is None: t_eval = np.linspace(0, 3600 / Crate, 100) - self.solution = self.solver.solve(self.model, t_eval) - - def test_solving_with_sensitivities(self, solver=None, t_eval=None): - # Overwrite solver if given - if solver is not None: - self.solver = solver - # Use tighter default tolerances for testing - self.solver.rtol = 1e-8 - self.solver.atol = 1e-8 - - Crate = abs( - self.parameter_values["Current function [A]"] - / self.parameter_values["Nominal cell capacity [A.h]"] + self.solution = self.solver.solve( + self.model, t_eval, inputs=inputs, + calculate_sensitivities=calculate_sensitivities ) - # don't allow zero C-rate - if Crate == 0: - Crate = 1 - if t_eval is None: - t_eval = np.linspace(0, 3600 / Crate, 100) - # replace a parameter with an input param - param_name = "Negative electrode conductivity [S.m-1]" - neg_electrode_cond = 100.0 - self.parameter_values.update({param_name: "[input]"}) - inputs = {param_name: neg_electrode_cond} - - self.solution_sensitivities = self.solver.solve( - self.model, t_eval, inputs=inputs, calculate_sensitivities=True - ) def test_outputs(self): # run the standard output tests @@ -117,13 +94,22 @@ def test_outputs(self): ) std_out_test.test_all() - # also run them on the sensitivity solution to make sure we havn't messed - # up the solution - std_out_sensitivity_test = tests.StandardOutputTests( - self.model, self.parameter_values, self.disc, - self.solution_sensitivities - ) - std_out_sensitivity_test.test_all() + def test_sensitivities(self): + param_name = "Negative electrode conductivity [S.m-1]" + neg_electrode_cond = 100.0 + self.parameter_values.update({param_name: "[input]"}) + inputs = {param_name: neg_electrode_cond} + + self.test_processing_parameters() + self.test_processing_disc() + self.test_solving(inputs=inputs, calculate_sensitivities=True) + + if ( + isinstance( + self.model, (pybamm.lithium_ion.BaseModel, pybamm.lead_acid.BaseModel) + ) + ): + self.test_outputs() def test_all( @@ -133,7 +119,6 @@ def test_all( self.test_processing_parameters(param) self.test_processing_disc(disc) self.test_solving(solver, t_eval) - self.test_solving_with_sensitivities(solver, t_eval) if ( isinstance( diff --git a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_dfn.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_dfn.py index fef5bb35de..48eca49167 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_dfn.py +++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_dfn.py @@ -21,6 +21,18 @@ def test_basic_processing(self): ) modeltest.test_all() + def test_sensitivities(self): + options = {"thermal": "isothermal"} + model = pybamm.lithium_ion.DFN(options) + # use Ecker parameters for nonlinear diffusion + param = pybamm.ParameterValues(chemistry=pybamm.parameter_sets.Ecker2015) + var = pybamm.standard_spatial_vars + var_pts = {var.x_n: 10, var.x_s: 10, var.x_p: 10, var.r_n: 5, var.r_p: 5} + modeltest = tests.StandardModelTest( + model, parameter_values=param, var_pts=var_pts + ) + modeltest.test_sensitivities() + def test_basic_processing_1plus1D(self): options = {"current collector": "potential pair", "dimensionality": 1} model = pybamm.lithium_ion.DFN(options) diff --git a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spm.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spm.py index 6439ac7bb1..81bbe466d4 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spm.py +++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spm.py @@ -17,6 +17,14 @@ def test_basic_processing(self): modeltest = tests.StandardModelTest(model, parameter_values=param) modeltest.test_all() + def test_sensitivities(self): + options = {"thermal": "isothermal"} + model = pybamm.lithium_ion.SPM(options) + # use Ecker parameters for nonlinear diffusion + param = pybamm.ParameterValues(chemistry=pybamm.parameter_sets.Ecker2015) + modeltest = tests.StandardModelTest(model, parameter_values=param) + modeltest.test_sensitivities() + def test_basic_processing_1plus1D(self): options = {"current collector": "potential pair", "dimensionality": 1} diff --git a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spme.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spme.py index b9cadb31a4..42db8d9c79 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spme.py +++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spme.py @@ -18,6 +18,14 @@ def test_basic_processing(self): modeltest = tests.StandardModelTest(model, parameter_values=param) modeltest.test_all() + def test_sensitivities(self): + options = {"thermal": "isothermal"} + model = pybamm.lithium_ion.SPMe(options) + # use Ecker parameters for nonlinear diffusion + param = pybamm.ParameterValues(chemistry=pybamm.parameter_sets.Ecker2015) + modeltest = tests.StandardModelTest(model, parameter_values=param) + modeltest.test_sensitivities() + def test_basic_processing_python(self): options = {"thermal": "isothermal"} model = pybamm.lithium_ion.SPMe(options) From 5fdeaa9477b915049cc16c5f44133f3637af9ab2 Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Wed, 21 Jul 2021 13:30:59 +0100 Subject: [PATCH 43/73] #1477 flake8 --- tests/integration/test_models/standard_model_tests.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/integration/test_models/standard_model_tests.py b/tests/integration/test_models/standard_model_tests.py index 14617db720..c61b068495 100644 --- a/tests/integration/test_models/standard_model_tests.py +++ b/tests/integration/test_models/standard_model_tests.py @@ -86,7 +86,6 @@ def test_solving(self, solver=None, t_eval=None, inputs=None, calculate_sensitivities=calculate_sensitivities ) - def test_outputs(self): # run the standard output tests std_out_test = tests.StandardOutputTests( @@ -111,7 +110,6 @@ def test_sensitivities(self): ): self.test_outputs() - def test_all( self, param=None, disc=None, solver=None, t_eval=None, skip_output_tests=False ): From fc72078c07b91b096b9fb71fbee8189b818290c0 Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Wed, 21 Jul 2021 14:01:53 +0100 Subject: [PATCH 44/73] #1477 update changelog --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f0039cafaf..ff7d66a66d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,12 @@ ## Features -- Added fitted expressions for OCPs for the Chen2020 parameter set ([#1526](https://github.com/pybamm-team/PyBaMM/pull/1497)) +- `pybamm.base_solver.solve` function can take a list of input parameters to calculate + the sensitivities of the solution with respect to. Alternatively, it can be set + to `True` to calculate the sensitivities for all input parameters. + ([#1552](https://github.com/pybamm-team/PyBaMM/pull/1552)) +- Added fitted expressions for OCPs for the Chen2020 parameter set + ([#1526](https://github.com/pybamm-team/PyBaMM/pull/1497)) - Added `initial_soc` argument to `Simualtion.solve` for specifying the initial SOC when solving a model ([#1512](https://github.com/pybamm-team/PyBaMM/pull/1512)) - Added `print_name` to some symbols ([#1495](https://github.com/pybamm-team/PyBaMM/pull/1495), [#1497](https://github.com/pybamm-team/PyBaMM/pull/1497)) - Added Base Parameters class and SymPy in dependencies ([#1495](https://github.com/pybamm-team/PyBaMM/pull/1495)) From 3b8a9024463d4ee127ca2338c54b627a2481bb25 Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Wed, 21 Jul 2021 14:04:05 +0100 Subject: [PATCH 45/73] #1477 update changelog --- CHANGELOG.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff7d66a66d..babd507b53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,12 +2,8 @@ ## Features -- `pybamm.base_solver.solve` function can take a list of input parameters to calculate - the sensitivities of the solution with respect to. Alternatively, it can be set - to `True` to calculate the sensitivities for all input parameters. - ([#1552](https://github.com/pybamm-team/PyBaMM/pull/1552)) -- Added fitted expressions for OCPs for the Chen2020 parameter set - ([#1526](https://github.com/pybamm-team/PyBaMM/pull/1497)) +- `pybamm.base_solver.solve` function can take a list of input parameters to calculate the sensitivities of the solution with respect to. Alternatively, it can be set to `True` to calculate the sensitivities for all input parameters ([#1552](https://github.com/pybamm-team/PyBaMM/pull/1552)) +- Added fitted expressions for OCPs for the Chen2020 parameter set ([#1526](https://github.com/pybamm-team/PyBaMM/pull/1497)) - Added `initial_soc` argument to `Simualtion.solve` for specifying the initial SOC when solving a model ([#1512](https://github.com/pybamm-team/PyBaMM/pull/1512)) - Added `print_name` to some symbols ([#1495](https://github.com/pybamm-team/PyBaMM/pull/1495), [#1497](https://github.com/pybamm-team/PyBaMM/pull/1497)) - Added Base Parameters class and SymPy in dependencies ([#1495](https://github.com/pybamm-team/PyBaMM/pull/1495)) From 6ce1d5da0cabaef698bfbaf699f5ae3db68da92c Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Thu, 22 Jul 2021 08:38:59 +0100 Subject: [PATCH 46/73] #1477 fix bugs in scikits odes tests --- pybamm/solvers/scikits_dae_solver.py | 2 -- pybamm/solvers/scikits_ode_solver.py | 2 -- tox.ini | 2 +- 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/pybamm/solvers/scikits_dae_solver.py b/pybamm/solvers/scikits_dae_solver.py index 869e77c68a..eff037cc77 100644 --- a/pybamm/solvers/scikits_dae_solver.py +++ b/pybamm/solvers/scikits_dae_solver.py @@ -162,8 +162,6 @@ def jacfn(t, y, ydot, residuals, cj, J): t_root, np.transpose(sol.roots.y), termination, - model=model, - inputs=inputs_dict, ) sol.integration_time = integration_time return sol diff --git a/pybamm/solvers/scikits_ode_solver.py b/pybamm/solvers/scikits_ode_solver.py index 8846121ba2..0c8913b9c8 100644 --- a/pybamm/solvers/scikits_ode_solver.py +++ b/pybamm/solvers/scikits_ode_solver.py @@ -177,8 +177,6 @@ def jac_times_setupfn(t, y, fy, userdata): t_root, np.transpose(sol.roots.y), termination, - model=model, - inputs=inputs_dict, ) sol.integration_time = integration_time return sol diff --git a/tox.ini b/tox.ini index 1501988e90..6aa488bc8b 100644 --- a/tox.ini +++ b/tox.ini @@ -19,7 +19,7 @@ deps = !windows-!mac: scikits.odes commands = - tests: python run-tests.py --unit --folder all + tests: python tests/unit/test_solvers/test_scikits_solvers.py quick: python run-tests.py --unit examples: python run-tests.py --examples dev-!windows-!mac: sh -c "echo export LD_LIBRARY_PATH={env:LD_LIBRARY_PATH} >> {envbindir}/activate" From f7a3a14b17d57c81e7a2e5b2fbb5dc64fc417389 Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Thu, 22 Jul 2021 11:48:50 +0100 Subject: [PATCH 47/73] #1477 remove old notebook --- examples/notebooks/DFN-sensitivity.ipynb | 366 ----------------------- 1 file changed, 366 deletions(-) delete mode 100644 examples/notebooks/DFN-sensitivity.ipynb diff --git a/examples/notebooks/DFN-sensitivity.ipynb b/examples/notebooks/DFN-sensitivity.ipynb deleted file mode 100644 index 47fb8e597d..0000000000 --- a/examples/notebooks/DFN-sensitivity.ipynb +++ /dev/null @@ -1,366 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Sensitivity analysis for the DFN" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Example showing how to perform sensitivity analysis for the DFN with PyBaMM" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Note: you may need to restart the kernel to use updated packages.\n" - ] - } - ], - "source": [ - "%pip install pybamm -q\n", - "import pybamm\n", - "import numpy as np" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Load model" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "model = pybamm.lithium_ion.SPMe()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Before performing a sensitivity analysis, we scale the parameters with their reference values. Some parameters are functions of states, but we can evaluate them at appropriate values of the states to obtain a reference value. In this notebook, we choose to study the effect on the voltage of:\n", - "- negative particle diffusivity (via Ds_n)\n", - "- positive particle diffusivity (via Ds_p)\n", - "- electrolyte diffusivity (via D_e)\n", - "- electrolyte conductivity (via kappa_e)\n", - "- negative electrode kinetics (via j0_n)\n", - "- positive electrode kinetics (via j0_p)" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "param = model.default_parameter_values\n", - "# Get reference values for evaluating functions\n", - "ce_ref = param[\"Typical electrolyte concentration [mol.m-3]\"]\n", - "csn_ref = param[\"Maximum concentration in negative electrode [mol.m-3]\"]\n", - "csp_ref = param[\"Maximum concentration in positive electrode [mol.m-3]\"]\n", - "T_ref = param[\"Reference temperature [K]\"]\n", - "# Evaluate functions at reference values\n", - "Dsn_ref = param[\"Negative electrode diffusivity [m2.s-1]\"](0.5, T_ref).evaluate()\n", - "Dsp_ref = param[\"Positive electrode diffusivity [m2.s-1]\"](0.5, T_ref).evaluate()\n", - "De_ref = param[\"Electrolyte diffusivity [m2.s-1]\"](ce_ref, T_ref).evaluate()\n", - "kappae_ref = param[\"Electrolyte conductivity [S.m-1]\"](ce_ref, T_ref).evaluate()\n", - "j0n_ref = param.evaluate(param[\"Negative electrode exchange-current density [A.m-2]\"](ce_ref, 0.5 * csn_ref, T_ref))\n", - "j0p_ref = param.evaluate(param[\"Positive electrode exchange-current density [A.m-2]\"](ce_ref, 0.5 * csp_ref, T_ref))" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "param[\"Negative electrode diffusivity [m2.s-1]\"] = Dsn_ref * pybamm.InputParameter(\"Dsn\")\n", - "param[\"Positive electrode diffusivity [m2.s-1]\"] = Dsp_ref * pybamm.InputParameter(\"Dsp\")\n", - "# param[\"Electrolyte diffusivity [m2.s-1]\"] = De_ref * pybamm.InputParameter(\"D_e\")\n", - "# param[\"Electrolyte conductivity [S.m-1]\"] = kappae_ref * pybamm.InputParameter(\"kappa_e\")\n", - "# param[\"Negative electrode exchange-current density [A.m-2]\"] = j0n_ref * pybamm.InputParameter(\"j0n\")\n", - "# param[\"Positive electrode exchange-current density [A.m-2]\"] = j0p_ref * pybamm.InputParameter(\"j0p\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Create simulation, run and read solution" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [], - "source": [ - "solver = pybamm.CasadiSolver(mode=\"fast\", sensitivity=\"casadi\")\n", - "sim = pybamm.Simulation(model, parameter_values=param, solver=solver)\n", - "solution = sim.solve(t_eval=np.linspace(0,3600), inputs={\"Dsn\": 1, \"Dsp\": 1})" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "0.015949444000000312" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "solution.solve_time" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Since we have not specified the parameter values when solving, the resulting solution contains _symbolic_ variables, such as the voltage" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "CPU times: user 3.73 s, sys: 194 ms, total: 3.92 s\n", - "Wall time: 3.91 s\n" - ] - }, - { - "data": { - "text/plain": [ - "{'all': DM(sparse: 1500-by-100, 73500 nnz\n", - " (30, 0) -> -8.80308e-05\n", - " (31, 0) -> -9.35556e-05\n", - " (32, 0) -> -0.000107887\n", - " ...\n", - " (1497, 98) -> 0.0170617\n", - " (1498, 98) -> 0.021474\n", - " (1499, 98) -> 0.0260439),\n", - " 'Dsn': DM([00, 00, 00, ..., 0.0170617, 0.021474, 0.0260439]),\n", - " 'Dsp': DM([00, 00, 00, ..., 00, 00, 00])}" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "%%time\n", - "solution[\"X-averaged negative particle concentration\"].sensitivity" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "metadata": {}, - "outputs": [], - "source": [ - "solver = pybamm.CasadiSolver(mode=\"fast\")\n", - "sim = pybamm.Simulation(model, parameter_values=param, solver=solver)\n", - "solution = sim.solve(t_eval=np.linspace(0,3600), inputs={\"Dsn\": 1, \"Dsp\": 1})" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "0.005656635999997661" - ] - }, - "execution_count": 18, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "solution.solve_time" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "V = solution[\"Terminal voltage [V]\"]\n", - "V" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now we can evaluate the voltage at specific values for the input parameters to get both the value" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'all': DM([0, 0.000305216, 0.000323451, 0.000307878, 0.000281315, 0.000252542, 0.000226269, 0.00020523, 0.000191031, 0.000184763, 0.000187613, 0.000201625, 0.000230913, 0.000283916, 0.000377805, 0.000547086, 0.000859691, 0.00144399, 0.00252183, 0.00440237, 0.00728527, 0.0106768, 0.0129031, 0.0123404, 0.00953625, 0.00642767, 0.00416935, 0.00284452, 0.00214823, 0.00179002, 0.00157891, 0.00140318, 0.00120162, 0.000947612, 0.000645371, 0.0003332, 8.76979e-05, 2.15342e-05, 0.000267893, 0.000949333, 0.00213811, 0.00382395, 0.00590563, 0.00821168, 0.0105401, 0.0127, 0.0145411, 0.0159704, 0.0169657, 0.017614]),\n", - " 'Dsn': DM([0, 0.000305216, 0.000323451, 0.000307878, 0.000281315, 0.000252542, 0.000226269, 0.00020523, 0.000191031, 0.000184763, 0.000187613, 0.000201625, 0.000230913, 0.000283916, 0.000377805, 0.000547086, 0.000859691, 0.00144399, 0.00252183, 0.00440237, 0.00728527, 0.0106768, 0.0129031, 0.0123404, 0.00953625, 0.00642767, 0.00416935, 0.00284452, 0.00214823, 0.00179002, 0.00157891, 0.00140318, 0.00120162, 0.000947612, 0.000645371, 0.0003332, 8.76979e-05, 2.15342e-05, 0.000267893, 0.000949333, 0.00213811, 0.00382395, 0.00590563, 0.00821168, 0.0105401, 0.0127, 0.0145411, 0.0159704, 0.0169657, 0.017614])}" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "V.sensitivity" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "%%time\n", - "V.value({\"Dsn\": 1, \"Dsp\": 1, \"D_e\": 1, \"kappa_e\": 1, \"j0n\": 1, \"j0p\": 1})" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "and sensitivity" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "%%time\n", - "sens = V.sensitivity({\"Dsn\": 1, \"Dsp\": 1, \"D_e\": 1, \"kappa_e\": 1, \"j0n\": 1, \"j0p\": 1})" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "sens" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [ - { - "ename": "AttributeError", - "evalue": "'ProcessedVariable' object has no attribute 'symbolic_inputs_dict'", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mAttributeError\u001b[0m Traceback (most recent call last)", - "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0minputs\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m{\u001b[0m\u001b[0mk\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;36m1\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mk\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mV\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msymbolic_inputs_dict\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mkeys\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m}\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 2\u001b[0m \u001b[0mh\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;36m1e-6\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 3\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mk\u001b[0m \u001b[0;32min\u001b[0m \u001b[0minputs\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mkeys\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 4\u001b[0m \u001b[0mV_down\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mV\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mvalue\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0minputs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 5\u001b[0m \u001b[0minputs\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mk\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;36m1\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0mh\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;31mAttributeError\u001b[0m: 'ProcessedVariable' object has no attribute 'symbolic_inputs_dict'" - ] - } - ], - "source": [ - "inputs = {k: 1 for k in V.symbolic_inputs_dict.keys()}\n", - "h = 1e-6\n", - "for k in inputs.keys():\n", - " V_down = V.value(inputs)\n", - " inputs[k] = 1 + h\n", - " V_up = V.value(inputs)\n", - " sens = (V_up - V_down) / h\n", - " print(sens)\n", - " inputs[k] = 1" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "inputs" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.7" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} From c7543b1f8a2837df780172c020d88e536e8a6455 Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Thu, 22 Jul 2021 12:19:29 +0100 Subject: [PATCH 48/73] #1477 restore tox.ini --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 6aa488bc8b..1501988e90 100644 --- a/tox.ini +++ b/tox.ini @@ -19,7 +19,7 @@ deps = !windows-!mac: scikits.odes commands = - tests: python tests/unit/test_solvers/test_scikits_solvers.py + tests: python run-tests.py --unit --folder all quick: python run-tests.py --unit examples: python run-tests.py --examples dev-!windows-!mac: sh -c "echo export LD_LIBRARY_PATH={env:LD_LIBRARY_PATH} >> {envbindir}/activate" From 8d361fe498831ac003f19a577c2cb8db21b777dd Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Thu, 22 Jul 2021 14:49:15 +0100 Subject: [PATCH 49/73] #1477 fix bug in idaklu --- pybamm/solvers/c_solvers/idaklu.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pybamm/solvers/c_solvers/idaklu.cpp b/pybamm/solvers/c_solvers/idaklu.cpp index 2aea5926f2..22cba79947 100644 --- a/pybamm/solvers/c_solvers/idaklu.cpp +++ b/pybamm/solvers/c_solvers/idaklu.cpp @@ -343,7 +343,9 @@ Solution solve(np_array t_np, np_array y0_np, np_array yp0_np, // set initial value yval = N_VGetArrayPointer(yy); - ySval = N_VGetArrayPointer(yyS[0]); + if (number_of_parameters > 0) { + ySval = N_VGetArrayPointer(yyS[0]); + } ypval = N_VGetArrayPointer(yp); atval = N_VGetArrayPointer(avtol); int i; From 4c7bbe5494d2ef78e7df070144ad8f9d2649fa8b Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Thu, 22 Jul 2021 15:16:00 +0100 Subject: [PATCH 50/73] #1477 skip sens test in base_solver if klu not installed --- tests/unit/test_solvers/test_base_solver.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/test_solvers/test_base_solver.py b/tests/unit/test_solvers/test_base_solver.py index eb8ae4a694..7967c96949 100644 --- a/tests/unit/test_solvers/test_base_solver.py +++ b/tests/unit/test_solvers/test_base_solver.py @@ -325,6 +325,7 @@ def test_extrapolation_warnings(self): with self.assertWarns(pybamm.SolverWarning): solver.solve(model, t_eval=[0, 1]) + @unittest.skipIf(not pybamm.have_idaklu(), "idaklu solver is not installed") def test_sensitivities(self): def exact_diff_a(y, a, b): From 03528da85812f39acc2a849f64301fa4f78d3d77 Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Mon, 2 Aug 2021 17:40:35 +0100 Subject: [PATCH 51/73] #1477 add some tests and remove uncovered lines not neccessary --- .../operations/evaluate_python.py | 12 +----- pybamm/solvers/base_solver.py | 21 +--------- pybamm/solvers/solution.py | 6 +-- .../test_solvers/test_processed_variable.py | 42 +++++++++++++++++++ tests/unit/test_solvers/test_solution.py | 6 +++ 5 files changed, 53 insertions(+), 34 deletions(-) diff --git a/pybamm/expression_tree/operations/evaluate_python.py b/pybamm/expression_tree/operations/evaluate_python.py index 1ad3939564..d8eb5cd431 100644 --- a/pybamm/expression_tree/operations/evaluate_python.py +++ b/pybamm/expression_tree/operations/evaluate_python.py @@ -685,11 +685,7 @@ def evaluate(self, t=None, y=None, y_dot=None, inputs=None, known_evals=None): result = self._jac_evaluate(*self._constants, t, y, y_dot, inputs, known_evals) result = result.reshape(result.shape[0], -1) - # don't need known_evals, but need to reproduce Symbol.evaluate signature - if known_evals is not None: - return result, known_evals - else: - return result + return result class EvaluatorJaxSensitivities: @@ -708,8 +704,4 @@ def evaluate(self, t=None, y=None, y_dot=None, inputs=None, known_evals=None): # execute code result = self._jac_evaluate(*self._constants, t, y, y_dot, inputs, known_evals) - # don't need known_evals, but need to reproduce Symbol.evaluate signature - if known_evals is not None: - return result, known_evals - else: - return result + return result diff --git a/pybamm/solvers/base_solver.py b/pybamm/solvers/base_solver.py index ac2164a12b..deccecbf10 100644 --- a/pybamm/solvers/base_solver.py +++ b/pybamm/solvers/base_solver.py @@ -220,13 +220,6 @@ def set_up(self, model, inputs=None, t_eval=None, ics_only=False): if model.calculate_sensitivities and not isinstance(self, pybamm.IDAKLUSolver): calculate_sensitivities_explicit = True - if calculate_sensitivities_explicit and model.convert_to_format != 'casadi': - raise NotImplementedError( - "Sensitivities only supported for:\n" - " - model.convert_to_format = 'casadi'\n" - " - IDAKLUSolver (any convert_to_format)" - ) - # if we are calculating sensitivities explicitly then the number of # equations will change if calculate_sensitivities_explicit: @@ -284,12 +277,7 @@ def report(string): report(f"Converting {name} to jax") func = pybamm.EvaluatorJax(func) jacp = None - if calculate_sensitivities_explicit: - raise NotImplementedError( - "explicit sensitivity equations not supported for " - "convert_to_format='jax'" - ) - elif model.calculate_sensitivities: + if model.calculate_sensitivities: report(( f"Calculating sensitivities for {name} with respect " f"to parameters {model.calculate_sensitivities} using jax" @@ -308,12 +296,7 @@ def report(string): elif model.convert_to_format != "casadi": # Process with pybamm functions, optionally converting # to python evaluator - if calculate_sensitivities_explicit: - raise NotImplementedError( - "explicit sensitivity equations not supported for " - "convert_to_format='{}'".format(model.convert_to_format) - ) - elif model.calculate_sensitivities: + if model.calculate_sensitivities: report(( f"Calculating sensitivities for {name} with respect " f"to parameters {model.calculate_sensitivities}" diff --git a/pybamm/solvers/solution.py b/pybamm/solvers/solution.py index 32c4d0488c..abc1e52a05 100644 --- a/pybamm/solvers/solution.py +++ b/pybamm/solvers/solution.py @@ -97,7 +97,7 @@ def __init__( elif isinstance(sensitivities, dict): self._sensitivities = sensitivities else: - raise RuntimeError('sensitivities arg needs to be a bool or dict') + raise TypeError('sensitivities arg needs to be a bool or dict') self._t_event = t_event self._y_event = y_event @@ -304,10 +304,6 @@ def all_ts(self): def all_ys(self): return self._all_ys - @property - def all_ys_and_sens(self): - return self._all_ys_and_sens - @property def all_models(self): """Model(s) used for solution""" diff --git a/tests/unit/test_solvers/test_processed_variable.py b/tests/unit/test_solvers/test_processed_variable.py index 6d692da6ea..b7b3d0b373 100644 --- a/tests/unit/test_solvers/test_processed_variable.py +++ b/tests/unit/test_solvers/test_processed_variable.py @@ -58,6 +58,48 @@ def test_processed_variable_0D(self): ) np.testing.assert_array_equal(processed_var.entries, y_sol[0]) + # check empty sensitivity works + + def test_processed_variable_0D_no_sensitivity(self): + # without space + t = pybamm.t + y = pybamm.StateVector(slice(0, 1)) + var = t * y + var.mesh = None + t_sol = np.linspace(0, 1) + y_sol = np.array([np.linspace(0, 5)]) + var_casadi = to_casadi(var, y_sol) + processed_var = pybamm.ProcessedVariable( + [var], + [var_casadi], + pybamm.Solution(t_sol, y_sol, pybamm.BaseModel(), {}), + warn=False, + ) + + # test no inputs (i.e. no sensitivity) + self.assertDictEqual(processed_var.sensitivities, {}) + + # with parameter + t = pybamm.t + y = pybamm.StateVector(slice(0, 1)) + a = pybamm.InputParameter('a') + var = t * y * a + var.mesh = None + t_sol = np.linspace(0, 1) + y_sol = np.array([np.linspace(0, 5)]) + inputs = {'a': np.array([1.0])} + var_casadi = to_casadi(var, y_sol, inputs=inputs) + processed_var = pybamm.ProcessedVariable( + [var], + [var_casadi], + pybamm.Solution(t_sol, y_sol, pybamm.BaseModel(), inputs), + warn=False, + ) + + # test no sensitivity raises error + with self.assertRaisesRegex(ValueError, 'Cannot compute sensitivities'): + print(processed_var.sensitivities) + def test_processed_variable_1D(self): t = pybamm.t var = pybamm.Variable("var", domain=["negative electrode", "separator"]) diff --git a/tests/unit/test_solvers/test_solution.py b/tests/unit/test_solvers/test_solution.py index 5ded64e328..3edf888f2f 100644 --- a/tests/unit/test_solvers/test_solution.py +++ b/tests/unit/test_solvers/test_solution.py @@ -22,6 +22,12 @@ def test_init(self): self.assertEqual(sol.all_inputs, [{}]) self.assertIsInstance(sol.all_models[0], pybamm.BaseModel) + def test_sensitivities(self): + t = np.linspace(0, 1) + y = np.tile(t, (20, 1)) + with self.assertRaises(TypeError): + pybamm.Solution(t, y, pybamm.BaseModel(), {}, sensitivities=1.0) + def test_errors(self): bad_ts = [np.array([1, 2, 3]), np.array([3, 4, 5])] sol = pybamm.Solution( From 06984c4279510e285dbf4f33505c70f95fd3363e Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Mon, 2 Aug 2021 18:04:32 +0100 Subject: [PATCH 52/73] #1477 check sensitivities with fd in integration tests --- pybamm/solvers/base_solver.py | 33 ++++++++++++++----- .../test_models/standard_model_tests.py | 17 ++++++++++ 2 files changed, 41 insertions(+), 9 deletions(-) diff --git a/pybamm/solvers/base_solver.py b/pybamm/solvers/base_solver.py index deccecbf10..576a397c58 100644 --- a/pybamm/solvers/base_solver.py +++ b/pybamm/solvers/base_solver.py @@ -608,18 +608,33 @@ def jacp(*args, **kwargs): # if we have changed the equations to include the explicit sensitivity # equations, then we also need to update the mass matrix + n_inputs = model.len_rhs_sens // model.len_rhs + n_state_without_sens = model.len_rhs_and_alg if calculate_sensitivities_explicit: - n_inputs = model.len_rhs_sens // model.len_rhs - model.mass_matrix_inv = pybamm.Matrix( - block_diag( - [model.mass_matrix_inv.entries] * (n_inputs + 1), format="csr" + if model.mass_matrix.shape[0] == n_state_without_sens: + model.mass_matrix_inv = pybamm.Matrix( + block_diag( + [model.mass_matrix_inv.entries] * (n_inputs + 1), + format="csr" + ) ) - ) - model.mass_matrix = pybamm.Matrix( - block_diag( - [model.mass_matrix.entries] * (n_inputs + 1), format="csr" + model.mass_matrix = pybamm.Matrix( + block_diag( + [model.mass_matrix.entries] * (n_inputs + 1), format="csr" + ) + ) + else: + # take care if calculate_sensitivites used then not used + n_state_with_sens = model.len_rhs_and_alg * (n_inputs + 1) + if model.mass_matrix.shape[0] == n_state_with_sens: + model.mass_matrix_inv = pybamm.Matrix( + model.mass_matrix_inv.entries[:n_state_without_sens, + :n_state_without_sens] + ) + model.mass_matrix = pybamm.Matrix( + model.mass_matrix.entries[:n_state_without_sens, + :n_state_without_sens] ) - ) # Save CasADi functions for the CasADi solver # Note: when we pass to casadi the ode part of the problem must be in diff --git a/tests/integration/test_models/standard_model_tests.py b/tests/integration/test_models/standard_model_tests.py index c61b068495..839e60f1d0 100644 --- a/tests/integration/test_models/standard_model_tests.py +++ b/tests/integration/test_models/standard_model_tests.py @@ -101,8 +101,25 @@ def test_sensitivities(self): self.test_processing_parameters() self.test_processing_disc() + self.test_solving(inputs=inputs, calculate_sensitivities=True) + # check via finite differencing + h = 1e-6 + inputs_plus = {param_name: neg_electrode_cond + 0.5 * h} + inputs_neg = {param_name: neg_electrode_cond - 0.5 * h} + sol_plus = self.solver.solve( + self.model, self.solution.all_ts[0], inputs=inputs_plus + ) + sol_neg = self.solver.solve( + self.model, self.solution.all_ts[0], inputs=inputs_neg + ) + n = self.solution.sensitivities[param_name].shape[0] + np.testing.assert_array_almost_equal( + self.solution.sensitivities[param_name], + ((sol_plus.y - sol_neg.y) / h).reshape((n, 1)) + ) + if ( isinstance( self.model, (pybamm.lithium_ion.BaseModel, pybamm.lead_acid.BaseModel) From a1cf26e29e4e361f1cf22e03c444704005589883 Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Mon, 2 Aug 2021 18:12:48 +0100 Subject: [PATCH 53/73] #1477 fix codacity errors --- pybamm/solvers/c_solvers/idaklu.cpp | 6 +++--- tests/unit/test_solvers/test_idaklu_solver.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pybamm/solvers/c_solvers/idaklu.cpp b/pybamm/solvers/c_solvers/idaklu.cpp index 22cba79947..d08796ba95 100644 --- a/pybamm/solvers/c_solvers/idaklu.cpp +++ b/pybamm/solvers/c_solvers/idaklu.cpp @@ -396,9 +396,9 @@ Solution solve(np_array t_np, np_array y0_np, np_array yp0_np, if (number_of_parameters > 0) { - retval = IDASensInit(ida_mem, number_of_parameters, - IDA_SIMULTANEOUS, sensitivities, yyS, ypS); - retval = IDASensEEtolerances(ida_mem); + IDASensInit(ida_mem, number_of_parameters, + IDA_SIMULTANEOUS, sensitivities, yyS, ypS); + IDASensEEtolerances(ida_mem); } int t_i = 1; diff --git a/tests/unit/test_solvers/test_idaklu_solver.py b/tests/unit/test_solvers/test_idaklu_solver.py index 2e547846ff..8e31fed297 100644 --- a/tests/unit/test_solvers/test_idaklu_solver.py +++ b/tests/unit/test_solvers/test_idaklu_solver.py @@ -85,7 +85,7 @@ def test_ida_roberts_klu_sensitivities(self): # should be no sensitivities calculated with self.assertRaises(KeyError): - sol.sensitivities["a"] + print(sol.sensitivities["a"]) # now solve with sensitivities (this should cause set_up to be run again) sol = solver.solve( From 57262510e9b68593591db4dd9037ca4926a15522 Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Mon, 2 Aug 2021 20:18:26 +0100 Subject: [PATCH 54/73] #1477 fix bug in base_solver --- pybamm/solvers/base_solver.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pybamm/solvers/base_solver.py b/pybamm/solvers/base_solver.py index 576a397c58..6cce86f81f 100644 --- a/pybamm/solvers/base_solver.py +++ b/pybamm/solvers/base_solver.py @@ -608,9 +608,9 @@ def jacp(*args, **kwargs): # if we have changed the equations to include the explicit sensitivity # equations, then we also need to update the mass matrix - n_inputs = model.len_rhs_sens // model.len_rhs n_state_without_sens = model.len_rhs_and_alg if calculate_sensitivities_explicit: + n_inputs = model.len_rhs_sens // model.len_rhs if model.mass_matrix.shape[0] == n_state_without_sens: model.mass_matrix_inv = pybamm.Matrix( block_diag( @@ -625,8 +625,7 @@ def jacp(*args, **kwargs): ) else: # take care if calculate_sensitivites used then not used - n_state_with_sens = model.len_rhs_and_alg * (n_inputs + 1) - if model.mass_matrix.shape[0] == n_state_with_sens: + if model.mass_matrix.shape[0] > n_state_without_sens: model.mass_matrix_inv = pybamm.Matrix( model.mass_matrix_inv.entries[:n_state_without_sens, :n_state_without_sens] From f9daa06b4b9cbeb9f5f499acf0338905e600b410 Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Mon, 2 Aug 2021 20:19:56 +0100 Subject: [PATCH 55/73] #1477 make fix better --- pybamm/solvers/base_solver.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/pybamm/solvers/base_solver.py b/pybamm/solvers/base_solver.py index 6cce86f81f..0a9d976d6c 100644 --- a/pybamm/solvers/base_solver.py +++ b/pybamm/solvers/base_solver.py @@ -608,10 +608,9 @@ def jacp(*args, **kwargs): # if we have changed the equations to include the explicit sensitivity # equations, then we also need to update the mass matrix - n_state_without_sens = model.len_rhs_and_alg if calculate_sensitivities_explicit: n_inputs = model.len_rhs_sens // model.len_rhs - if model.mass_matrix.shape[0] == n_state_without_sens: + if model.mass_matrix.shape[0] == model.len_rhs_and_alg: model.mass_matrix_inv = pybamm.Matrix( block_diag( [model.mass_matrix_inv.entries] * (n_inputs + 1), @@ -625,14 +624,14 @@ def jacp(*args, **kwargs): ) else: # take care if calculate_sensitivites used then not used - if model.mass_matrix.shape[0] > n_state_without_sens: + if model.mass_matrix.shape[0] > model.len_rhs_and_alg: model.mass_matrix_inv = pybamm.Matrix( - model.mass_matrix_inv.entries[:n_state_without_sens, - :n_state_without_sens] + model.mass_matrix_inv.entries[:model.len_rhs_and_alg, + :model.len_rhs_and_alg] ) model.mass_matrix = pybamm.Matrix( - model.mass_matrix.entries[:n_state_without_sens, - :n_state_without_sens] + model.mass_matrix.entries[:model.len_rhs_and_alg, + :model.len_rhs_and_alg] ) # Save CasADi functions for the CasADi solver From 241af9a9a75494f5f5b72b99721c8ba585dd73db Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Mon, 2 Aug 2021 21:47:25 +0100 Subject: [PATCH 56/73] #1477 fix mass matrix inv bug --- pybamm/solvers/base_solver.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pybamm/solvers/base_solver.py b/pybamm/solvers/base_solver.py index 0a9d976d6c..25f1014c29 100644 --- a/pybamm/solvers/base_solver.py +++ b/pybamm/solvers/base_solver.py @@ -610,7 +610,8 @@ def jacp(*args, **kwargs): # equations, then we also need to update the mass matrix if calculate_sensitivities_explicit: n_inputs = model.len_rhs_sens // model.len_rhs - if model.mass_matrix.shape[0] == model.len_rhs_and_alg: + if (model.mass_matrix is not None + and model.mass_matrix.shape[0] == model.len_rhs_and_alg): model.mass_matrix_inv = pybamm.Matrix( block_diag( [model.mass_matrix_inv.entries] * (n_inputs + 1), @@ -624,10 +625,11 @@ def jacp(*args, **kwargs): ) else: # take care if calculate_sensitivites used then not used - if model.mass_matrix.shape[0] > model.len_rhs_and_alg: + if (model.mass_matrix is not None and + model.mass_matrix.shape[0] > model.len_rhs_and_alg): model.mass_matrix_inv = pybamm.Matrix( - model.mass_matrix_inv.entries[:model.len_rhs_and_alg, - :model.len_rhs_and_alg] + model.mass_matrix_inv.entries[:model.len_rhs, + :model.len_rhs] ) model.mass_matrix = pybamm.Matrix( model.mass_matrix.entries[:model.len_rhs_and_alg, From 073eb589462bfa89d9275bdd6ee5c4a6d29dbfb9 Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Tue, 3 Aug 2021 17:39:18 +0100 Subject: [PATCH 57/73] #1477 integration tests work ok, pretty poor accuracy (perhaps on fd?) --- pybamm/solvers/idaklu_solver.py | 5 +- pybamm/solvers/solution.py | 47 ++++++------------ .../test_models/standard_model_tests.py | 48 ++++++++++--------- .../test_lithium_ion/test_dfn.py | 5 +- .../test_lithium_ion/test_spm.py | 12 ++++- .../test_lithium_ion/test_spme.py | 5 +- 6 files changed, 61 insertions(+), 61 deletions(-) diff --git a/pybamm/solvers/idaklu_solver.py b/pybamm/solvers/idaklu_solver.py index 4df4cbf0fb..9689d80f6b 100644 --- a/pybamm/solvers/idaklu_solver.py +++ b/pybamm/solvers/idaklu_solver.py @@ -321,10 +321,11 @@ def sensfn(resvalS, t, y, yp, yS, ypS): number_of_states = y0.size y_out = sol.y.reshape((number_of_timesteps, number_of_states)) - # return solution, we need to tranpose y to match scipy's interface + # return sensitivity solution, we need to flatten yS to + # (#timesteps * #states,) to match format used by Solution if number_of_sensitivity_parameters != 0: yS_out = { - name: sol.yS[i].transpose() for i, name in enumerate(sens0.keys()) + name: sol.yS[i].reshape(-1, 1) for i, name in enumerate(sens0.keys()) } else: yS_out = False diff --git a/pybamm/solvers/solution.py b/pybamm/solvers/solution.py index abc1e52a05..40ef2fc635 100644 --- a/pybamm/solvers/solution.py +++ b/pybamm/solvers/solution.py @@ -81,23 +81,10 @@ def __init__( else: self.all_inputs = all_inputs - # sensitivities - if isinstance(sensitivities, bool): - self._sensitivities = {} - # if solution consists of explicit sensitivity equations, extract them - if ( - sensitivities is True - and all_models[0] is not None - and not isinstance(all_ys[0], casadi.Function) - and all_models[0].len_rhs_and_alg != all_ys[0].shape[0] - and all_models[0].len_rhs_and_alg != 0 # for the dummy solver - ): - self.extract_explicit_sensitivities() - - elif isinstance(sensitivities, dict): - self._sensitivities = sensitivities - else: + # sensitivities must be a dict or bool + if not isinstance(sensitivities, (bool, dict)): raise TypeError('sensitivities arg needs to be a bool or dict') + self._sensitivities = sensitivities self._t_event = t_event self._y_event = y_event @@ -148,23 +135,10 @@ def __init__( pybamm.citations.register("Andersson2019") def extract_explicit_sensitivities(self): - for index, (model, ys, ts, inputs) in enumerate( - zip(self.all_models, self.all_ys, self.all_ts, - self.all_inputs) - ): - # TODO: only support sensitivities for one solution atm - # but make sure that sensitivities are removed for all - # solutions - if index == 0: - self._all_ys[index], self._sensitivities = \ - self._extract_explicit_sensitivities( - model, ys, ts, inputs - ) - else: - self._all_ys[index], _ = \ - self._extract_explicit_sensitivities( - model, ys, ts, inputs - ) + self._y, self._sensitivities = \ + self._extract_explicit_sensitivities( + self.all_models[0], self.y, self.t, self.all_inputs[0] + ) def _extract_explicit_sensitivities(self, model, y, t_eval, inputs): """ @@ -277,11 +251,18 @@ def y(self): return self._y except AttributeError: self.set_y() + + # if y is evaluated before sensitivities then need to extract them + if isinstance(self._sensitivities, bool) and self._sensitivities: + self.extract_explicit_sensitivities() + return self._y @property def sensitivities(self): """Values of the sensitivities. Returns a dict of param_name: np_array""" + if isinstance(self._sensitivities, bool) and self._sensitivities: + self.extract_explicit_sensitivities() return self._sensitivities def set_y(self): diff --git a/tests/integration/test_models/standard_model_tests.py b/tests/integration/test_models/standard_model_tests.py index 839e60f1d0..fa798c94ad 100644 --- a/tests/integration/test_models/standard_model_tests.py +++ b/tests/integration/test_models/standard_model_tests.py @@ -83,7 +83,6 @@ def test_solving(self, solver=None, t_eval=None, inputs=None, self.solution = self.solver.solve( self.model, t_eval, inputs=inputs, - calculate_sensitivities=calculate_sensitivities ) def test_outputs(self): @@ -93,40 +92,45 @@ def test_outputs(self): ) std_out_test.test_all() - def test_sensitivities(self): - param_name = "Negative electrode conductivity [S.m-1]" - neg_electrode_cond = 100.0 + def test_sensitivities(self, param_name, param_value): self.parameter_values.update({param_name: "[input]"}) - inputs = {param_name: neg_electrode_cond} + inputs = {param_name: param_value} self.test_processing_parameters() self.test_processing_disc() - self.test_solving(inputs=inputs, calculate_sensitivities=True) + # Use tighter default tolerances for testing + self.solver.rtol = 1e-8 + self.solver.atol = 1e-8 + + Crate = abs( + self.parameter_values["Current function [A]"] + / self.parameter_values["Nominal cell capacity [A.h]"] + ) + t_eval = np.linspace(0, 3600 / Crate, 100) + + self.solution = self.solver.solve( + self.model, t_eval, inputs=inputs, + calculate_sensitivities=True + ) # check via finite differencing - h = 1e-6 - inputs_plus = {param_name: neg_electrode_cond + 0.5 * h} - inputs_neg = {param_name: neg_electrode_cond - 0.5 * h} + h = 1e-6 * param_value + inputs_plus = {param_name: (param_value + 0.5 * h)} + inputs_neg = {param_name: (param_value - 0.5 * h)} sol_plus = self.solver.solve( - self.model, self.solution.all_ts[0], inputs=inputs_plus + self.model, t_eval, inputs=inputs_plus, ) sol_neg = self.solver.solve( - self.model, self.solution.all_ts[0], inputs=inputs_neg + self.model, t_eval, inputs=inputs_neg ) - n = self.solution.sensitivities[param_name].shape[0] - np.testing.assert_array_almost_equal( - self.solution.sensitivities[param_name], - ((sol_plus.y - sol_neg.y) / h).reshape((n, 1)) + fd = ((np.array(sol_plus.y) - np.array(sol_neg.y)) / h) + fd = fd.transpose().reshape(-1, 1) + np.testing.assert_allclose( + self.solution.sensitivities[param_name], fd, + rtol=1e-1, atol=1e-5, ) - if ( - isinstance( - self.model, (pybamm.lithium_ion.BaseModel, pybamm.lead_acid.BaseModel) - ) - ): - self.test_outputs() - def test_all( self, param=None, disc=None, solver=None, t_eval=None, skip_output_tests=False ): diff --git a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_dfn.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_dfn.py index 48eca49167..732273f27d 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_dfn.py +++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_dfn.py @@ -31,7 +31,10 @@ def test_sensitivities(self): modeltest = tests.StandardModelTest( model, parameter_values=param, var_pts=var_pts ) - modeltest.test_sensitivities() + modeltest.test_sensitivities( + #'Separator thickness [m]', 2e-05, + 'Typical current [A]', 0.15652, + ) def test_basic_processing_1plus1D(self): options = {"current collector": "potential pair", "dimensionality": 1} diff --git a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spm.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spm.py index 81bbe466d4..0bd8d1f5e3 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spm.py +++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spm.py @@ -20,10 +20,18 @@ def test_basic_processing(self): def test_sensitivities(self): options = {"thermal": "isothermal"} model = pybamm.lithium_ion.SPM(options) - # use Ecker parameters for nonlinear diffusion param = pybamm.ParameterValues(chemistry=pybamm.parameter_sets.Ecker2015) modeltest = tests.StandardModelTest(model, parameter_values=param) - modeltest.test_sensitivities() + modeltest.test_sensitivities( + #"Negative electrode conductivity [S.m-1]", 14.0 + 'Typical current [A]', 0.15652, + #"Typical electrolyte concentration [mol.m-3]", 1000.0 + #''Negative electrode diffusivity [m2.s-1]', 1e-3, + #'Negative electrode active material volume fraction', 0.372403, + #'Separator thickness [m]', 2e-05, + #'Negative electrode electrons in reaction', 1.0, + #'Outer SEI open-circuit potential [V]', 0.8, + ) def test_basic_processing_1plus1D(self): options = {"current collector": "potential pair", "dimensionality": 1} diff --git a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spme.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spme.py index 42db8d9c79..c5e651a00e 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spme.py +++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spme.py @@ -24,7 +24,10 @@ def test_sensitivities(self): # use Ecker parameters for nonlinear diffusion param = pybamm.ParameterValues(chemistry=pybamm.parameter_sets.Ecker2015) modeltest = tests.StandardModelTest(model, parameter_values=param) - modeltest.test_sensitivities() + modeltest.test_sensitivities( + #'Separator thickness [m]', 2e-05, + 'Typical current [A]', 0.15652, + ) def test_basic_processing_python(self): options = {"thermal": "isothermal"} From d18cf81460540a2cead75c60d4299d8e2ef50cb9 Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Tue, 3 Aug 2021 17:54:41 +0100 Subject: [PATCH 58/73] #1477 fix idaklu unit test --- pybamm/solvers/solution.py | 7 +++++-- .../test_full_battery_models/test_lithium_ion/test_spm.py | 7 ------- tests/unit/test_solvers/test_idaklu_solver.py | 1 + 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/pybamm/solvers/solution.py b/pybamm/solvers/solution.py index 40ef2fc635..6cba2dd8c6 100644 --- a/pybamm/solvers/solution.py +++ b/pybamm/solvers/solution.py @@ -261,8 +261,11 @@ def y(self): @property def sensitivities(self): """Values of the sensitivities. Returns a dict of param_name: np_array""" - if isinstance(self._sensitivities, bool) and self._sensitivities: - self.extract_explicit_sensitivities() + if isinstance(self._sensitivities, bool): + if self._sensitivities: + self.extract_explicit_sensitivities() + else: + self._sensitivities = {} return self._sensitivities def set_y(self): diff --git a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spm.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spm.py index 0bd8d1f5e3..14f0d6963a 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spm.py +++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spm.py @@ -23,14 +23,7 @@ def test_sensitivities(self): param = pybamm.ParameterValues(chemistry=pybamm.parameter_sets.Ecker2015) modeltest = tests.StandardModelTest(model, parameter_values=param) modeltest.test_sensitivities( - #"Negative electrode conductivity [S.m-1]", 14.0 'Typical current [A]', 0.15652, - #"Typical electrolyte concentration [mol.m-3]", 1000.0 - #''Negative electrode diffusivity [m2.s-1]', 1e-3, - #'Negative electrode active material volume fraction', 0.372403, - #'Separator thickness [m]', 2e-05, - #'Negative electrode electrons in reaction', 1.0, - #'Outer SEI open-circuit potential [V]', 0.8, ) def test_basic_processing_1plus1D(self): diff --git a/tests/unit/test_solvers/test_idaklu_solver.py b/tests/unit/test_solvers/test_idaklu_solver.py index 8e31fed297..c80d26650c 100644 --- a/tests/unit/test_solvers/test_idaklu_solver.py +++ b/tests/unit/test_solvers/test_idaklu_solver.py @@ -110,6 +110,7 @@ def test_ida_roberts_klu_sensitivities(self): sol_plus = solver.solve(model, t_eval, inputs={"a": a_value + 0.5 * h}) sol_neg = solver.solve(model, t_eval, inputs={"a": a_value - 0.5 * h}) dyda_fd = (sol_plus.y - sol_neg.y) / h + dyda_fd = dyda_fd.transpose().reshape(-1, 1) np.testing.assert_array_almost_equal( dyda_ida, dyda_fd From 313c9c39b63c7f2dfd29106b2d4aa5e2f10a8f0a Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Tue, 3 Aug 2021 21:51:08 +0100 Subject: [PATCH 59/73] #1477 fix bug in solution --- pybamm/solvers/solution.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/pybamm/solvers/solution.py b/pybamm/solvers/solution.py index 6cba2dd8c6..8c6b794be5 100644 --- a/pybamm/solvers/solution.py +++ b/pybamm/solvers/solution.py @@ -135,11 +135,25 @@ def __init__( pybamm.citations.register("Andersson2019") def extract_explicit_sensitivities(self): + # if we got here, we havn't set y yet + self.set_y() + + # extract sensitivities from full y solution self._y, self._sensitivities = \ self._extract_explicit_sensitivities( self.all_models[0], self.y, self.t, self.all_inputs[0] ) + # make sure we remove all sensitivities from all_ys + for index, (model, ys, ts, inputs) in enumerate( + zip(self.all_models, self.all_ys, self.all_ts, + self.all_inputs) + ): + self._all_ys[index], _ = \ + self._extract_explicit_sensitivities( + model, ys, ts, inputs + ) + def _extract_explicit_sensitivities(self, model, y, t_eval, inputs): """ given a model and a solution y, extracts the sensitivities @@ -411,6 +425,10 @@ def set_summary_variables(self, all_summary_variables): def update(self, variables): """Add ProcessedVariables to the dictionary of variables in the solution""" + # make sure that sensitivities are extracted if required + if isinstance(self._sensitivities, bool) and self._sensitivities: + self.extract_explicit_sensitivities() + # Convert single entry to list if isinstance(variables, str): variables = [variables] From bf59b7d1941ec0677b54f9408e73566c0575fde3 Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Wed, 4 Aug 2021 07:56:54 +0100 Subject: [PATCH 60/73] #1477 fix integration idaklu test --- tests/integration/test_solvers/test_idaklu.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/tests/integration/test_solvers/test_idaklu.py b/tests/integration/test_solvers/test_idaklu.py index 7b128dbee1..805c6ffbe1 100644 --- a/tests/integration/test_solvers/test_idaklu.py +++ b/tests/integration/test_solvers/test_idaklu.py @@ -20,20 +20,20 @@ def test_on_spme(self): np.testing.assert_array_less(1, solution.t.size) def test_on_spme_sensitivities(self): - param_name = "Negative electrode conductivity [S.m-1]" - neg_electrode_cond = 100.0 + param_name = 'Typical current [A]' + param_value = 0.15652 model = pybamm.lithium_ion.SPMe() geometry = model.default_geometry param = model.default_parameter_values param.update({param_name: "[input]"}) - inputs = {param_name: neg_electrode_cond} + inputs = {param_name: param_value} param.process_model(model) param.process_geometry(geometry) mesh = pybamm.Mesh(geometry, model.default_submesh_types, model.default_var_pts) disc = pybamm.Discretisation(mesh, model.default_spatial_methods) disc.process_model(model) t_eval = np.linspace(0, 3600, 100) - solver = pybamm.IDAKLUSolver() + solver = pybamm.IDAKLUSolver(rtol=1e-10, atol=1e-10) solution = solver.solve( model, t_eval, inputs=inputs, @@ -48,16 +48,18 @@ def test_on_spme_sensitivities(self): h = 1e-6 sol_plus = solver.solve( model, t_eval, - inputs={param_name: neg_electrode_cond + 0.5 * h} + inputs={param_name: param_value + 0.5 * h} ) sol_neg = solver.solve( model, t_eval, - inputs={param_name: neg_electrode_cond - 0.5 * h} + inputs={param_name: param_value - 0.5 * h} ) dyda_fd = (sol_plus.y - sol_neg.y) / h + dyda_fd = dyda_fd.transpose().reshape(-1, 1) - np.testing.assert_array_almost_equal( - dyda_ida, dyda_fd + np.testing.assert_allclose( + dyda_ida, dyda_fd, + rtol=1e-2, atol=1e-3, ) def test_set_tol_by_variable(self): @@ -109,7 +111,6 @@ def test_changing_grid(self): if __name__ == "__main__": print("Add -v for more debug output") - pybamm.set_logging_level('INFO') if "-v" in sys.argv: debug = True pybamm.settings.debug_mode = True From 88ecb3f2e127022485cd3d3573dd2483c7c99c18 Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Wed, 4 Aug 2021 09:12:52 +0100 Subject: [PATCH 61/73] #1477 do sensitivity integration tests using a processed variable --- pybamm/solvers/processed_variable.py | 23 ++++++++++--------- .../test_models/standard_model_tests.py | 12 ++++++---- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/pybamm/solvers/processed_variable.py b/pybamm/solvers/processed_variable.py index f9fca1b938..4c06021b26 100644 --- a/pybamm/solvers/processed_variable.py +++ b/pybamm/solvers/processed_variable.py @@ -537,17 +537,18 @@ def initialise_sensitivity_explicit_forward(self): dvar_dp_func = casadi.Function( "dvar_dp", [t_casadi, y_casadi, p_casadi_stacked], [dvar_dp] ) - for idx in range(len(self.all_ts[0])): - t = self.all_ts[0][idx] - u = self.all_ys[0][:, idx] - next_dvar_dy_eval = dvar_dy_func(t, u, inputs_stacked) - next_dvar_dp_eval = dvar_dp_func(t, u, inputs_stacked) - if idx == 0: - dvar_dy_eval = next_dvar_dy_eval - dvar_dp_eval = next_dvar_dp_eval - else: - dvar_dy_eval = casadi.diagcat(dvar_dy_eval, next_dvar_dy_eval) - dvar_dp_eval = casadi.vertcat(dvar_dp_eval, next_dvar_dp_eval) + for index, (ts, ys) in enumerate(zip(self.all_ts, self.all_ys)): + for idx in range(len(ts)): + t = ts[idx] + u = ys[:, idx] + next_dvar_dy_eval = dvar_dy_func(t, u, inputs_stacked) + next_dvar_dp_eval = dvar_dp_func(t, u, inputs_stacked) + if index == 0 and idx == 0: + dvar_dy_eval = next_dvar_dy_eval + dvar_dp_eval = next_dvar_dp_eval + else: + dvar_dy_eval = casadi.diagcat(dvar_dy_eval, next_dvar_dy_eval) + dvar_dp_eval = casadi.vertcat(dvar_dp_eval, next_dvar_dp_eval) # Compute sensitivity dy_dp = self.solution_sensitivities["all"] diff --git a/tests/integration/test_models/standard_model_tests.py b/tests/integration/test_models/standard_model_tests.py index fa798c94ad..a54bccbbc4 100644 --- a/tests/integration/test_models/standard_model_tests.py +++ b/tests/integration/test_models/standard_model_tests.py @@ -92,7 +92,8 @@ def test_outputs(self): ) std_out_test.test_all() - def test_sensitivities(self, param_name, param_value): + def test_sensitivities(self, param_name, param_value, + output_name='Terminal voltage [V]'): self.parameter_values.update({param_name: "[input]"}) inputs = {param_name: param_value} @@ -113,6 +114,7 @@ def test_sensitivities(self, param_name, param_value): self.model, t_eval, inputs=inputs, calculate_sensitivities=True ) + output_sens = self.solution[output_name].sensitivities[param_name] # check via finite differencing h = 1e-6 * param_value @@ -121,14 +123,16 @@ def test_sensitivities(self, param_name, param_value): sol_plus = self.solver.solve( self.model, t_eval, inputs=inputs_plus, ) + output_plus = sol_plus[output_name](t=t_eval) sol_neg = self.solver.solve( self.model, t_eval, inputs=inputs_neg ) - fd = ((np.array(sol_plus.y) - np.array(sol_neg.y)) / h) + output_neg = sol_neg[output_name](t=t_eval) + fd = ((np.array(output_plus) - np.array(output_neg)) / h) fd = fd.transpose().reshape(-1, 1) np.testing.assert_allclose( - self.solution.sensitivities[param_name], fd, - rtol=1e-1, atol=1e-5, + output_sens, fd, + rtol=1e-2, atol=1e-6, ) def test_all( From 1b3266068b84883d6c03b5a9f6664c64a8dbc345 Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Wed, 4 Aug 2021 09:34:48 +0100 Subject: [PATCH 62/73] #1477 fix codacity --- pybamm/solvers/processed_variable.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pybamm/solvers/processed_variable.py b/pybamm/solvers/processed_variable.py index 4c06021b26..2b930a4716 100644 --- a/pybamm/solvers/processed_variable.py +++ b/pybamm/solvers/processed_variable.py @@ -538,8 +538,7 @@ def initialise_sensitivity_explicit_forward(self): "dvar_dp", [t_casadi, y_casadi, p_casadi_stacked], [dvar_dp] ) for index, (ts, ys) in enumerate(zip(self.all_ts, self.all_ys)): - for idx in range(len(ts)): - t = ts[idx] + for idx, t in enumerate(ts): u = ys[:, idx] next_dvar_dy_eval = dvar_dy_func(t, u, inputs_stacked) next_dvar_dp_eval = dvar_dp_func(t, u, inputs_stacked) From f8bc0914f68bbdd6a95dffefb15b8a020fd6c152 Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Thu, 5 Aug 2021 15:37:06 +0100 Subject: [PATCH 63/73] #1477 improve coverage --- pybamm/solvers/base_solver.py | 38 ++-- pybamm/solvers/casadi_algebraic_solver.py | 5 +- pybamm/solvers/casadi_solver.py | 13 +- pybamm/solvers/solution.py | 5 +- tests/unit/test_solvers/test_casadi_solver.py | 209 ++++-------------- 5 files changed, 77 insertions(+), 193 deletions(-) diff --git a/pybamm/solvers/base_solver.py b/pybamm/solvers/base_solver.py index 25f1014c29..6369aeb89b 100644 --- a/pybamm/solvers/base_solver.py +++ b/pybamm/solvers/base_solver.py @@ -607,17 +607,26 @@ def jacp(*args, **kwargs): ) # if we have changed the equations to include the explicit sensitivity - # equations, then we also need to update the mass matrix + # equations, then we also need to update the mass matrix and bounds if calculate_sensitivities_explicit: - n_inputs = model.len_rhs_sens // model.len_rhs + if model.len_rhs != 0: + n_inputs = model.len_rhs_sens // model.len_rhs + elif model.len_alg != 0: + n_inputs = model.len_alg_sens // model.len_alg + model.bounds = ( + np.repeat(model.bounds[0], n_inputs + 1), + np.repeat(model.bounds[1], n_inputs + 1), + ) if (model.mass_matrix is not None and model.mass_matrix.shape[0] == model.len_rhs_and_alg): - model.mass_matrix_inv = pybamm.Matrix( - block_diag( - [model.mass_matrix_inv.entries] * (n_inputs + 1), - format="csr" + + if model.mass_matrix_inv is not None: + model.mass_matrix_inv = pybamm.Matrix( + block_diag( + [model.mass_matrix_inv.entries] * (n_inputs + 1), + format="csr" + ) ) - ) model.mass_matrix = pybamm.Matrix( block_diag( [model.mass_matrix.entries] * (n_inputs + 1), format="csr" @@ -627,10 +636,11 @@ def jacp(*args, **kwargs): # take care if calculate_sensitivites used then not used if (model.mass_matrix is not None and model.mass_matrix.shape[0] > model.len_rhs_and_alg): - model.mass_matrix_inv = pybamm.Matrix( - model.mass_matrix_inv.entries[:model.len_rhs, - :model.len_rhs] - ) + if model.mass_matrix_inv is not None: + model.mass_matrix_inv = pybamm.Matrix( + model.mass_matrix_inv.entries[:model.len_rhs, + :model.len_rhs] + ) model.mass_matrix = pybamm.Matrix( model.mass_matrix.entries[:model.len_rhs_and_alg, :model.len_rhs_and_alg] @@ -1428,12 +1438,6 @@ def _set_up_ext_and_inputs(self, model, external_variables, inputs, for input_param in model.input_parameters: name = input_param.name if name not in inputs: - # Don't allow symbolic inputs if using `sensitivity` - if calculate_sensitivities: - raise pybamm.SolverError( - "Cannot have symbolic inputs if explicitly solving forward" - "sensitivity equations" - ) # Only allow symbolic inputs for CasadiSolver and CasadiAlgebraicSolver if not isinstance( self, (pybamm.CasadiSolver, pybamm.CasadiAlgebraicSolver) diff --git a/pybamm/solvers/casadi_algebraic_solver.py b/pybamm/solvers/casadi_algebraic_solver.py index 085bc16d9c..881d4f7744 100644 --- a/pybamm/solvers/casadi_algebraic_solver.py +++ b/pybamm/solvers/casadi_algebraic_solver.py @@ -229,8 +229,11 @@ def _integrate(self, model, t_eval, inputs_dict=None): y_sol = casadi.vertcat(y_diff, y_alg) # Return solution object (no events, so pass None to t_event, y_event) + + explicit_sensitivities = bool(model.calculate_sensitivities) sol = pybamm.Solution( - [t_eval], y_sol, model, inputs_dict, termination="success" + [t_eval], y_sol, model, inputs_dict, termination="success", + sensitivities=explicit_sensitivities ) sol.integration_time = integration_time return sol diff --git a/pybamm/solvers/casadi_solver.py b/pybamm/solvers/casadi_solver.py index f6d3a1d0e1..f5bb9cbc5f 100644 --- a/pybamm/solvers/casadi_solver.py +++ b/pybamm/solvers/casadi_solver.py @@ -123,9 +123,6 @@ def _integrate(self, model, t_eval, inputs_dict=None): Any external variables or input parameters to pass to the model when solving """ - # are we solving explicit forward equations? - explicit_sensitivities = bool(model.calculate_sensitivities) - # Record whether there are any symbolic inputs inputs_dict = inputs_dict or {} has_symbolic_inputs = any( @@ -275,10 +272,6 @@ def _integrate(self, model, t_eval, inputs_dict=None): # update y0 y0 = solution.all_ys[-1][:, -1] - # now we extract sensitivities from the solution - if (explicit_sensitivities): - solution.extract_explicit_sensitivities() - return solution def _solve_for_event(self, coarse_solution, init_event_signs): @@ -544,11 +537,7 @@ def create_integrator(self, model, inputs, t_eval=None, use_event_switch=False): # set up and solve t = casadi.MX.sym("t") p = casadi.MX.sym("p", inputs.shape[0]) - # If the initial conditions depend on inputs, evaluate the function - if isinstance(model.y0, casadi.Function): - y0 = model.y0(p) - else: - y0 = model.y0 + y0 = model.y0 y_diff = casadi.MX.sym("y_diff", rhs(0, y0, p).shape[0]) y_alg = casadi.MX.sym("y_alg", algebraic(0, y0, p).shape[0]) diff --git a/pybamm/solvers/solution.py b/pybamm/solvers/solution.py index 8c6b794be5..5e21d2eb39 100644 --- a/pybamm/solvers/solution.py +++ b/pybamm/solvers/solution.py @@ -182,7 +182,10 @@ def _extract_explicit_sensitivities(self, model, y, t_eval, inputs): n_rhs = model.len_rhs n_alg = model.len_alg # Get the point where the algebraic equations start - n_p = model.len_rhs_sens // model.len_rhs + if model.len_rhs != 0: + n_p = model.len_rhs_sens // model.len_rhs + else: + n_p = model.len_alg_sens // model.len_alg len_rhs_and_sens = model.len_rhs + model.len_rhs_sens n_t = len(t_eval) diff --git a/tests/unit/test_solvers/test_casadi_solver.py b/tests/unit/test_solvers/test_casadi_solver.py index 2fd1e4d456..c98d4f2e63 100644 --- a/tests/unit/test_solvers/test_casadi_solver.py +++ b/tests/unit/test_solvers/test_casadi_solver.py @@ -817,6 +817,38 @@ def test_solve_sensitivity_scalar_var_vector_input(self): np.vstack([-2 * t * np.exp(-p_eval * t) * l_n / n for t in t_eval]), ) + def test_solve_sensitivity_then_no_sensitivity(self): + # Create model + model = pybamm.BaseModel() + var = pybamm.Variable("var") + p = pybamm.InputParameter("p") + model.rhs = {var: p * var} + model.initial_conditions = {var: 1} + model.variables = {"var squared": var ** 2} + + # Solve + # Make sure that passing in extra options works + solver = pybamm.CasadiSolver( + mode="fast", rtol=1e-10, atol=1e-10 + ) + t_eval = np.linspace(0, 1, 80) + solution = solver.solve(model, t_eval, inputs={"p": 0.1}, + calculate_sensitivities=True) + + # check sensitivities + np.testing.assert_allclose( + solution.sensitivities["p"], + (solution.t * np.exp(0.1 * solution.t))[:, np.newaxis], + ) + + solution = solver.solve(model, t_eval, inputs={"p": 0.1}) + + np.testing.assert_array_equal(solution.t, t_eval) + np.testing.assert_allclose(solution.y, np.exp(0.1 * solution.t).reshape(1, -1)) + np.testing.assert_allclose( + solution["var squared"].data, np.exp(0.1 * solution.t) ** 2 + ) + class TestCasadiSolverDAEsWithForwardSensitivityEquations(unittest.TestCase): def test_solve_sensitivity_scalar_var_scalar_input(self): @@ -858,180 +890,33 @@ def test_solve_sensitivity_scalar_var_scalar_input(self): atol=1e-7 ) - def test_solve_sensitivity_vector_var_scalar_input(self): - var = pybamm.Variable("var", "negative electrode") - model = pybamm.BaseModel() - # Set length scales to avoid warning - model.length_scales = {"negative electrode": 1} - param = pybamm.InputParameter("param") - model.rhs = {var: -param * var} - model.initial_conditions = {var: 2} - model.variables = {"var": var} - - # create discretisation - disc = get_discretisation_for_testing() - disc.process_model(model) - n = disc.mesh["negative electrode"].npts - - # Solve - scalar input - solver = pybamm.CasadiSolver() - t_eval = np.linspace(0, 1) - solution = solver.solve(model, t_eval, inputs={"param": 7}, - calculate_sensitivities=["param"]) - np.testing.assert_array_almost_equal( - solution["var"].data, np.tile(2 * np.exp(-7 * t_eval), (n, 1)), decimal=4, - ) - np.testing.assert_array_almost_equal( - solution["var"].sensitivities["param"], - np.repeat(-2 * t_eval * np.exp(-7 * t_eval), n)[:, np.newaxis], - decimal=4, - ) - - # More complicated model + def test_solve_sensitivity_algebraic(self): # Create model model = pybamm.BaseModel() - # Set length scales to avoid warning - model.length_scales = {"negative electrode": 1} - var = pybamm.Variable("var", "negative electrode") + var = pybamm.Variable("var") p = pybamm.InputParameter("p") - q = pybamm.InputParameter("q") - r = pybamm.InputParameter("r") - s = pybamm.InputParameter("s") - model.rhs = {var: p * q} - model.initial_conditions = {var: r} - model.variables = {"var times s": var * s} - - # Discretise - disc.process_model(model) + model.algebraic = {var: var - p * pybamm.t} + model.initial_conditions = {var: 0} + model.variables = {"var squared": var ** 2} # Solve # Make sure that passing in extra options works - solver = pybamm.CasadiSolver( - rtol=1e-10, atol=1e-10, - ) + solver = pybamm.CasadiAlgebraicSolver(tol=1e-10) t_eval = np.linspace(0, 1, 80) - solution = solver.solve( - model, t_eval, inputs={"p": 0.1, "q": 2, "r": -1, "s": 0.5}, - calculate_sensitivities=True, - ) - np.testing.assert_allclose(solution.y, np.tile(-1 + 0.2 * solution.t, (n, 1))) - np.testing.assert_allclose( - solution.sensitivities["p"], np.repeat(2 * solution.t, n)[:, np.newaxis], - ) - np.testing.assert_allclose( - solution.sensitivities["q"], np.repeat(0.1 * solution.t, n)[:, np.newaxis], - ) - np.testing.assert_allclose(solution.sensitivities["r"], 1) - np.testing.assert_allclose(solution.sensitivities["s"], 0) - np.testing.assert_allclose( - solution.sensitivities["all"], - np.hstack( - [ - solution.sensitivities["p"], - solution.sensitivities["q"], - solution.sensitivities["r"], - solution.sensitivities["s"], - ] - ), - ) + solution = solver.solve(model, t_eval, inputs={"p": 0.1}, + calculate_sensitivities=True) + np.testing.assert_array_equal(solution.t, t_eval) + np.testing.assert_allclose(solution.y[0], 0.1 * solution.t) np.testing.assert_allclose( - solution["var times s"].data, np.tile(0.5 * (-1 + 0.2 * solution.t), (n, 1)) + solution.sensitivities["p"], solution.t.reshape(-1, 1), atol=1e-7 ) np.testing.assert_allclose( - solution["var times s"].sensitivities["p"], - np.repeat(0.5 * (2 * solution.t), n)[:, np.newaxis], + solution["var squared"].data, (0.1 * solution.t) ** 2 ) np.testing.assert_allclose( - solution["var times s"].sensitivities["q"], - np.repeat(0.5 * (0.1 * solution.t), n)[:, np.newaxis], - ) - np.testing.assert_allclose(solution["var times s"].sensitivities["r"], 0.5) - np.testing.assert_allclose( - solution["var times s"].sensitivities["s"], - np.repeat(-1 + 0.2 * solution.t, n)[:, np.newaxis], - ) - np.testing.assert_allclose( - solution["var times s"].sensitivities["all"], - np.hstack( - [ - solution["var times s"].sensitivities["p"], - solution["var times s"].sensitivities["q"], - solution["var times s"].sensitivities["r"], - solution["var times s"].sensitivities["s"], - ] - ), - ) - - def test_solve_sensitivity_scalar_var_vector_input(self): - var = pybamm.Variable("var", "negative electrode") - model = pybamm.BaseModel() - # Set length scales to avoid warning - model.length_scales = {"negative electrode": 1} - - param = pybamm.InputParameter("param", "negative electrode") - model.rhs = {var: -param * var} - model.initial_conditions = {var: 2} - model.variables = { - "var": var, - "integral of var": pybamm.Integral(var, pybamm.standard_spatial_vars.x_n), - } - - # create discretisation - mesh = get_mesh_for_testing(xpts=5) - spatial_methods = {"macroscale": pybamm.FiniteVolume()} - disc = pybamm.Discretisation(mesh, spatial_methods) - disc.process_model(model) - n = disc.mesh["negative electrode"].npts - - # Solve - constant input - solver = pybamm.CasadiSolver( - mode="fast", rtol=1e-10, atol=1e-10 - ) - t_eval = np.linspace(0, 1) - solution = solver.solve(model, t_eval, inputs={"param": 7 * np.ones(n)}, - calculate_sensitivities=True) - l_n = mesh["negative electrode"].edges[-1] - np.testing.assert_array_almost_equal( - solution["var"].data, np.tile(2 * np.exp(-7 * t_eval), (n, 1)), decimal=4, - ) - - np.testing.assert_array_almost_equal( - solution["var"].sensitivities["param"], - np.vstack([np.eye(n) * -2 * t * np.exp(-7 * t) for t in t_eval]), - ) - np.testing.assert_array_almost_equal( - solution["integral of var"].data, 2 * np.exp(-7 * t_eval) * l_n, decimal=4, - ) - np.testing.assert_array_almost_equal( - solution["integral of var"].sensitivities["param"], - np.tile(-2 * t_eval * np.exp(-7 * t_eval) * l_n / n, (n, 1)).T, - ) - - # Solve - linspace input - p_eval = np.linspace(1, 2, n) - solution = solver.solve(model, t_eval, inputs={"param": p_eval}, - calculate_sensitivities=True) - l_n = mesh["negative electrode"].edges[-1] - np.testing.assert_array_almost_equal( - solution["var"].data, 2 * np.exp(-p_eval[:, np.newaxis] * t_eval), decimal=4 - ) - np.testing.assert_array_almost_equal( - solution["var"].sensitivities["param"], - np.vstack([np.diag(-2 * t * np.exp(-p_eval * t)) for t in t_eval]), - ) - - np.testing.assert_array_almost_equal( - solution["integral of var"].data, - np.sum( - 2 - * np.exp(-p_eval[:, np.newaxis] * t_eval) - * mesh["negative electrode"].d_edges[:, np.newaxis], - axis=0, - ), - ) - np.testing.assert_array_almost_equal( - solution["integral of var"].sensitivities["param"], - np.vstack([-2 * t * np.exp(-p_eval * t) * l_n / n for t in t_eval]), + solution["var squared"].sensitivities["p"], + (2 * 0.1 * solution.t ** 2).reshape(-1, 1), + atol=1e-7 ) From 6ca02be11301f99da7ce01603a46a652bd848f0e Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Thu, 5 Aug 2021 17:15:06 +0100 Subject: [PATCH 64/73] #1477 fix some remaining bugs with algebraic solver bounds --- pybamm/solvers/base_solver.py | 14 ++++++++++---- pybamm/solvers/casadi_algebraic_solver.py | 6 +++++- pybamm/solvers/casadi_solver.py | 4 ++++ pybamm/solvers/solution.py | 14 ++++++++++---- 4 files changed, 29 insertions(+), 9 deletions(-) diff --git a/pybamm/solvers/base_solver.py b/pybamm/solvers/base_solver.py index 6369aeb89b..0d843f190c 100644 --- a/pybamm/solvers/base_solver.py +++ b/pybamm/solvers/base_solver.py @@ -613,10 +613,11 @@ def jacp(*args, **kwargs): n_inputs = model.len_rhs_sens // model.len_rhs elif model.len_alg != 0: n_inputs = model.len_alg_sens // model.len_alg - model.bounds = ( - np.repeat(model.bounds[0], n_inputs + 1), - np.repeat(model.bounds[1], n_inputs + 1), - ) + if model.bounds[0].shape[0] < model.len_alg + model.len_alg_sens: + model.bounds = ( + np.repeat(model.bounds[0], n_inputs + 1), + np.repeat(model.bounds[1], n_inputs + 1), + ) if (model.mass_matrix is not None and model.mass_matrix.shape[0] == model.len_rhs_and_alg): @@ -634,6 +635,11 @@ def jacp(*args, **kwargs): ) else: # take care if calculate_sensitivites used then not used + if model.bounds[0].shape[0] > model.len_alg: + model.bounds = ( + model.bounds[0][:model.len_alg], + model.bounds[1][:model.len_alg], + ) if (model.mass_matrix is not None and model.mass_matrix.shape[0] > model.len_rhs_and_alg): if model.mass_matrix_inv is not None: diff --git a/pybamm/solvers/casadi_algebraic_solver.py b/pybamm/solvers/casadi_algebraic_solver.py index 881d4f7744..3396523753 100644 --- a/pybamm/solvers/casadi_algebraic_solver.py +++ b/pybamm/solvers/casadi_algebraic_solver.py @@ -230,7 +230,11 @@ def _integrate(self, model, t_eval, inputs_dict=None): # Return solution object (no events, so pass None to t_event, y_event) - explicit_sensitivities = bool(model.calculate_sensitivities) + try: + explicit_sensitivities = bool(model.calculate_sensitivities) + except AttributeError: + explicit_sensitivities = False + sol = pybamm.Solution( [t_eval], y_sol, model, inputs_dict, termination="success", sensitivities=explicit_sensitivities diff --git a/pybamm/solvers/casadi_solver.py b/pybamm/solvers/casadi_solver.py index f5bb9cbc5f..8890e05d7a 100644 --- a/pybamm/solvers/casadi_solver.py +++ b/pybamm/solvers/casadi_solver.py @@ -272,6 +272,10 @@ def _integrate(self, model, t_eval, inputs_dict=None): # update y0 y0 = solution.all_ys[-1][:, -1] + # now we extract sensitivities from the solution + if (bool(model.calculate_sensitivities)): + solution.sensitivities = True + return solution def _solve_for_event(self, coarse_solution, init_event_signs): diff --git a/pybamm/solvers/solution.py b/pybamm/solvers/solution.py index 5e21d2eb39..5fd20187c9 100644 --- a/pybamm/solvers/solution.py +++ b/pybamm/solvers/solution.py @@ -81,10 +81,8 @@ def __init__( else: self.all_inputs = all_inputs - # sensitivities must be a dict or bool - if not isinstance(sensitivities, (bool, dict)): - raise TypeError('sensitivities arg needs to be a bool or dict') - self._sensitivities = sensitivities + + self.sensitivities = sensitivities self._t_event = t_event self._y_event = y_event @@ -285,6 +283,14 @@ def sensitivities(self): self._sensitivities = {} return self._sensitivities + @sensitivities.setter + def sensitivities(self, value): + """Updates the sensitivity""" + # sensitivities must be a dict or bool + if not isinstance(value, (bool, dict)): + raise TypeError('sensitivities arg needs to be a bool or dict') + self._sensitivities = value + def set_y(self): try: if isinstance(self.all_ys[0], (casadi.DM, casadi.MX)): From 9291bb936e0b12f4e2443ee90d9271b41e2e820a Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Thu, 5 Aug 2021 17:16:59 +0100 Subject: [PATCH 65/73] #1477 flake8 --- pybamm/solvers/solution.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pybamm/solvers/solution.py b/pybamm/solvers/solution.py index 5fd20187c9..5c0ef66893 100644 --- a/pybamm/solvers/solution.py +++ b/pybamm/solvers/solution.py @@ -81,7 +81,6 @@ def __init__( else: self.all_inputs = all_inputs - self.sensitivities = sensitivities self._t_event = t_event From ea59d9f2882d306c39b4b3b9ee21ba263e2169e6 Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Thu, 5 Aug 2021 18:02:43 +0100 Subject: [PATCH 66/73] #1477 fix bug with bounds --- pybamm/solvers/base_solver.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pybamm/solvers/base_solver.py b/pybamm/solvers/base_solver.py index 0d843f190c..49c372985d 100644 --- a/pybamm/solvers/base_solver.py +++ b/pybamm/solvers/base_solver.py @@ -613,7 +613,7 @@ def jacp(*args, **kwargs): n_inputs = model.len_rhs_sens // model.len_rhs elif model.len_alg != 0: n_inputs = model.len_alg_sens // model.len_alg - if model.bounds[0].shape[0] < model.len_alg + model.len_alg_sens: + if model.bounds[0].shape[0] == model.len_rhs_and_alg: model.bounds = ( np.repeat(model.bounds[0], n_inputs + 1), np.repeat(model.bounds[1], n_inputs + 1), @@ -635,10 +635,10 @@ def jacp(*args, **kwargs): ) else: # take care if calculate_sensitivites used then not used - if model.bounds[0].shape[0] > model.len_alg: + if model.bounds[0].shape[0] > model.len_rhs_and_alg: model.bounds = ( - model.bounds[0][:model.len_alg], - model.bounds[1][:model.len_alg], + model.bounds[0][:model.len_rhs_and_alg], + model.bounds[1][:model.len_rhs_and_alg], ) if (model.mass_matrix is not None and model.mass_matrix.shape[0] > model.len_rhs_and_alg): From af945067b79ca756fbd0bf7f89f16ee36204307f Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Thu, 5 Aug 2021 21:46:35 +0100 Subject: [PATCH 67/73] #1477 increase coverage --- pybamm/solvers/idaklu_solver.py | 4 +- tests/unit/test_solvers/test_idaklu_solver.py | 42 +++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/pybamm/solvers/idaklu_solver.py b/pybamm/solvers/idaklu_solver.py index 9689d80f6b..d18213f5b7 100644 --- a/pybamm/solvers/idaklu_solver.py +++ b/pybamm/solvers/idaklu_solver.py @@ -53,7 +53,7 @@ def __init__( max_steps="deprecated", ): - if idaklu_spec is None: + if idaklu_spec is None: # pragma: no cover raise ImportError("KLU is not installed") super().__init__( @@ -140,7 +140,7 @@ def _check_atol_type(self, atol, size): if atol.size != size: raise pybamm.SolverError( """Absolute tolerances must be either a scalar or a numpy arrray - of the same shape at y0""" + of the same shape as y0 ({})""".format(size) ) return atol diff --git a/tests/unit/test_solvers/test_idaklu_solver.py b/tests/unit/test_solvers/test_idaklu_solver.py index c80d26650c..ca66e0c9c5 100644 --- a/tests/unit/test_solvers/test_idaklu_solver.py +++ b/tests/unit/test_solvers/test_idaklu_solver.py @@ -130,6 +130,31 @@ def test_set_atol(self): variable_tols = {"Porosity times concentration": 1e-3} solver.set_atol_by_variable(variable_tols, model) + model = pybamm.BaseModel() + u = pybamm.Variable("u") + model.rhs = {u: -0.1 * u} + model.initial_conditions = {u: 1} + t_eval = np.linspace(0, 3, 100) + + disc = pybamm.Discretisation() + disc.process_model(model) + + # numpy array atol + atol = np.zeros(1) + solver = pybamm.IDAKLUSolver(root_method="lm", atol=atol) + solver.solve(model, t_eval) + + # list atol + atol = [1] + solver = pybamm.IDAKLUSolver(root_method="lm", atol=atol) + solver.solve(model, t_eval) + + # wrong size (should fail) + atol = [1, 2] + solver = pybamm.IDAKLUSolver(root_method="lm", atol=atol) + with self.assertRaisesRegex(pybamm.SolverError, 'Absolute tolerances'): + solver.solve(model, t_eval) + def test_failures(self): # this test implements a python version of the ida Roberts # example provided in sundials @@ -149,6 +174,23 @@ def test_failures(self): with self.assertRaisesRegex(pybamm.SolverError, "KLU requires the Jacobian"): solver.solve(model, t_eval) + model = pybamm.BaseModel() + u = pybamm.Variable("u") + model.rhs = {u: -0.1 * u} + model.initial_conditions = {u: 1} + + disc = pybamm.Discretisation() + disc.process_model(model) + + solver = pybamm.IDAKLUSolver(root_method="lm") + + # will give solver error + t_eval = np.linspace(0, -3, 100) + with self.assertRaisesRegex( + pybamm.SolverError, 't_eval must increase monotonically' + ): + solver.solve(model, t_eval) + def test_dae_solver_algebraic_model(self): model = pybamm.BaseModel() var = pybamm.Variable("var") From bec169840cd0bbc4b7cd6734fc825f9a4b506531 Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Wed, 18 Aug 2021 11:41:58 +0100 Subject: [PATCH 68/73] #1477 update changelog --- CHANGELOG.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8aa1100821..aa5d5e381c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,7 @@ ## Breaking changes +- Changed sensitivity API. Removed `ProcessedSymbolicVariable`, all sensitivity now handled within the solvers and `ProcessedVariable` () ([#1552](https://github.com/pybamm-team/PyBaMM/pull/1552)) - The `Yang2017` parameter set has been removed as the complete parameter set is not publicly available in the literature ([#1577](https://github.com/pybamm-team/PyBaMM/pull/1577)) - Changed how options are specified for the "loss of active material" and "particle cracking" submodels. "loss of active material" can now be one of "none", "stress-driven", or "reaction-driven", or a 2-tuple for different options in negative and positive electrode. Similarly "particle cracking" (now called "particle mechanics") can now be "none", "swelling only", "swelling and cracking", or a 2-tuple ([#1490](https://github.com/pybamm-team/PyBaMM/pull/1490)) - Changed the variable in the full diffusion model from "Electrolyte concentration" to "Porosity times concentration" ([#1476](https://github.com/pybamm-team/PyBaMM/pull/1476)) @@ -185,8 +186,6 @@ This release adds new operators for more complex models, some basic sensitivity ## Breaking changes -- Changed sensitivity API. Removed `ProcessedSymbolicVariable`, all sensitivity now handled within the solvers and `ProcessedVariable` () -- Renamed `quick_plot_vars` to `output_variables` in `Simulation` to be consistent with `QuickPlot`. Passing `quick_plot_vars` to `Simulation.plot()` has been deprecated and `output_variables` should be passed instead ([#1099](https://github.com/pybamm-team/PyBaMM/pull/1099)) - The "fast diffusion" particle option has been renamed "uniform profile" ([#1130](https://github.com/pybamm-team/PyBaMM/pull/1130)) - The modules containing standard parameters are now classes so they can take options (e.g. `standard_parameters_lithium_ion` is now `LithiumIonParameters`) ([#1120](https://github.com/pybamm-team/PyBaMM/pull/1120)) From 608fa1f443e0f2c3f73262ab231fffaeb17db6f5 Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Wed, 18 Aug 2021 11:45:03 +0100 Subject: [PATCH 69/73] #1477 remove test files --- sens-test-pybamm.py | 63 --------------------------------------------- sens-test.py | 51 ------------------------------------ 2 files changed, 114 deletions(-) delete mode 100644 sens-test-pybamm.py delete mode 100644 sens-test.py diff --git a/sens-test-pybamm.py b/sens-test-pybamm.py deleted file mode 100644 index 9f5f9cc5d3..0000000000 --- a/sens-test-pybamm.py +++ /dev/null @@ -1,63 +0,0 @@ -import pybamm -import casadi - -model = pybamm.lithium_ion.SPMe() -param = model.default_parameter_values -param["Negative electrode porosity"] = 0.3 -param["Separator porosity"] = 0.3 -param["Positive electrode porosity"] = 0.3 -param["Cation transference number"] = pybamm.InputParameter("t") - -solver = pybamm.CasadiSolver(mode="fast") # , sensitivity=True) -sim = pybamm.Simulation(model, parameter_values=param, solver=solver) -sol = sim.solve([0, 3600], inputs={"t": 0.5}) - -# print(sol["X-averaged electrolyte concentration"].data) -var = sol["Terminal voltage [V]"] - -t = casadi.MX.sym("t") -y = casadi.MX.sym("y", sim.built_model.len_rhs) -p = casadi.MX.sym("p") - -rhs = sim.built_model.casadi_rhs(t, y, p) - -jac_x_func = casadi.Function("jac_x", [t, y, p], [casadi.jacobian(rhs, y)]) -jac_p_func = casadi.Function("jac_x", [t, y, p], [casadi.jacobian(rhs, p)]) -for idx in range(len(sol.t)): - t = sol.t[idx] - u = sol.y[:, idx] - inp = 0.5 - next_jac_x_eval = jac_x_func(t, u, inp) - next_jac_p_eval = jac_p_func(t, u, inp) - if idx == 0: - jac_x_eval = next_jac_x_eval - jac_p_eval = next_jac_p_eval - else: - jac_x_eval = casadi.diagcat(jac_x_eval, next_jac_x_eval) - jac_p_eval = casadi.diagcat(jac_p_eval, next_jac_p_eval) - -# Convert variable to casadi format for differentiating -# var_casadi = self.base_variable.to_casadi(t_casadi, y_casadi, inputs=p_casadi) -# dvar_dy = casadi.jacobian(var_casadi, y_casadi) -# dvar_dp = casadi.jacobian(var_casadi, p_casadi_stacked) - -# # Convert to functions and evaluate index-by-index -# dvar_dy_func = casadi.Function( -# "dvar_dy", [t_casadi, y_casadi, p_casadi_stacked], [dvar_dy] -# ) -# dvar_dp_func = casadi.Function( -# "dvar_dp", [t_casadi, y_casadi, p_casadi_stacked], [dvar_dp] -# ) -# for idx in range(len(self.t_sol)): -# t = self.t_sol[idx] -# u = self.u_sol[:, idx] -# inp = inputs_stacked[:, idx] -# next_dvar_dy_eval = dvar_dy_func(t, u, inp) -# next_dvar_dp_eval = dvar_dp_func(t, u, inp) -# if idx == 0: -# dvar_dy_eval = next_dvar_dy_eval -# dvar_dp_eval = next_dvar_dp_eval -# else: -# dvar_dy_eval = casadi.diagcat(dvar_dy_eval, next_dvar_dy_eval) -# dvar_dp_eval = casadi.vertcat(dvar_dp_eval, next_dvar_dp_eval) - diff --git a/sens-test.py b/sens-test.py deleted file mode 100644 index 8c0eba8620..0000000000 --- a/sens-test.py +++ /dev/null @@ -1,51 +0,0 @@ -import casadi -import numpy as np -import matplotlib.pyplot as plt - -t = casadi.MX.sym("t") -x = casadi.MX.sym("x") -ode = -x - -x0 = 1 -t_eval = np.linspace(0, 10, 20) - -sol_exact = np.exp(-t_eval) - -# Casadi -opts = {"grid": t_eval, "output_t0": True} -itg = casadi.integrator("F", "cvodes", {"t": t, "x": x, "ode": ode}, opts) -sol_casadi = itg(x0=x0)["xf"].full().flatten() - -# Forward Euler -ode_fn = casadi.Function("ode", [t, x], [ode]) -sol_fwd = [x0] -x = x0 -for i in range(len(t_eval) - 1): - dt = t_eval[i + 1] - t_eval[i] - step = dt * ode_fn(t_eval[i], x) - x += step - sol_fwd.append(x) - -# Backward Euler -sol_back = [x0] -x = x0 -for i in range(len(t_eval) - 1): - dt = t_eval[i + 1] - t_eval[i] - x = x / (1 + dt) - sol_back.append(x) - -# Crank-Nicolson -sol_CN = [x0] -x = x0 -for i in range(len(t_eval) - 1): - dt = t_eval[i + 1] - t_eval[i] - x = (1 - dt / 2) * x / (1 + dt / 2) - sol_CN.append(x) - -plt.plot(t_eval, sol_exact, "o", label="exact") -plt.plot(t_eval, sol_casadi, label="casadi") -plt.plot(t_eval, sol_fwd, label="fwd") -plt.plot(t_eval, sol_back, label="back") -plt.plot(t_eval, sol_CN, label="CN") -plt.legend() -plt.show() From 1daa4ba1c5a5dd2871a049ca8e1b131f8f7da24f Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Wed, 18 Aug 2021 11:45:41 +0100 Subject: [PATCH 70/73] #1477 remove sensitivities notebook --- test-sensitivities.ipynb | 604 --------------------------------------- 1 file changed, 604 deletions(-) delete mode 100644 test-sensitivities.ipynb diff --git a/test-sensitivities.ipynb b/test-sensitivities.ipynb deleted file mode 100644 index 1e9709c26f..0000000000 --- a/test-sensitivities.ipynb +++ /dev/null @@ -1,604 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "import pybamm\n", - "import casadi\n", - "import numpy as np\n", - "from scipy.sparse import eye, linalg, csr_matrix" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "model = pybamm.lithium_ion.DFN()\n", - "param = model.default_parameter_values\n", - "param[\"Negative electrode porosity\"] = 0.3\n", - "param[\"Separator porosity\"] = 0.3\n", - "param[\"Positive electrode porosity\"] = 0.3\n", - "param[\"Cation transference number\"] = pybamm.InputParameter(\"t\")\n", - "\n", - "# param = pybamm.ParameterValues({})\n", - "# model = pybamm.BaseModel()\n", - "# v = pybamm.Variable(\"v\")\n", - "# t = pybamm.InputParameter(\"t\")\n", - "# model.rhs = {v: t}\n", - "# model.initial_conditions = {v: 1}\n", - "# model.variables = {\"Terminal voltage [V]\": v}\n", - "\n", - "solver = pybamm.CasadiSolver(mode=\"fast\") # , sensitivity=True)\n", - "sim = pybamm.Simulation(model, parameter_values=param, solver=solver)\n", - "t_eval = np.linspace(0,3600,50)\n", - "sol = sim.solve(t_eval, inputs={\"t\": 0.5})\n", - "\n", - "# print(sol[\"X-averaged electrolyte concentration\"].data)\n", - "var = sol[\"Terminal voltage [V]\"]\n", - "\n", - "t = casadi.MX.sym(\"t\")\n", - "y = casadi.MX.sym(\"y\", sim.built_model.len_rhs_and_alg)\n", - "p = casadi.MX.sym(\"p\")\n", - "\n", - "rhs = casadi.vertcat(\n", - " sim.built_model.casadi_rhs(t, y, p),\n", - " sim.built_model.casadi_algebraic(t, y, p),\n", - ")\n", - "\n", - "jac_x_func = casadi.Function(\"jac_x\", [t, y, p], [casadi.jacobian(rhs, y)])\n", - "jac_p_func = casadi.Function(\"jac_x\", [t, y, p], [casadi.jacobian(rhs, p)])" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "# sim_with_sens = pybamm.Simulation(model, parameter_values=param, \n", - "# # solver=pybamm.CasadiSolver(mode=\"fast\", sensitivity=True)\n", - "# solver=pybamm.CasadiSolver(mode=\"fast\", sensitivity=True)\n", - "# )\n", - "# sol_with_sens = sim_with_sens.solve(\n", - "# np.linspace(0,3600,50), \n", - "# inputs={\"t\": 0.5}, \n", - "# )" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "0.10660402900000054" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "sol.solve_time" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [], - "source": [ - "# sol_with_sens.solve_time" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [], - "source": [ - "inp = 0.5\n", - "x0 = sim.built_model.init_eval(p)\n", - "S_0 = casadi.Function(\"S_0\", [p], [casadi.jacobian(x0,p)])(inp)" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "CPU times: user 170 ms, sys: 21.4 ms, total: 191 ms\n", - "Wall time: 190 ms\n" - ] - } - ], - "source": [ - "%%time\n", - "n = sim.built_model.len_rhs_and_alg\n", - "for idx in range(len(sol.t)):\n", - " ti = sol.t[idx]\n", - " ui = sol.y[:, idx]\n", - " next_jac_x_eval = jac_x_func(ti, ui, inp)\n", - " next_jac_p_eval = jac_p_func(ti, ui, inp)\n", - " if idx == 0:\n", - " jac_x_eval = next_jac_x_eval\n", - " jac_p_eval = next_jac_p_eval\n", - " else:\n", - " jac_x_eval = casadi.diagcat(jac_x_eval, next_jac_x_eval)\n", - " jac_p_eval = casadi.vertcat(jac_p_eval, next_jac_p_eval)" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "(68050, 68050)" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "jac_x_eval.shape" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "(68050, 1)" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "jac_p_eval.shape" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "CPU times: user 301 µs, sys: 6 µs, total: 307 µs\n", - "Wall time: 312 µs\n" - ] - }, - { - "data": { - "text/plain": [ - "DM(sparse: 1361-by-1361, 4868 nnz\n", - " (1, 1) -> -31728.4\n", - " (2, 1) -> 3525.38\n", - " (1, 2) -> 31728.4\n", - " ...\n", - " (1300, 1360) -> -1.21814\n", - " (1359, 1360) -> -1670.37\n", - " (1360, 1360) -> 1671.59)" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "%%time\n", - "i=0\n", - "jac_x_eval[n*(i+1):n*(i+2),n*(i+1):n*(i+2)]" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "CPU times: user 42.1 s, sys: 6.11 s, total: 48.2 s\n", - "Wall time: 6.39 s\n" - ] - } - ], - "source": [ - "%%time\n", - "# Solve for sensitivities symbolically\n", - "# Forward Euler\n", - "# Sx_all = Sx_0.full()\n", - "# S_x = Sx_0\n", - "# n = sim.built_model.len_rhs\n", - "# for i in range(len(sol.t)-1):\n", - "# dt = sol.t[i+1] - sol.t[i]\n", - "# S_x = dt * S_x + jac_x_eval[n*i:n*(i+1),n*i:n*(i+1)] @ S_x + jac_p_eval[n*i:n*(i+1)]\n", - "# Sx_all = np.hstack([Sx_all, S_x.full()])\n", - "# Backward Euler\n", - "# Sx_all = Sx_0.full()\n", - "# S_x = Sx_0\n", - "# n = sim.built_model.len_rhs\n", - "# for i in range(len(sol.t)-1):\n", - "# dt = sol.t[i+1] - sol.t[i]\n", - "# A = np.eye(n) - dt * jac_x_eval[n*(i+1):n*(i+2),n*(i+1):n*(i+2)]\n", - "# b = dt * jac_p_eval[n*(i+1):n*(i+2)] + S_x\n", - "# S_x = np.linalg.solve(A,b)\n", - "# Sx_all = np.hstack([Sx_all, S_x])\n", - "# Crank-Nicolson\n", - "Sx_all = S_0.full()\n", - "S_x = S_0\n", - "\n", - "timer = pybamm.Timer()\n", - "I = casadi.DM.eye(n)\n", - "I2 = np.eye(n)\n", - "# jxf = jac_x_eval.full()\n", - "for i in range(len(sol.t)-1):\n", - "# print(1, timer.time())\n", - "# timer.reset()\n", - " dt = sol.t[i+1] - sol.t[i]\n", - "# print(2, timer.time())\n", - "# timer.reset()\n", - " A = (\n", - "# I2 - dt / 2 * jac_x_eval[n*(i+1):n*(i+2),n*(i+1):n*(i+2)].full()\n", - " I - dt / 2 * jac_x_eval[n*(i+1):n*(i+2),n*(i+1):n*(i+2)]\n", - " ).full()\n", - "# print(3, timer.time())\n", - "# timer.reset()\n", - " b = (\n", - " dt / 2 * (jac_p_eval[n*i:n*(i+1)] + jac_p_eval[n*(i+1):n*(i+2)])\n", - " + (I + dt / 2 * jac_x_eval[n*i:n*(i+1),n*i:n*(i+1)]) @ S_x\n", - " ).full()\n", - "# print(4, timer.time())\n", - "# timer.reset()\n", - " S_x = np.linalg.solve(A,b)\n", - "# print(5, timer.time())\n", - "# timer.reset()\n", - " Sx_all = np.hstack([Sx_all, S_x])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Solve with casadi integrator" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "metadata": {}, - "outputs": [], - "source": [ - "S_x = casadi.SX.sym(\"S_x\", n)\n", - "ode = jac_" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "DM(sparse: 1361-by-1361, 4868 nnz\n", - " (1, 1) -> -31728.4\n", - " (2, 1) -> 3525.38\n", - " (1, 2) -> 31728.4\n", - " ...\n", - " (1300, 1360) -> -1.30835\n", - " (1359, 1360) -> -1643.65\n", - " (1360, 1360) -> 1644.96)" - ] - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "next_jac_x_eval" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "scrolled": true - }, - "outputs": [], - "source": [ - "%%time\n", - "np.linalg.solve(A,b)\n", - "b.shape" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "jac_x_eval[n*(i+1):n*(i+2),n*(i+1):n*(i+2)]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "scrolled": false - }, - "outputs": [], - "source": [ - "Sx_all[:,1][61:] / (sol_with_sens.sensitivity[\"t\"][121:242])[61:].T" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "Sx_all[:,1][61:] - (sol_with_sens.sensitivity[\"t\"][121:242])[61:].T" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "%%time\n", - "# Convert variable to casadi format for differentiating\n", - "var_casadi = var.base_variable.to_casadi(t, y, inputs={\"t\": p})\n", - "dvar_dy = casadi.jacobian(var_casadi, y)\n", - "dvar_dp = casadi.jacobian(var_casadi, p)\n", - "\n", - "# Convert to functions and evaluate index-by-index\n", - "dvar_dy_func = casadi.Function(\n", - " \"dvar_dy\", [t, y, p], [dvar_dy]\n", - ")\n", - "dvar_dp_func = casadi.Function(\n", - " \"dvar_dp\", [t, y, p], [dvar_dp]\n", - ")\n", - "for idx in range(len(var.t_sol)):\n", - " ti = var.t_sol[idx]\n", - " ui = var.u_sol[:, idx]\n", - " next_dvar_dy_eval = dvar_dy_func(ti, ui, inp)\n", - " next_dvar_dp_eval = dvar_dp_func(ti, ui, inp)\n", - " if idx == 0:\n", - " dvar_dy_eval = next_dvar_dy_eval\n", - " dvar_dp_eval = next_dvar_dp_eval\n", - " else:\n", - " dvar_dy_eval = casadi.vertcat(dvar_dy_eval, next_dvar_dy_eval)\n", - " dvar_dp_eval = casadi.vertcat(dvar_dp_eval, next_dvar_dp_eval)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "S_var = dvar_dy_eval @ Sx_all + dvar_dp_eval" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "dvar_dy_eval.shape" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "sol_with_sens[\"Terminal voltage [V]\"].sensitivity[\"all\"].shape" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "np.diag(S_var - sol_with_sens[\"Terminal voltage [V]\"].sensitivity[\"all\"])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Finite difference for comparison" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "CPU times: user 227 ms, sys: 6.25 ms, total: 233 ms\n", - "Wall time: 231 ms\n" - ] - } - ], - "source": [ - "%%time\n", - "h = 1e-8\n", - "sol_fd = (\n", - " sim.solve(t_eval, inputs={\"t\": 0.5+h})[\"Terminal voltage [V]\"].data\n", - " - sim.solve(t_eval, inputs={\"t\": 0.5})[\"Terminal voltage [V]\"].data\n", - ") / h" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "sol_fd- sol_with_sens[\"Terminal voltage [V]\"].sensitivity[\"all\"]" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "CPU times: user 122 ms, sys: 6.74 ms, total: 129 ms\n", - "Wall time: 127 ms\n" - ] - }, - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 16, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "%%time\n", - "sim.solve(t_eval, inputs={\"t\": 0.5})" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "metadata": {}, - "outputs": [], - "source": [ - "from scipy.interpolate import interp1d" - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "CPU times: user 165 ms, sys: 99.4 ms, total: 264 ms\n", - "Wall time: 263 ms\n" - ] - } - ], - "source": [ - "%%time\n", - "n = sim.built_model.len_rhs_and_alg\n", - "for idx in range(len(sol.t)):\n", - " ti = sol.t[idx]\n", - " ui = sol.y[:, idx]\n", - " next_jac_x_eval = jac_x_func(ti, ui, inp)\n", - " next_jac_p_eval = jac_p_func(ti, ui, inp)\n", - " if idx == 0:\n", - " jac_x_eval = next_jac_x_eval\n", - " jac_p_eval = next_jac_p_eval\n", - " else:\n", - " jac_x_eval = casadi.diagcat(jac_x_eval, next_jac_x_eval)\n", - " jac_p_eval = casadi.vertcat(jac_p_eval, next_jac_p_eval)" - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "DM([3.00095, -3.09973, 0.730214, 0.427567, 2.73288])" - ] - }, - "execution_count": 22, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "A = casadi.DM(np.random.rand(5,5))\n", - "b = casadi.DM([1,2,3,4,5])\n", - "casadi.solve(A,b)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.7" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} From df0ff950d85cc388b5d3de1715bf3d85102c3f51 Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Wed, 18 Aug 2021 12:24:04 +0100 Subject: [PATCH 71/73] #1477 swap to using current function for sens tests --- .../test_models/standard_model_tests.py | 15 +++++++++------ .../test_lithium_ion/test_dfn.py | 3 +-- .../test_lithium_ion/test_spm.py | 2 +- .../test_lithium_ion/test_spme.py | 3 +-- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/tests/integration/test_models/standard_model_tests.py b/tests/integration/test_models/standard_model_tests.py index a54bccbbc4..5fa5fa2fa1 100644 --- a/tests/integration/test_models/standard_model_tests.py +++ b/tests/integration/test_models/standard_model_tests.py @@ -94,6 +94,15 @@ def test_outputs(self): def test_sensitivities(self, param_name, param_value, output_name='Terminal voltage [V]'): + + self.parameter_values.update({param_name: param_value}) + Crate = abs( + self.parameter_values["Current function [A]"] + / self.parameter_values["Nominal cell capacity [A.h]"] + ) + t_eval = np.linspace(0, 3600 / Crate, 100) + + # make param_name an input self.parameter_values.update({param_name: "[input]"}) inputs = {param_name: param_value} @@ -104,12 +113,6 @@ def test_sensitivities(self, param_name, param_value, self.solver.rtol = 1e-8 self.solver.atol = 1e-8 - Crate = abs( - self.parameter_values["Current function [A]"] - / self.parameter_values["Nominal cell capacity [A.h]"] - ) - t_eval = np.linspace(0, 3600 / Crate, 100) - self.solution = self.solver.solve( self.model, t_eval, inputs=inputs, calculate_sensitivities=True diff --git a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_dfn.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_dfn.py index 00531123b8..9f52b6a7d0 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_dfn.py +++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_dfn.py @@ -32,8 +32,7 @@ def test_sensitivities(self): model, parameter_values=param, var_pts=var_pts ) modeltest.test_sensitivities( - #'Separator thickness [m]', 2e-05, - 'Typical current [A]', 0.15652, + 'Current function [A]', 0.15652, ) def test_basic_processing_1plus1D(self): diff --git a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spm.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spm.py index 92bb581303..7fc44d4e94 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spm.py +++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spm.py @@ -23,7 +23,7 @@ def test_sensitivities(self): param = pybamm.ParameterValues(chemistry=pybamm.parameter_sets.Ecker2015) modeltest = tests.StandardModelTest(model, parameter_values=param) modeltest.test_sensitivities( - 'Typical current [A]', 0.15652, + 'Current function [A]', 0.15652, ) def test_basic_processing_1plus1D(self): diff --git a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spme.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spme.py index 2e55569b1e..62cec28bcd 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spme.py +++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spme.py @@ -25,8 +25,7 @@ def test_sensitivities(self): param = pybamm.ParameterValues(chemistry=pybamm.parameter_sets.Ecker2015) modeltest = tests.StandardModelTest(model, parameter_values=param) modeltest.test_sensitivities( - #'Separator thickness [m]', 2e-05, - 'Typical current [A]', 0.15652, + 'Current function [A]', 0.15652, ) def test_basic_processing_python(self): From 2cb99ad0099e05c638e26ebf74cf66b8e3d9cae0 Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Wed, 18 Aug 2021 12:48:43 +0100 Subject: [PATCH 72/73] #1477 fix bug in jax evaluate --- pybamm/expression_tree/operations/evaluate_python.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pybamm/expression_tree/operations/evaluate_python.py b/pybamm/expression_tree/operations/evaluate_python.py index fd0ec805c9..ff8bf92853 100644 --- a/pybamm/expression_tree/operations/evaluate_python.py +++ b/pybamm/expression_tree/operations/evaluate_python.py @@ -685,7 +685,10 @@ def evaluate(self, t=None, y=None, y_dot=None, inputs=None, known_evals=None): result = self._jac_evaluate(*self._constants, t, y, y_dot, inputs, known_evals) result = result.reshape(result.shape[0], -1) - return result + if known_evals is not None: + return result, known_evals + else: + return result class EvaluatorJaxSensitivities: @@ -704,4 +707,7 @@ def evaluate(self, t=None, y=None, y_dot=None, inputs=None, known_evals=None): # execute code result = self._jac_evaluate(*self._constants, t, y, y_dot, inputs, known_evals) - return result + if known_evals is not None: + return result, known_evals + else: + return result From 5857a3a6f89490ccc364a43137aa1960c2cd0556 Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Wed, 18 Aug 2021 12:52:16 +0100 Subject: [PATCH 73/73] #1477 put back algebraic solver sens tests --- .../test_casadi_algebraic_solver.py | 153 ++++++++++++++++++ 1 file changed, 153 insertions(+) diff --git a/tests/unit/test_solvers/test_casadi_algebraic_solver.py b/tests/unit/test_solvers/test_casadi_algebraic_solver.py index 7f3f4aefa3..f7af342562 100644 --- a/tests/unit/test_solvers/test_casadi_algebraic_solver.py +++ b/tests/unit/test_solvers/test_casadi_algebraic_solver.py @@ -5,6 +5,8 @@ import pybamm import unittest import numpy as np +from scipy.optimize import least_squares +import tests class TestCasadiAlgebraicSolver(unittest.TestCase): @@ -172,6 +174,157 @@ def test_solve_with_input(self): np.testing.assert_array_equal(solution.y, -7) +class TestCasadiAlgebraicSolverSensitivity(unittest.TestCase): + def test_solve_with_symbolic_input(self): + # Simple system: a single algebraic equation + var = pybamm.Variable("var") + model = pybamm.BaseModel() + model.algebraic = {var: var + pybamm.InputParameter("param")} + model.initial_conditions = {var: 2} + model.variables = {"var": var} + + # create discretisation + disc = pybamm.Discretisation() + disc.process_model(model) + + # Solve + solver = pybamm.CasadiAlgebraicSolver() + solution = solver.solve(model, [0]) + np.testing.assert_array_equal(solution["var"].value({"param": 7}), -7) + np.testing.assert_array_equal(solution["var"].value({"param": 3}), -3) + np.testing.assert_array_equal(solution["var"].sensitivity({"param": 3}), -1) + + def test_least_squares_fit(self): + # Simple system: a single algebraic equation + var = pybamm.Variable("var", domain="negative electrode") + model = pybamm.BaseModel() + p = pybamm.InputParameter("p") + q = pybamm.InputParameter("q") + model.algebraic = {var: (var - p)} + model.initial_conditions = {var: 3} + model.variables = {"objective": (var - q) ** 2 + (p - 3) ** 2} + + # create discretisation + disc = tests.get_discretisation_for_testing() + disc.process_model(model) + + # Solve + solver = pybamm.CasadiAlgebraicSolver() + solution = solver.solve(model, [0]) + sol_var = solution["objective"] + + def objective(x): + return sol_var.value({"p": x[0], "q": x[1]}).full().flatten() + + # without jacobian + lsq_sol = least_squares(objective, [2, 2], method="lm") + np.testing.assert_array_almost_equal(lsq_sol.x, [3, 3], decimal=3) + + def jac(x): + return sol_var.sensitivity({"p": x[0], "q": x[1]}) + + # with jacobian + lsq_sol = least_squares(objective, [2, 2], jac=jac, method="lm") + np.testing.assert_array_almost_equal(lsq_sol.x, [3, 3], decimal=3) + + def test_solve_with_symbolic_input_1D_scalar_input(self): + var = pybamm.Variable("var", "negative electrode") + model = pybamm.BaseModel() + param = pybamm.InputParameter("param") + model.algebraic = {var: var + param} + model.initial_conditions = {var: 2} + model.variables = {"var": var} + + # create discretisation + disc = tests.get_discretisation_for_testing() + disc.process_model(model) + + # Solve - scalar input + solver = pybamm.CasadiAlgebraicSolver() + solution = solver.solve(model, [0]) + np.testing.assert_array_equal(solution["var"].value({"param": 7}), -7) + np.testing.assert_array_equal(solution["var"].value({"param": 3}), -3) + np.testing.assert_array_equal(solution["var"].sensitivity({"param": 3}), -1) + + def test_solve_with_symbolic_input_1D_vector_input(self): + var = pybamm.Variable("var", "negative electrode") + model = pybamm.BaseModel() + param = pybamm.InputParameter("param", "negative electrode") + model.algebraic = {var: var + param} + model.initial_conditions = {var: 2} + model.variables = {"var": var} + + # create discretisation + disc = tests.get_discretisation_for_testing() + disc.process_model(model) + + # Solve - scalar input + solver = pybamm.CasadiAlgebraicSolver() + solution = solver.solve(model, [0]) + n = disc.mesh["negative electrode"].npts + + solver = pybamm.CasadiAlgebraicSolver() + solution = solver.solve(model, [0]) + p = np.linspace(0, 1, n)[:, np.newaxis] + np.testing.assert_array_almost_equal( + solution["var"].value({"param": 3 * np.ones(n)}), -3 + ) + np.testing.assert_array_almost_equal( + solution["var"].value({"param": 2 * p}), -2 * p + ) + np.testing.assert_array_almost_equal( + solution["var"].sensitivity({"param": 3 * np.ones(n)}), -np.eye(40) + ) + np.testing.assert_array_almost_equal( + solution["var"].sensitivity({"param": p}), -np.eye(40) + ) + + def test_solve_with_symbolic_input_in_initial_conditions(self): + # Simple system: a single algebraic equation + var = pybamm.Variable("var") + model = pybamm.BaseModel() + model.algebraic = {var: var + 2} + model.initial_conditions = {var: pybamm.InputParameter("param")} + model.variables = {"var": var} + + # create discretisation + disc = pybamm.Discretisation() + disc.process_model(model) + + # Solve + solver = pybamm.CasadiAlgebraicSolver() + solution = solver.solve(model, [0]) + np.testing.assert_array_equal(solution["var"].value({"param": 7}), -2) + np.testing.assert_array_equal(solution["var"].value({"param": 3}), -2) + np.testing.assert_array_equal(solution["var"].sensitivity({"param": 3}), 0) + + def test_least_squares_fit_input_in_initial_conditions(self): + # Simple system: a single algebraic equation + var = pybamm.Variable("var", domain="negative electrode") + model = pybamm.BaseModel() + p = pybamm.InputParameter("p") + q = pybamm.InputParameter("q") + model.algebraic = {var: (var - p)} + model.initial_conditions = {var: p} + model.variables = {"objective": (var - q) ** 2 + (p - 3) ** 2} + + # create discretisation + disc = tests.get_discretisation_for_testing() + disc.process_model(model) + + # Solve + solver = pybamm.CasadiAlgebraicSolver() + solution = solver.solve(model, [0]) + sol_var = solution["objective"] + + def objective(x): + return sol_var.value({"p": x[0], "q": x[1]}).full().flatten() + + # without jacobian + lsq_sol = least_squares(objective, [2, 2], method="lm") + np.testing.assert_array_almost_equal(lsq_sol.x, [3, 3], decimal=3) + + if __name__ == "__main__": print("Add -v for more debug output") import sys