From 3e32d20b8697c4c5dda1431cf0a6c11dd1aa8c90 Mon Sep 17 00:00:00 2001 From: Pip Liggins Date: Fri, 12 Sep 2025 17:42:51 -0700 Subject: [PATCH 01/42] Add test and fix bug --- src/pybamm/solvers/base_solver.py | 8 +++--- tests/unit/test_solvers/test_base_solver.py | 30 +++++++++++++++++++++ 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/pybamm/solvers/base_solver.py b/src/pybamm/solvers/base_solver.py index ce0599b230..9d44e9fd26 100644 --- a/src/pybamm/solvers/base_solver.py +++ b/src/pybamm/solvers/base_solver.py @@ -860,7 +860,7 @@ def solve( for it in model.concatenated_initial_conditions.pre_order() ] ) - if all_inputs_names.issubset(initial_conditions_node_names): + if not initial_conditions_node_names.isdisjoint(all_inputs_names): raise pybamm.SolverError( "Input parameters cannot appear in expression " "for initial conditions." @@ -910,9 +910,9 @@ def solve( # If the new initial conditions are different # and cannot be evaluated directly, set up again self.set_up(model, model_inputs_list[0], t_eval, ics_only=True) - self._model_set_up[model]["initial conditions"] = ( - model.concatenated_initial_conditions - ) + self._model_set_up[model][ + "initial conditions" + ] = model.concatenated_initial_conditions else: # Set the standard initial conditions self._set_initial_conditions(model, t_eval[0], model_inputs_list[0]) diff --git a/tests/unit/test_solvers/test_base_solver.py b/tests/unit/test_solvers/test_base_solver.py index c0bcbf3ce2..18ec1aece1 100644 --- a/tests/unit/test_solvers/test_base_solver.py +++ b/tests/unit/test_solvers/test_base_solver.py @@ -446,3 +446,33 @@ def test_on_extrapolation_and_on_failure_settings(self): ValueError, match="on_failure must be 'warn', 'raise', or 'ignore'" ): base_solver.on_failure = "invalid" + + def test_solver_multiple_inputs_initial_conditions_error(self): + + y = pybamm.Variable("y") + y0 = pybamm.InputParameter("y0") + k = pybamm.InputParameter("k") + + model = pybamm.BaseModel() + model.rhs = {y: -k * y} + model.initial_conditions = {y: y0} + model.variables = {"y": y} + + disc = pybamm.Discretisation() + disc.process_model(model) + + t_eval = np.linspace(0.0, 1.0, 6) + + # Three different ICs so each run is clearly distinct + inputs_list = [ + {"y0": 1.0, "k": 0.5}, + {"y0": 2.0, "k": 1.0}, + {"y0": 3.0, "k": 1.5}, + ] + + solver = pybamm.BaseSolver() + with pytest.raises( + pybamm.SolverError, + match="Input parameters cannot appear in expression for initial conditions", + ): + solver.solve(model, t_eval=t_eval, inputs=inputs_list) From 6f5443816454d7e47ba95f4d581d812d21722128 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 13 Sep 2025 00:48:26 +0000 Subject: [PATCH 02/42] style: pre-commit fixes --- src/pybamm/solvers/base_solver.py | 6 +++--- tests/unit/test_solvers/test_base_solver.py | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/pybamm/solvers/base_solver.py b/src/pybamm/solvers/base_solver.py index 9d44e9fd26..0c1b68818a 100644 --- a/src/pybamm/solvers/base_solver.py +++ b/src/pybamm/solvers/base_solver.py @@ -910,9 +910,9 @@ def solve( # If the new initial conditions are different # and cannot be evaluated directly, set up again self.set_up(model, model_inputs_list[0], t_eval, ics_only=True) - self._model_set_up[model][ - "initial conditions" - ] = model.concatenated_initial_conditions + self._model_set_up[model]["initial conditions"] = ( + model.concatenated_initial_conditions + ) else: # Set the standard initial conditions self._set_initial_conditions(model, t_eval[0], model_inputs_list[0]) diff --git a/tests/unit/test_solvers/test_base_solver.py b/tests/unit/test_solvers/test_base_solver.py index 18ec1aece1..90c84eb986 100644 --- a/tests/unit/test_solvers/test_base_solver.py +++ b/tests/unit/test_solvers/test_base_solver.py @@ -448,7 +448,6 @@ def test_on_extrapolation_and_on_failure_settings(self): base_solver.on_failure = "invalid" def test_solver_multiple_inputs_initial_conditions_error(self): - y = pybamm.Variable("y") y0 = pybamm.InputParameter("y0") k = pybamm.InputParameter("k") From 6c889db049dcae7ce08baf815e6660d31c66a9f6 Mon Sep 17 00:00:00 2001 From: Pip Liggins Date: Wed, 17 Sep 2025 11:19:45 -0700 Subject: [PATCH 03/42] Remove some unused arguments --- src/pybamm/solvers/base_solver.py | 42 ++++++++++++------------------- 1 file changed, 16 insertions(+), 26 deletions(-) diff --git a/src/pybamm/solvers/base_solver.py b/src/pybamm/solvers/base_solver.py index 0c1b68818a..091789e33b 100644 --- a/src/pybamm/solvers/base_solver.py +++ b/src/pybamm/solvers/base_solver.py @@ -166,13 +166,13 @@ def set_up(self, model, inputs=None, t_eval=None, ics_only=False): else: pybamm.logger.info("Start solver set-up") - self._check_and_prepare_model_inplace(model, inputs, ics_only) + self._check_and_prepare_model_inplace(model) # set default calculate sensitivities on model if not hasattr(model, "calculate_sensitivities"): model.calculate_sensitivities = [] - self._set_up_model_sensitivities_inplace(model, inputs) + self._set_up_model_sensitivities_inplace(model) vars_for_processing = self._get_vars_for_processing(model, inputs) @@ -369,7 +369,7 @@ def _wrangle_name(cls, name: str) -> str: name = name.replace(string, replacement) return name - def _check_and_prepare_model_inplace(self, model, inputs, ics_only): + def _check_and_prepare_model_inplace(self, model): """ Performs checks on the model and prepares it for solving. """ @@ -461,7 +461,7 @@ def _get_vars_for_processing(model, inputs): return vars_for_processing @staticmethod - def _set_up_model_sensitivities_inplace(model, inputs): + def _set_up_model_sensitivities_inplace(model): """ Set up model attributes related to sensitivities. """ @@ -826,14 +826,9 @@ def solve( t_interp = self.process_t_interp(t_interp) # Set up inputs - # - # Argument "inputs" can be either a list of input dicts or - # a single dict. The remaining of this function is only working - # with variable "input_list", which is a list of dictionaries. - # If "inputs" is a single dict, "inputs_list" is a list of only one dict. - inputs_list = inputs if isinstance(inputs, list) else [inputs] - model_inputs_list = [ - self._set_up_model_inputs(model, inputs) for inputs in inputs_list + model_inputs_list: list[dict] = [ + self._set_up_model_inputs(model, inputs) + for inputs in (inputs if isinstance(inputs, list) else [inputs]) ] calculate_sensitivities_list, sensitivities_have_changed = ( @@ -848,18 +843,13 @@ def solve( # is passed to `_set_consistent_initialization`. # See https://github.com/pybamm-team/PyBaMM/pull/1261 if len(model_inputs_list) > 1: - all_inputs_names = set( - itertools.chain.from_iterable( - [model_inputs.keys() for model_inputs in model_inputs_list] - ) - ) + all_inputs_names = { + key for model_inputs in model_inputs_list for key in model_inputs + } if all_inputs_names: - initial_conditions_node_names = set( - [ - it.name - for it in model.concatenated_initial_conditions.pre_order() - ] - ) + initial_conditions_node_names = { + it.name for it in model.concatenated_initial_conditions.pre_order() + } if not initial_conditions_node_names.isdisjoint(all_inputs_names): raise pybamm.SolverError( "Input parameters cannot appear in expression " @@ -910,9 +900,9 @@ def solve( # If the new initial conditions are different # and cannot be evaluated directly, set up again self.set_up(model, model_inputs_list[0], t_eval, ics_only=True) - self._model_set_up[model]["initial conditions"] = ( - model.concatenated_initial_conditions - ) + self._model_set_up[model][ + "initial conditions" + ] = model.concatenated_initial_conditions else: # Set the standard initial conditions self._set_initial_conditions(model, t_eval[0], model_inputs_list[0]) From cad2c3f517fc1596fcc35cce288edc97ad4055b5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 13 Oct 2025 13:41:33 +0000 Subject: [PATCH 04/42] style: pre-commit fixes --- src/pybamm/solvers/base_solver.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pybamm/solvers/base_solver.py b/src/pybamm/solvers/base_solver.py index 091789e33b..2d75385326 100644 --- a/src/pybamm/solvers/base_solver.py +++ b/src/pybamm/solvers/base_solver.py @@ -900,9 +900,9 @@ def solve( # If the new initial conditions are different # and cannot be evaluated directly, set up again self.set_up(model, model_inputs_list[0], t_eval, ics_only=True) - self._model_set_up[model][ - "initial conditions" - ] = model.concatenated_initial_conditions + self._model_set_up[model]["initial conditions"] = ( + model.concatenated_initial_conditions + ) else: # Set the standard initial conditions self._set_initial_conditions(model, t_eval[0], model_inputs_list[0]) From 37c28a5cba4d599f667f75500be34a3b837ea91a Mon Sep 17 00:00:00 2001 From: Pip Liggins Date: Thu, 16 Oct 2025 15:29:00 +0100 Subject: [PATCH 05/42] First pass at making model.y0 a list of len(inputs). --- src/pybamm/simulation.py | 2 - src/pybamm/solvers/algebraic_solver.py | 3 +- src/pybamm/solvers/base_solver.py | 437 ++++++++++-------- src/pybamm/solvers/casadi_algebraic_solver.py | 6 +- src/pybamm/solvers/casadi_solver.py | 24 +- src/pybamm/solvers/dummy_solver.py | 2 +- src/pybamm/solvers/idaklu_solver.py | 172 +++---- src/pybamm/solvers/jax_solver.py | 10 +- src/pybamm/solvers/scipy_solver.py | 3 +- src/pybamm/solvers/solution.py | 3 +- .../test_solvers/test_algebraic_solver.py | 17 +- tests/unit/test_solvers/test_base_solver.py | 65 +-- .../test_casadi_algebraic_solver.py | 6 +- tests/unit/test_solvers/test_scipy_solver.py | 13 +- 14 files changed, 386 insertions(+), 377 deletions(-) diff --git a/src/pybamm/simulation.py b/src/pybamm/simulation.py index d2a696005f..15ebdbaafc 100644 --- a/src/pybamm/simulation.py +++ b/src/pybamm/simulation.py @@ -379,7 +379,6 @@ def solve( showprogress=False, inputs=None, t_interp=None, - initial_conditions=None, **kwargs, ): """ @@ -542,7 +541,6 @@ def solve( inputs=inputs, t_interp=t_interp, **kwargs, - initial_conditions=initial_conditions, ) elif self.operating_mode == "with experiment": diff --git a/src/pybamm/solvers/algebraic_solver.py b/src/pybamm/solvers/algebraic_solver.py index 400787f7c6..3daf29bdcc 100644 --- a/src/pybamm/solvers/algebraic_solver.py +++ b/src/pybamm/solvers/algebraic_solver.py @@ -78,7 +78,7 @@ def tol(self): def tol(self, value): self._tol = value - def _integrate(self, model, t_eval, inputs_dict=None, t_interp=None): + def _integrate_single(self, model, t_eval, y0, y0S, inputs_dict, inputs_list=None): """ Calculate the solution of the algebraic equations through root-finding @@ -97,7 +97,6 @@ def _integrate(self, model, t_eval, inputs_dict=None, t_interp=None): else: inputs = inputs_dict - y0 = model.y0 if isinstance(y0, casadi.DM): y0 = y0.full() y0 = y0.flatten() diff --git a/src/pybamm/solvers/base_solver.py b/src/pybamm/solvers/base_solver.py index 2d75385326..a80d14d2fe 100644 --- a/src/pybamm/solvers/base_solver.py +++ b/src/pybamm/solvers/base_solver.py @@ -146,7 +146,13 @@ def copy(self): new_solver._model_set_up = {} return new_solver - def set_up(self, model, inputs=None, t_eval=None, ics_only=False): + def set_up( + self, + model: pybamm.BaseModel, + inputs: dict | list[dict] | None = None, + t_eval=None, + ics_only=False, + ): """Unpack model, perform checks, and calculate jacobian. Parameters @@ -154,12 +160,14 @@ def set_up(self, model, inputs=None, t_eval=None, ics_only=False): model : :class:`pybamm.BaseModel` The model whose solution to calculate. Must have attributes rhs and initial_conditions - inputs : dict, optional + inputs : dict or list of dict, optional Any input parameters to pass to the model when solving t_eval : numeric type, optional The times at which to stop the integration due to a discontinuity in time. """ - inputs = inputs or {} + if isinstance(inputs, dict): + inputs = [inputs] + inputs = inputs or [{}] if ics_only: pybamm.logger.info("Start solver set-up, initial_conditions only") @@ -174,7 +182,7 @@ def set_up(self, model, inputs=None, t_eval=None, ics_only=False): self._set_up_model_sensitivities_inplace(model) - vars_for_processing = self._get_vars_for_processing(model, inputs) + vars_for_processing = self._get_vars_for_processing(model, inputs[0]) # Process initial conditions initial_conditions, _, jacp_ic, _ = process( @@ -275,7 +283,7 @@ def set_up(self, model, inputs=None, t_eval=None, ics_only=False): model.casadi_sensitivities_algebraic = jacp_algebraic if getattr(self.root_method, "algebraic_solver", False): - self.root_method.set_up_root_solver(model, inputs, t_eval) + self.root_method.set_up_root_solver(model, inputs[0], t_eval) # if output_variables specified then convert functions to casadi # expressions for evaluation within the respective solver @@ -315,37 +323,41 @@ def set_up(self, model, inputs=None, t_eval=None, ics_only=False): pybamm.logger.info("Finish solver set-up") - def _set_initial_conditions(self, model, time, inputs): + def _set_initial_conditions(self, model, time, inputs: list[dict]): # model should have been discretised or an error raised in Self._check_and_prepare_model_inplace len_tot = model.len_rhs_and_alg y_zero = np.zeros((len_tot, 1)) casadi_format = model.convert_to_format == "casadi" - if casadi_format: - # stack inputs - inputs_y0_ics = casadi.vertcat(*[x for x in inputs.values()]) - else: - inputs_y0_ics = inputs - - model.y0 = model.initial_conditions_eval(time, y_zero, inputs_y0_ics) - - if model.jacp_initial_conditions_eval is None: - model.y0S = None - return + model.y0_list = [] + model.y0S_list = [] if model.jacp_initial_conditions_eval is not None else None + for ipts in inputs: + if casadi_format: + # stack inputs + inputs_y0_ics = casadi.vertcat(*[x for x in ipts.values()]) + else: + inputs_y0_ics = ipts - if casadi_format: - inputs_jacp_ics = inputs_y0_ics - else: - # we are calculating the derivative wrt the inputs - # so need to make sure we convert int -> float - # This is to satisfy JAX jacfwd function which requires - # float inputs - inputs_jacp_ics = { - key: float(value) if isinstance(value, int) else value - for key, value in inputs.items() - } + model.y0_list.append( + model.initial_conditions_eval(time, y_zero, inputs_y0_ics) + ) - model.y0S = model.jacp_initial_conditions_eval(time, y_zero, inputs_jacp_ics) + if model.jacp_initial_conditions_eval is not None: + if casadi_format: + inputs_jacp_ics = inputs_y0_ics + else: + # we are calculating the derivative wrt the inputs + # so need to make sure we convert int -> float + # This is to satisfy JAX jacfwd function which requires + # float inputs + inputs_jacp_ics = { + key: float(value) if isinstance(value, int) else value + for key, value in ipts.items() + } + + model.y0S_list.append( + model.jacp_initial_conditions_eval(time, y_zero, inputs_jacp_ics) + ) @classmethod def _wrangle_name(cls, name: str) -> str: @@ -420,7 +432,7 @@ def _check_and_prepare_model_inplace(self, model): model.convert_to_format = "casadi" @staticmethod - def _get_vars_for_processing(model, inputs): + def _get_vars_for_processing(model, inputs: dict): vars_for_processing = { "model": model, } @@ -505,8 +517,9 @@ def heaviside_event(symbol, expr): ) ) + # TODO: still needs to be fixed def heaviside_t_discon(symbol, expr): - value = expr.evaluate(0, model.y0.full(), inputs=inputs) + value = expr.evaluate(0, model.y0_list[0].full(), inputs=inputs) append_t_discon(value) if isinstance(symbol, pybamm.EqualHeaviside): @@ -543,7 +556,7 @@ def modulo_event(symbol, expr, num_events): ) def modulo_t_discon(symbol, expr, num_events): - value = expr.evaluate(0, model.y0.full(), inputs=inputs) + value = expr.evaluate(0, model.y0_list[0].full(), inputs=inputs) for i in np.arange(num_events): t = value * (i + 1) # Stop right before t and at t @@ -601,8 +614,11 @@ def modulo_t_discon(symbol, expr, num_events): k = 20 # address numpy 1.25 deprecation warning: array should have # ndim=0 before conversion + # note: assumes that the sign for all batches is the same init_sign = float( - np.sign(event.evaluate(0, model.y0.full(), inputs=inputs)).item() + np.sign( + event.evaluate(0, model.y0_list[0].full(), inputs=inputs) + ).item() ) # We create a sigmoid for each event which will multiply the # rhs. Doing * 2 - 1 ensures that when the event is crossed, @@ -641,7 +657,7 @@ def modulo_t_discon(symbol, expr, num_events): discontinuity_events, ) - def _set_consistent_initialization(self, model, time, inputs_dict): + def _set_consistent_initialization(self, model, time, inputs_list): """ Set initialized states for the model. This is skipped if the solver is an algebraic solver (since this would make the algebraic solver redundant), and if @@ -654,24 +670,21 @@ def _set_consistent_initialization(self, model, time, inputs_dict): The model for which to calculate initial conditions. time : numeric type The time at which to calculate the initial conditions - inputs_dict : dict + inputs_list : list of dict Any input parameters to pass to the model when solving - update_rhs : bool - Whether to update the rhs. True for 'solve', False for 'step'. - """ if self._algebraic_solver or model.len_alg == 0: - # Don't update model.y0 + # Don't update model.y0_list return # Calculate consistent states for the algebraic equations - model.y0 = self.calculate_consistent_state(model, time, inputs_dict) + model.y0_list = self.calculate_consistent_state(model, time, inputs_list) def calculate_consistent_state(self, model, time=0, inputs=None): """ Calculate consistent state for the algebraic equations through - root-finding. model.y0 is used as the initial guess for rootfinding + root-finding. model.y0_list is used as the initial guess for rootfinding Parameters ---------- @@ -679,7 +692,7 @@ def calculate_consistent_state(self, model, time=0, inputs=None): The model for which to calculate initial conditions. time : float The time at which to calculate the initial conditions - inputs: dict, optional + inputs: list of dict, optional Any input parameters to pass to the model when solving Returns @@ -687,22 +700,28 @@ def calculate_consistent_state(self, model, time=0, inputs=None): y0_consistent : array-like, same shape as y0_guess Initial conditions that are consistent with the algebraic equations (roots of the algebraic equations). If self.root_method == None then returns - model.y0. + model.y0_list. """ pybamm.logger.debug("Start calculating consistent states") + if isinstance(inputs, dict): + inputs = [inputs] + inputs = inputs or [{}] + if self.root_method is None: - return model.y0 + return model.y0_list try: - root_sol = self.root_method._integrate(model, np.array([time]), inputs) + root_sols = self.root_method._integrate(model, np.array([time]), inputs) except pybamm.SolverError as e: raise pybamm.SolverError( f"Could not find consistent states: {e.args[0]}" ) from e pybamm.logger.debug("Found consistent states") - self.check_extrapolation(root_sol, model.events) - y0 = root_sol.all_ys[0] - return y0 + y0s = [] + for s in root_sols: + self.check_extrapolation(s, model.events) + y0s.append(s.all_ys[0]) + return y0s def _solve_process_calculate_sensitivities_arg( inputs, model, calculate_sensitivities @@ -731,6 +750,79 @@ def _solve_process_calculate_sensitivities_arg( return calculate_sensitivities_list, sensitivities_have_changed + def _integrate(self, model, t_eval, inputs_list=None, t_interp=None, nproc=1): + """ + Solve a DAE model defined by residuals with initial conditions y0. + + Parameters + ---------- + model : :class:`pybamm.BaseModel` + The model whose solution to calculate. + t_eval : numeric type + The times at which to compute the solution + inputs_list : list of dict + Any input parameters to pass to the model when solving + """ + + if isinstance(inputs_list, dict): + inputs_list = [inputs_list] + inputs_list = inputs_list or [{}] + + y0S_list = ( + model.y0S_list + if model.y0S_list is not None + else ([None] * len(inputs_list)) + ) + + ninputs = len(inputs_list) + if ninputs == 1: + new_solution = self._integrate_single( + model, + t_eval, + model.y0_list[0], + y0S_list[0], + inputs_list[0], + inputs_list=inputs_list, + ) + new_solutions = [new_solution] + else: + with mp.get_context(self._mp_context).Pool(processes=nproc) as p: + model_list = [model] * len(inputs_list) + t_eval_list = [t_eval] * len(inputs_list) + y0_list = model.y0_list + new_solutions = p.starmap( + self._integrate_single, + zip( + model_list, + t_eval_list, + y0_list, + y0S_list, + inputs_list, + strict=False, + ), + ) + p.close() + p.join() + + return new_solutions + + def _integrate_single(self, model, t_eval, y0, y0S, inputs_dict, inputs_list=None): + """ + Solve a single batch for the DAE model defined by residuals with initial conditions y0. + + Parameters + ---------- + model : :class:`pybamm.BaseModel` + The model whose solution to calculate. + t_eval : numeric type + The times at which to compute the solution + inputs_list : list of dict, optional + Any input parameters to pass to the model when solving + inputs : array, optional + The input parameters in array form, to pass to the model when solving + """ + raise NotImplementedError + def solve( self, model, @@ -739,7 +831,6 @@ def solve( nproc=None, calculate_sensitivities=False, t_interp=None, - initial_conditions=None, ): """ Execute the solver setup and calculate the solution of the model at @@ -771,7 +862,7 @@ def solve( The times (in seconds) at which to interpolate the solution. Defaults to None. Only valid for solvers that support intra-solve interpolation (`IDAKLUSolver`). initial_conditions : dict, numpy.ndarray, or list, optional - Override the model’s default `y0`. Can be: + Override the model's default `y0`. Can be: - a dict mapping variable names → values - a 1D array of length `n_states` @@ -826,36 +917,22 @@ def solve( t_interp = self.process_t_interp(t_interp) # Set up inputs + if isinstance(inputs, dict): + inputs_list = [inputs] + else: + inputs_list = inputs or [{}] + model_inputs_list: list[dict] = [ - self._set_up_model_inputs(model, inputs) - for inputs in (inputs if isinstance(inputs, list) else [inputs]) + self._set_up_model_inputs(model, inputs) for inputs in inputs_list ] - calculate_sensitivities_list, sensitivities_have_changed = ( + _, sensitivities_have_changed = ( BaseSolver._solve_process_calculate_sensitivities_arg( model_inputs_list[0], model, calculate_sensitivities ) ) # (Re-)calculate consistent initialization - # Assuming initial conditions do not depend on input parameters - # when len(inputs_list) > 1, only `model_inputs_list[0]` - # is passed to `_set_consistent_initialization`. - # See https://github.com/pybamm-team/PyBaMM/pull/1261 - if len(model_inputs_list) > 1: - all_inputs_names = { - key for model_inputs in model_inputs_list for key in model_inputs - } - if all_inputs_names: - initial_conditions_node_names = { - it.name for it in model.concatenated_initial_conditions.pre_order() - } - if not initial_conditions_node_names.isdisjoint(all_inputs_names): - raise pybamm.SolverError( - "Input parameters cannot appear in expression " - "for initial conditions." - ) - # if any setup configuration has changed, we need to re-set up if sensitivities_have_changed: self._model_set_up.pop(model, None) @@ -863,10 +940,6 @@ def solve( 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() # Set the initial conditions @@ -878,12 +951,7 @@ def solve( f'"{existing_model.name}". Please create a separate ' "solver for this model" ) - # 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. Therefore, only `model_inputs[0]` - # is passed to `set_up`. - # See https://github.com/pybamm-team/PyBaMM/pull/1261 - self.set_up(model, model_inputs_list[0], t_eval) + self.set_up(model, model_inputs_list, t_eval) self._model_set_up.update( {model: {"initial conditions": model.concatenated_initial_conditions}} ) @@ -895,26 +963,29 @@ def solve( # For an algebraic solver, we don't need to set up the initial # conditions function and we can just evaluate # model.concatenated_initial_conditions - model.y0 = model.concatenated_initial_conditions.evaluate() + model.y0_list = [ + model.concatenated_initial_conditions.evaluate() + ] * len(inputs_list) else: # If the new initial conditions are different # and cannot be evaluated directly, set up again - self.set_up(model, model_inputs_list[0], t_eval, ics_only=True) - self._model_set_up[model]["initial conditions"] = ( - model.concatenated_initial_conditions - ) + self.set_up(model, model_inputs_list, t_eval, ics_only=True) + self._model_set_up[model][ + "initial conditions" + ] = model.concatenated_initial_conditions else: # Set the standard initial conditions - self._set_initial_conditions(model, t_eval[0], model_inputs_list[0]) + self._set_initial_conditions(model, t_eval[0], model_inputs_list) # Solve for the consistent initialization - self._set_consistent_initialization(model, t_eval[0], model_inputs_list[0]) + self._set_consistent_initialization(model, t_eval[0], model_inputs_list) set_up_time = timer.time() timer.reset() # Check initial conditions don't violate events - self._check_events_with_initialization(t_eval, model, model_inputs_list[0]) + for y0, inpts in zip(model.y0_list, model_inputs_list, strict=False): + self._check_events_with_initialization(t_eval, model, y0, inpts) # Process discontinuities ( @@ -926,45 +997,19 @@ def solve( # Integrate separately over each time segment and accumulate into the solution # object, restarting the solver at each discontinuity (and recalculating a # consistent state afterwards if a DAE) - old_y0 = model.y0 + old_y0_list = model.y0_list solutions = None for start_index, end_index in zip(start_indices, end_indices, strict=False): pybamm.logger.verbose( f"Calling solver for {t_eval[start_index]} < t < {t_eval[end_index - 1]}" ) - if self.supports_parallel_solve: - # Jax and IDAKLU solver can accept a list of inputs - new_solutions = self._integrate( - model, - t_eval[start_index:end_index], - model_inputs_list, - t_interp, - initial_conditions, - ) - else: - ninputs = len(model_inputs_list) - if ninputs == 1: - new_solution = self._integrate( - model, - t_eval[start_index:end_index], - model_inputs_list[0], - t_interp=t_interp, - ) - new_solutions = [new_solution] - else: - with mp.get_context(self._mp_context).Pool(processes=nproc) as p: - new_solutions = p.starmap( - self._integrate, - zip( - [model] * ninputs, - [t_eval[start_index:end_index]] * ninputs, - model_inputs_list, - [t_interp] * ninputs, - strict=False, - ), - ) - p.close() - p.join() + new_solutions = self._integrate( + model, + t_eval[start_index:end_index], + model_inputs_list, + t_interp, + nproc=nproc, + ) # Setting the solve time for each segment. # pybamm.Solution.__add__ assumes attribute solve_time. solve_time = timer.time() @@ -981,13 +1026,14 @@ def solve( if end_index != len(t_eval): # setup for next integration subsection - last_state = solutions[0].y[:, -1] - # update y0 (for DAE solvers, this updates the initial guess for the - # rootfinder) - model.y0 = last_state + for i, soln in enumerate(new_solutions): + last_state = soln.y[:, -1] + # update y0 (for DAE solvers, this updates the initial guess for the + # rootfinder) + model.y0_list[i] = last_state if len(model.algebraic) > 0: - model.y0 = self.calculate_consistent_state( - model, t_eval[end_index], model_inputs_list[0] + model.y0_list = self.calculate_consistent_state( + model, t_eval[end_index], model_inputs_list ) solve_time = timer.time() @@ -1006,7 +1052,7 @@ def solve( solutions[i].solve_time = solve_time # Restore old y0 - model.y0 = old_y0 + model.y0_list = old_y0_list # Report times if len(solutions) == 1: @@ -1113,7 +1159,7 @@ def _get_discontinuity_start_end_indices(self, model, inputs, t_eval): return start_indices, end_indices, t_eval @staticmethod - def _check_events_with_initialization(t_eval, model, inputs_dict): + def _check_events_with_initialization(t_eval, model, y0, inputs_dict): num_terminate_events = len(model.terminate_events_eval) if num_terminate_events == 0: return @@ -1124,9 +1170,9 @@ def _check_events_with_initialization(t_eval, model, inputs_dict): events_eval = [None] * num_terminate_events for idx, event in enumerate(model.terminate_events_eval): if model.convert_to_format == "casadi": - event_eval = event(t_eval[0], model.y0, inputs) + event_eval = event(t_eval[0], y0, inputs) elif model.convert_to_format in ["python", "jax"]: - event_eval = event(t=t_eval[0], y=model.y0, inputs=inputs_dict) + event_eval = event(t=t_eval[0], y=y0, inputs=inputs_dict) events_eval[idx] = event_eval events_eval = np.array(events_eval) @@ -1229,8 +1275,8 @@ def step( Parameters ---------- - old_solution : :class:`pybamm.Solution` or None - The previous solution to be added to. If `None`, a new solution is created. + old_solution : :class:`pybamm.Solution` or list of :class:`pybamm.Solution` or None + The previous solution(s) to be added to. If `None`, a new solution is created. model : :class:`pybamm.BaseModel` The model whose solution to calculate. Must have attributes rhs and initial_conditions @@ -1241,7 +1287,7 @@ def step( (Note: t_eval is the time measured from the start of the step, so should start at 0 and end at dt). By default, the solution is returned at t0 and t0 + dt. npts : deprecated - inputs : dict, optional + inputs : dict, or list of dict, optional Any input parameters to pass to the model when solving save : bool, optional Save solution with all previous timesteps. Defaults to True. @@ -1263,17 +1309,29 @@ def step( `model.variables = {}`) """ + + # Set up inputs + if isinstance(inputs, dict): + inputs_list = [inputs] + else: + inputs_list = inputs or [{}] + if old_solution is None: - old_solution = pybamm.EmptySolution() + old_solutions = [pybamm.EmptySolution()] * len(inputs_list) + elif not isinstance(old_solution, list): + old_solutions = [old_solution] if not ( - isinstance(old_solution, pybamm.EmptySolution) - or old_solution.termination == "final time" - or "[experiment]" in old_solution.termination + isinstance(old_solutions[0], pybamm.EmptySolution) + or old_solutions[0].termination == "final time" + or "[experiment]" in old_solutions[0].termination ): # Return same solution as an event has already been triggered # With hack to allow stepping past experiment current / voltage cut-off - return old_solution + if len(old_solutions) == 1: + return old_solutions[0] + else: + return old_solutions # Make sure model isn't empty self._check_empty_model(model) @@ -1307,7 +1365,7 @@ def step( t_interp = self.process_t_interp(t_interp) - t_start = old_solution.t[-1] + t_start = old_solutions[0].t[-1] t_eval = t_start + t_eval t_interp = t_start + t_interp t_end = t_start + dt @@ -1328,12 +1386,14 @@ def step( timer = pybamm.Timer() # Set up inputs - model_inputs = self._set_up_model_inputs(model, inputs) + model_inputs_list: list[dict] = [ + self._set_up_model_inputs(model, inputs) for inputs in inputs_list + ] # process calculate_sensitivities argument - calculate_sensitivities_list, sensitivities_have_changed = ( + _, sensitivities_have_changed = ( BaseSolver._solve_process_calculate_sensitivities_arg( - model_inputs, model, calculate_sensitivities + model_inputs_list[0], model, calculate_sensitivities ) ) @@ -1346,80 +1406,95 @@ def step( f'"{existing_model.name}". Please create a separate ' "solver for this model" ) - self.set_up(model, model_inputs) + self.set_up(model, model_inputs_list) self._model_set_up.update( {model: {"initial conditions": model.concatenated_initial_conditions}} ) if ( - isinstance(old_solution, pybamm.EmptySolution) - and old_solution.termination is None + isinstance(old_solutions[0], pybamm.EmptySolution) + and old_solutions[0].termination is None ): pybamm.logger.verbose(f"Start stepping {model.name} with {self.name}") using_sensitivities = len(model.calculate_sensitivities) > 0 - if isinstance(old_solution, pybamm.EmptySolution): + if isinstance(old_solutions[0], pybamm.EmptySolution): if not first_step_this_model: # reset y0 to original initial conditions - self.set_up(model, model_inputs, ics_only=True) - elif old_solution.all_models[-1] == model: - last_state = old_solution.last_state - model.y0 = last_state.all_ys[0] + self.set_up(model, model_inputs_list, ics_only=True) + elif old_solutions[0].all_models[-1] == model: + model.y0_list = [s.last_state.all_ys[0] for s in old_solutions] if using_sensitivities: - full_sens = last_state._all_sensitivities["all"][0] - model.y0S = tuple(full_sens[:, i] for i in range(full_sens.shape[1])) + model.y0S_list = [] + for soln in old_solutions: + full_sens = soln.last_state._all_sensitivities["all"][0] + model.y0S_list.append( + tuple(full_sens[:, i] for i in range(full_sens.shape[1])) + ) else: - _, concatenated_initial_conditions = model.set_initial_conditions_from( - old_solution, return_type="ics" - ) - model.y0 = concatenated_initial_conditions.evaluate(0, inputs=model_inputs) + model.y0_list = [] + for soln, inputs in zip(old_solutions, model_inputs_list, strict=False): + _, concatenated_initial_conditions = model.set_initial_conditions_from( + soln, return_type="ics" + ) + model.y0_list.append( + concatenated_initial_conditions.evaluate(0, inputs=inputs) + ) + if using_sensitivities: - model.y0S = self._set_sens_initial_conditions_from(old_solution, model) + model.y0S_list = [ + self._set_sens_initial_conditions_from(soln, model) + for soln in old_solutions + ] set_up_time = timer.time() # (Re-)calculate consistent initialization - self._set_consistent_initialization(model, t_start_shifted, model_inputs) + self._set_consistent_initialization(model, t_start_shifted, model_inputs_list) # Check consistent initialization doesn't violate events - self._check_events_with_initialization(t_eval, model, model_inputs) + for y0, inpts in zip(model.y0_list, model_inputs_list, strict=False): + self._check_events_with_initialization(t_eval, model, y0, inpts) # Step pybamm.logger.verbose(f"Stepping for {t_start_shifted:.0f} < t < {t_end:.0f}") timer.reset() - # API for _integrate is different for JaxSolver and IDAKLUSolver - if self.supports_parallel_solve: - solutions = self._integrate(model, t_eval, [model_inputs], t_interp) - solution = solutions[0] - else: - solution = self._integrate(model, t_eval, model_inputs, t_interp) - solution.solve_time = timer.time() + solutions = self._integrate(model, t_eval, model_inputs_list, t_interp) + for i, s in enumerate(solutions): + solutions[i].solve_time = timer.time() - # Check if extrapolation occurred - self.check_extrapolation(solution, model.events) + # Check if extrapolation occurred + self.check_extrapolation(s, model.events) - # Identify the event that caused termination and update the solution to - # include the event time and state - solution, termination = self.get_termination_reason(solution, model.events) + # Identify the event that caused termination and update the solution to + # include the event time and state + solutions[i], termination = self.get_termination_reason(s, model.events) - # Assign setup time - solution.set_up_time = set_up_time + # Assign setup time + solutions[i].set_up_time = set_up_time # Report times pybamm.logger.verbose(f"Finish stepping {model.name} ({termination})") pybamm.logger.verbose( - f"Set-up time: {solution.set_up_time}, Step time: {solution.solve_time} (of which integration time: {solution.integration_time}), " - f"Total time: {solution.total_time}" + f"Set-up time: {solutions[0].set_up_time}, Step time: {solutions[0].solve_time} (of which integration time: {solutions[0].integration_time}), " + f"Total time: {solutions[0].total_time}" ) # Return solution if save is False: - return solution + ret = solutions + else: + ret = [ + old_s + s for (old_s, s) in zip(old_solutions, solutions, strict=False) + ] + + if len(ret) == 1: + return ret[0] else: - return old_solution + solution + return ret @staticmethod def get_termination_reason(solution, events): @@ -1626,6 +1701,8 @@ def process( expression tree to convert name: str function evaluators created will have this base name + vars_for_processing: dict + dictionary of variables for processing use_jacobian: bool, optional whether to return Jacobian functions return_jacp_stacked: bool, optional diff --git a/src/pybamm/solvers/casadi_algebraic_solver.py b/src/pybamm/solvers/casadi_algebraic_solver.py index f9ff973036..c7e6bfd7fc 100644 --- a/src/pybamm/solvers/casadi_algebraic_solver.py +++ b/src/pybamm/solvers/casadi_algebraic_solver.py @@ -87,7 +87,7 @@ def set_up_root_solver(self, model, inputs_dict, t_eval): The rootfinder function is stored in the model as `algebraic_root_solver`. """ pybamm.logger.info(f"Start building {self.name}") - y0 = model.y0 + y0 = model.y0_list[0] # 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 @@ -151,7 +151,7 @@ def set_up_root_solver(self, model, inputs_dict, t_eval): pybamm.logger.info(f"Finish building {self.name}") - def _integrate(self, model, t_eval, inputs_dict=None, t_interp=None): + def _integrate_single(self, model, t_eval, y0, y0S, inputs_dict, inputs_list=None): """ Calculate the solution of the algebraic equations through root-finding @@ -170,8 +170,6 @@ def _integrate(self, model, t_eval, inputs_dict=None, t_interp=None): # Create casadi objects for the root-finder inputs = casadi.vertcat(*[v for v in inputs_dict.values()]) - 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 # equations will be equal to the initial condition provided. This allows this diff --git a/src/pybamm/solvers/casadi_solver.py b/src/pybamm/solvers/casadi_solver.py index efea953901..6cb64d7431 100644 --- a/src/pybamm/solvers/casadi_solver.py +++ b/src/pybamm/solvers/casadi_solver.py @@ -140,7 +140,7 @@ def __init__( pybamm.citations.register("Andersson2019") - def _integrate(self, model, t_eval, inputs_dict=None, t_interp=None): + def _integrate_single(self, model, t_eval, y0, y0S, inputs_dict, inputs_list=None): """ Solve a DAE model defined by residuals with initial conditions y0. @@ -177,18 +177,15 @@ def _integrate(self, model, t_eval, inputs_dict=None, t_interp=None): use_event_switch = False # Create an integrator with the grid (we just need to do this once) self.create_integrator( - model, inputs, t_eval, use_event_switch=use_event_switch - ) - solution = self._run_integrator( - model, model.y0, inputs_dict, inputs, t_eval + model, y0, inputs, t_eval, use_event_switch=use_event_switch ) + solution = self._run_integrator(model, y0, inputs_dict, inputs, t_eval) # Check if the sign of an event changes, if so find an accurate # termination point and exit - solution = self._solve_for_event(solution) + solution = self._solve_for_event(solution, y0) solution.check_ys_are_not_too_large() return solution elif self.mode in ["safe", "safe without grid"]: - y0 = model.y0 # Step-and-check t = t_eval[0] t_f = t_eval[-1] @@ -199,7 +196,7 @@ def _integrate(self, model, t_eval, inputs_dict=None, t_interp=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, y0, inputs) # Initialize solution solution = pybamm.Solution( np.array([t]), @@ -246,7 +243,7 @@ def _integrate(self, model, t_eval, inputs_dict=None, t_interp=None): if self.mode == "safe": # update integrator with the grid - self.create_integrator(model, inputs, t_window) + self.create_integrator(model, y0, inputs, t_window) # Try to solve with the current global step, if it fails then # halve the step size and try again. try: @@ -300,7 +297,7 @@ def _integrate(self, model, t_eval, inputs_dict=None, t_interp=None): break # Check if the sign of an event changes, if so find an accurate # termination point and exit - current_step_sol = self._solve_for_event(current_step_sol) + current_step_sol = self._solve_for_event(current_step_sol, y0) # assign temporary solve time current_step_sol.solve_time = np.nan # append solution from the current step to solution @@ -318,7 +315,7 @@ def _integrate(self, model, t_eval, inputs_dict=None, t_interp=None): solution.check_ys_are_not_too_large() return solution - def _solve_for_event(self, coarse_solution): + def _solve_for_event(self, coarse_solution, y0): """ Check if the sign of an event changes, if so find an accurate termination point and exit @@ -447,7 +444,7 @@ def integer_bisect(): if self.mode == "safe without grid": use_grid = False else: - self.create_integrator(model, inputs, t_window_event_dense) + self.create_integrator(model, y0, inputs, t_window_event_dense) use_grid = True y0 = coarse_solution.y[:, event_idx_lower] @@ -493,7 +490,7 @@ def integer_bisect(): return solution - def create_integrator(self, model, inputs, t_eval=None, use_event_switch=False): + def create_integrator(self, model, y0, inputs, t_eval=None, use_event_switch=False): """ Method to create a casadi integrator object. If t_eval is provided, the integrator uses t_eval to make the grid. @@ -539,7 +536,6 @@ 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]) - 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/src/pybamm/solvers/dummy_solver.py b/src/pybamm/solvers/dummy_solver.py index 11472c13e7..65479edd1a 100644 --- a/src/pybamm/solvers/dummy_solver.py +++ b/src/pybamm/solvers/dummy_solver.py @@ -13,7 +13,7 @@ def __init__(self): super().__init__() self.name = "Dummy solver" - def _integrate(self, model, t_eval, inputs_dict=None, t_interp=None): + def _integrate_single(self, model, t_eval, y0, y0S, inputs_dict, inputs_list=None): """ Solve an empty model. diff --git a/src/pybamm/solvers/idaklu_solver.py b/src/pybamm/solvers/idaklu_solver.py index 780b20d215..bd9509087e 100644 --- a/src/pybamm/solvers/idaklu_solver.py +++ b/src/pybamm/solvers/idaklu_solver.py @@ -254,15 +254,22 @@ def _check_atol_type(self, atol, size): def set_up(self, model, inputs=None, t_eval=None, ics_only=False): base_set_up_return = super().set_up(model, inputs, t_eval, ics_only) - inputs_dict = inputs or {} + if isinstance(inputs, dict): + inputs_list = [inputs] + else: + inputs_list = inputs or [{}] + # stack inputs - if inputs_dict: - arrays_to_stack = [np.array(x).reshape(-1, 1) for x in inputs_dict.values()] + if inputs_list and inputs_list[0]: + arrays_to_stack = [ + np.array(x).reshape(-1, 1) for x in inputs_list[0].values() + ] inputs = np.vstack(arrays_to_stack) else: inputs = np.array([[]]) - y0 = model.y0 + y0 = model.y0_list[0] + if isinstance(y0, casadi.DM): y0 = y0.full() y0 = y0.flatten() @@ -294,7 +301,7 @@ def set_up(self, model, inputs=None, t_eval=None, ics_only=False): y_casadi = casadi.MX.sym("y", model.len_rhs_and_alg) cj_casadi = casadi.MX.sym("cj") p_casadi = {} - for name, value in inputs_dict.items(): + for name, value in inputs_list[0].items(): if isinstance(value, numbers.Number): p_casadi[name] = casadi.MX.sym(name) else: @@ -401,7 +408,7 @@ def set_up(self, model, inputs=None, t_eval=None, ics_only=False): idaklu.generate_function(self.var_idaklu_fcns_pkl[-1]) ) # Convert derivative functions for sensitivities - if (len(inputs) > 0) and (model.calculate_sensitivities): + if (inputs.shape[0] > 0) and (model.calculate_sensitivities): self.dvar_dy_idaklu_fcns_pkl.append( self.computed_dvar_dy_fcns[key].serialize() ) @@ -565,70 +572,16 @@ def __setstate__(self, d): def supports_parallel_solve(self): return True - def _apply_solver_initial_conditions(self, model, initial_conditions): + def _integrate(self, model, t_eval, inputs_list=None, t_interp=None, nproc=None): """ - Apply custom initial conditions to a model by overriding model.y0. - - Parameters - ---------- - model : pybamm.BaseModel - A model with a precomputed y0 vector. - initial_conditions : dict or numpy.ndarray - Either a mapping from variable names to values (scalar or array), - or a flat numpy array matching the length of model.y0. - """ - if isinstance(initial_conditions, dict): - y0_np = ( - model.y0.full() if isinstance(model.y0, casadi.DM) else model.y0.copy() - ) - - for var_name, value in initial_conditions.items(): - found = False - for symbol, slice_info in model.y_slices.items(): - if symbol.name == var_name: - var_slice = slice_info[0] - y0_np[var_slice] = value - found = True - break - if not found: - raise ValueError(f"Variable '{var_name}' not found in model") - - model.y0 = casadi.DM(y0_np) - - elif isinstance(initial_conditions, np.ndarray): - model.y0 = casadi.DM(initial_conditions) - else: - raise TypeError("Initial conditions must be dict or numpy array") - - def _integrate( - self, model, t_eval, inputs_list=None, t_interp=None, initial_conditions=None - ): - """ - Solve a DAE model defined by residuals with initial conditions y0. - - Parameters - ---------- - model : :class:`pybamm.BaseModel` - The model whose solution to calculate. - t_eval : numeric type - The times at which to stop the integration due to a discontinuity in time. - inputs_list: list of dict, optional - Any input parameters to pass to the model when solving. - t_interp : None, list or ndarray, optional - The times (in seconds) at which to interpolate the solution. Defaults to `None`, - which returns the adaptive time-stepping times. - initial_conditions : dict, numpy.ndarray, or list, optional - Override the model’s default `y0`. Can be: - - - a dict mapping variable names → values - - a 1D array of length `n_states` - - a list of such overrides (one per parallel solve) - + Overloads the _integrate method from BaseSolver to use the IDAKLU solver """ if model.convert_to_format != "casadi": # pragma: no cover # Shouldn't ever reach this point raise pybamm.SolverError("Unsupported IDAKLU solver configuration.") + if isinstance(inputs_list, dict): + inputs_list = [inputs_list] inputs_list = inputs_list or [{}] # stack inputs so that they are a 2D array of shape (number_of_inputs, number_of_parameters) @@ -642,36 +595,9 @@ def _integrate( else: inputs = np.array([[]] * len(inputs_list)) - if initial_conditions is not None: - if isinstance(initial_conditions, list): - if len(initial_conditions) != len(inputs_list): - raise ValueError( - "Number of initial conditions must match number of input sets" - ) - - y0_list = [] - - model_copy = model.new_copy() - for ic in initial_conditions: - self._apply_solver_initial_conditions(model_copy, ic) - y0_list.append(model_copy.y0.full().flatten()) - - y0full = np.vstack(y0_list) - ydot0full = np.zeros_like(y0full) - - else: - self._apply_solver_initial_conditions(model, initial_conditions) - - y0_np = model.y0.full() - - y0full = np.vstack([y0_np for _ in range(len(inputs_list))]) - ydot0full = np.zeros_like(y0full) - else: - # stack y0full and ydot0full so they are a 2D array of shape (number_of_inputs, number_of_states + number_of_parameters * number_of_states) - # note that y0full and ydot0full are currently 1D arrays (i.e. independent of inputs), but in the future we will support - # different initial conditions for different inputs. For now we just repeat the same initial conditions for each input - y0full = np.vstack([model.y0full] * len(inputs_list)) - ydot0full = np.vstack([model.ydot0full] * len(inputs_list)) + # y0full is now a list with length = number of input sets + y0full = np.vstack(model.y0full) + ydot0full = np.vstack(model.ydot0full) atol = getattr(model, "atol", self.atol) atol = self._check_atol_type(atol, y0full.size) @@ -831,7 +757,7 @@ def _get_variable_info(self, model, var) -> tuple: f"{model.convert_to_format}" ) - def _set_consistent_initialization(self, model, time, inputs_dict): + def _set_consistent_initialization(self, model, time, inputs_list): """ Initialize y0 and ydot0 for the solver. In addition to calculating y0 from BaseSolver, we also calculate ydot0 for semi-explicit DAEs @@ -846,33 +772,47 @@ def _set_consistent_initialization(self, model, time, inputs_dict): Any input parameters to pass to the model when solving. """ - # set model.y0 - super()._set_consistent_initialization(model, time, inputs_dict) + # set model.y0_list + super()._set_consistent_initialization(model, time, inputs_list) casadi_format = model.convert_to_format == "casadi" - y0 = model.y0 - if isinstance(y0, casadi.DM): - y0 = y0.full() - y0 = y0.flatten() + def handle_y0(y0): + if y0 is None: + return y0 + if isinstance(y0, casadi.DM): + y0 = y0.full() + return y0.flatten() + + y0_list = [handle_y0(y0) for y0 in model.y0_list] + # inputs_full = [handle_y0(input) for input in inputs_list] # calculate the time derivatives of the differential equations # for semi-explicit DAEs if model.len_rhs > 0: - ydot0 = self._rhs_dot_consistent_initialization( - y0, model, time, inputs_dict - ) + ydot0_list = [ + self._rhs_dot_consistent_initialization(y0, model, time, inputs_dict) + for y0, inputs_dict in zip(y0_list, inputs_list, strict=False) + ] else: - ydot0 = np.zeros_like(y0) + ydot0_list = [np.zeros_like(y0) for y0 in y0_list] - sensitivity = (model.y0S is not None) and casadi_format + sensitivity = model.y0S_list and casadi_format if sensitivity: - y0full, ydot0full = self._sensitivity_consistent_initialization( - y0, ydot0, model, time, inputs_dict - ) + y0S_list = model.y0S_list + y0full = [] + ydot0full = [] + for y0, ydot0, y0S, inputs_dict in zip( + y0_list, ydot0_list, y0S_list, inputs_list, strict=False + ): + y0f, ydot0f = self._sensitivity_consistent_initialization( + y0, ydot0, y0S, time, inputs_dict + ) + y0full.append(y0f) + ydot0full.append(ydot0f) else: - y0full = y0 - ydot0full = ydot0 + y0full = y0_list + ydot0full = ydot0_list model.y0full = y0full model.ydot0full = ydot0full @@ -924,9 +864,7 @@ def _rhs_dot_consistent_initialization(self, y0, model, time, inputs_dict): return ydot0 - def _sensitivity_consistent_initialization( - self, y0, ydot0, model, time, inputs_dict - ): + def _sensitivity_consistent_initialization(self, y0, ydot0, y0S, time, inputs_dict): """ Extend the consistent initialization to include the sensitivty equations @@ -936,17 +874,15 @@ def _sensitivity_consistent_initialization( The initial values of the state vector. ydot0 : :class:`numpy.array` The initial values of the time derivatives of the state vector. + y0S : :class:`numpy.array` + The initial values of the sensitivity state vectors. time : numeric type The time at which to calculate the initial conditions. - model : :class:`pybamm.BaseModel` - The model for which to calculate initial conditions. inputs_dict : dict Any input parameters to pass to the model when solving. """ - y0S = model.y0S - if isinstance(y0S, casadi.DM): y0S = (y0S,) diff --git a/src/pybamm/solvers/jax_solver.py b/src/pybamm/solvers/jax_solver.py index aa88f3a3a9..8bbbb98848 100644 --- a/src/pybamm/solvers/jax_solver.py +++ b/src/pybamm/solvers/jax_solver.py @@ -200,9 +200,7 @@ def supports_parallel_solve(self): def requires_explicit_sensitivities(self): return False - def _integrate( - self, model, t_eval, inputs=None, t_interp=None, intial_conditions=None - ): + def _integrate(self, model, t_eval, inputs=None, t_interp=None, nproc=None): """ Solve a model defined by dydt with initial conditions y0. @@ -222,12 +220,10 @@ def _integrate( various diagnostic messages. """ - if intial_conditions is not None: # pragma: no cover - raise NotImplementedError( - "Setting initial conditions is not yet implemented for the JAX IDAKLU solver" - ) if isinstance(inputs, dict): inputs = [inputs] + inputs = inputs or [{}] + timer = pybamm.Timer() if model not in self._cached_solves: self._cached_solves[model] = self.create_solve(model, t_eval) diff --git a/src/pybamm/solvers/scipy_solver.py b/src/pybamm/solvers/scipy_solver.py index fd2c56a188..81b6fbd5b3 100644 --- a/src/pybamm/solvers/scipy_solver.py +++ b/src/pybamm/solvers/scipy_solver.py @@ -52,7 +52,7 @@ def __init__( self.name = f"Scipy solver ({method})" pybamm.citations.register("Virtanen2020") - def _integrate(self, model, t_eval, inputs_dict=None, t_interp=None): + def _integrate_single(self, model, t_eval, y0, y0S, inputs_dict, inputs_list=None): """ Solve a model defined by dydt with initial conditions y0. @@ -88,7 +88,6 @@ def _integrate(self, model, t_eval, inputs_dict=None, t_interp=None): extra_options = {**self.extra_options, "rtol": self.rtol, "atol": self.atol} # Initial conditions - y0 = model.y0 if isinstance(y0, casadi.DM): y0 = y0.full() y0 = y0.flatten() diff --git a/src/pybamm/solvers/solution.py b/src/pybamm/solvers/solution.py index b958932f0d..cda6e784d9 100644 --- a/src/pybamm/solvers/solution.py +++ b/src/pybamm/solvers/solution.py @@ -316,7 +316,8 @@ def first_state(self): all_ys = self.all_ys[0][:, :1] else: # Get first state from initial conditions as all_ys is empty - all_ys = self.all_models[0].y0full.reshape(-1, 1) + # TODO: What should this look like when there are multiple y0's? + all_ys = self.all_models[0].y0full[0].reshape(-1, 1) new_sol = Solution( self.all_ts[0][:1], diff --git a/tests/unit/test_solvers/test_algebraic_solver.py b/tests/unit/test_solvers/test_algebraic_solver.py index 58068ca6e3..4b5436f221 100644 --- a/tests/unit/test_solvers/test_algebraic_solver.py +++ b/tests/unit/test_solvers/test_algebraic_solver.py @@ -41,7 +41,8 @@ def test_wrong_solver(self): def test_simple_root_find(self): # Simple system: a single algebraic equation class Model(pybamm.BaseModel): - y0 = np.array([2]) + y0_list = [np.array([2])] + y0S_list = None rhs = {} jac_algebraic_eval = None len_rhs_and_alg = 1 @@ -56,12 +57,13 @@ def algebraic_eval(self, t, y, inputs): # Try passing extra options to solver solver = pybamm.AlgebraicSolver(extra_options={"maxiter": 100}) model = Model() - solution = solver._integrate(model, np.array([0])) - np.testing.assert_array_equal(solution.y, -2) + solutions = solver._integrate(model, np.array([0])) + np.testing.assert_array_equal(solutions[0].y, -2) def test_root_find_fail(self): class Model(pybamm.BaseModel): - y0 = np.array([2]) + y0_list = [np.array([2])] + y0S_list = None rhs = {} jac_algebraic_eval = None len_rhs_and_alg = 1 @@ -95,7 +97,8 @@ def test_with_jacobian(self): b = np.array([0, 7]) class Model(pybamm.BaseModel): - y0 = np.zeros(2) + y0_list = [np.zeros(2)] + y0S_list = None rhs = {} len_rhs_and_alg = 2 @@ -113,8 +116,8 @@ def jac_algebraic_eval(self, t, y, inputs): sol = np.array([3, -4])[:, np.newaxis] solver = pybamm.AlgebraicSolver() - solution = solver._integrate(model, np.array([0])) - np.testing.assert_allclose(solution.y, sol, rtol=1e-7, atol=1e-6) + solutions = solver._integrate(model, np.array([0])) + np.testing.assert_allclose(solutions[0].y, sol, rtol=1e-7, atol=1e-6) def test_model_solver(self): # Create model diff --git a/tests/unit/test_solvers/test_base_solver.py b/tests/unit/test_solvers/test_base_solver.py index 90c84eb986..c5557dc748 100644 --- a/tests/unit/test_solvers/test_base_solver.py +++ b/tests/unit/test_solvers/test_base_solver.py @@ -137,7 +137,8 @@ def test_find_consistent_initialization(self): # Simple system: a single algebraic equation class ScalarModel: def __init__(self): - self.y0 = np.array([2]) + self.y0_list = [np.array([2])] + self.y0S_list = None self.rhs = {} self.jac_algebraic_eval = None t = casadi.MX.sym("t") @@ -172,7 +173,8 @@ def algebraic_eval(self, t, y, inputs): class VectorModel: def __init__(self): - self.y0 = np.zeros_like(vec) + self.y0_list = [np.zeros_like(vec)] + self.y0S_list = None self.rhs = {"test": "test"} self.concatenated_rhs = np.array([1]) self.jac_algebraic_eval = None @@ -195,11 +197,11 @@ def algebraic_eval(self, t, y, inputs): return (y[1:] - vec[1:]) ** 2 model = VectorModel() - init_states = solver.calculate_consistent_state(model) + [init_states] = solver.calculate_consistent_state(model) np.testing.assert_allclose(init_states.flatten(), vec, rtol=1e-7, atol=1e-6) # with casadi solver_with_casadi.root_method.step_tol = 1e-12 - init_states = solver_with_casadi.calculate_consistent_state(model) + [init_states] = solver_with_casadi.calculate_consistent_state(model) np.testing.assert_allclose( init_states.full().flatten(), vec, rtol=1e-7, atol=1e-6 ) @@ -209,7 +211,7 @@ def jac_dense(t, y, inputs): return 2 * np.hstack([np.zeros((3, 1)), np.diag(y[1:] - vec[1:])]) model.jac_algebraic_eval = jac_dense - init_states = solver.calculate_consistent_state(model) + [init_states] = solver.calculate_consistent_state(model) np.testing.assert_allclose(init_states.flatten(), vec, rtol=1e-7, atol=1e-6) # With sparse Jacobian @@ -219,13 +221,14 @@ def jac_sparse(t, y, inputs): ) model.jac_algebraic_eval = jac_sparse - init_states = solver.calculate_consistent_state(model) + [init_states] = solver.calculate_consistent_state(model) np.testing.assert_allclose(init_states.flatten(), vec, rtol=1e-7, atol=1e-6) def test_fail_consistent_initialization(self): class Model: def __init__(self): - self.y0 = np.array([2]) + self.y0_list = [np.array([2])] + self.y0S_list = None self.rhs = {} self.jac_algebraic_eval = None t = casadi.MX.sym("t") @@ -447,31 +450,33 @@ def test_on_extrapolation_and_on_failure_settings(self): ): base_solver.on_failure = "invalid" - def test_solver_multiple_inputs_initial_conditions_error(self): - y = pybamm.Variable("y") - y0 = pybamm.InputParameter("y0") - k = pybamm.InputParameter("k") + # need to test this outside the base solver + # def test_solver_multiple_inputs_initial_conditions(self): + # y = pybamm.Variable("y") + # y0 = pybamm.InputParameter("y0") + # k = pybamm.InputParameter("k") - model = pybamm.BaseModel() - model.rhs = {y: -k * y} - model.initial_conditions = {y: y0} - model.variables = {"y": y} + # model = pybamm.BaseModel() + # model.rhs = {y: -k * y} + # model.initial_conditions = {y: y0} + # model.variables = {"y": y} - disc = pybamm.Discretisation() - disc.process_model(model) + # disc = pybamm.Discretisation() + # disc.process_model(model) - t_eval = np.linspace(0.0, 1.0, 6) + # t_eval = np.linspace(0.0, 1.0, 6) - # Three different ICs so each run is clearly distinct - inputs_list = [ - {"y0": 1.0, "k": 0.5}, - {"y0": 2.0, "k": 1.0}, - {"y0": 3.0, "k": 1.5}, - ] + # # Three different ICs so each run is clearly distinct + # inputs_list = [ + # {"y0": 1.0, "k": 0.5}, + # {"y0": 2.0, "k": 1.0}, + # {"y0": 3.0, "k": 1.5}, + # ] - solver = pybamm.BaseSolver() - with pytest.raises( - pybamm.SolverError, - match="Input parameters cannot appear in expression for initial conditions", - ): - solver.solve(model, t_eval=t_eval, inputs=inputs_list) + # solver = pybamm.BaseSolver() + + # sols = solver.solve(model, t_eval=t_eval, inputs=inputs_list) + + # # Extract y(0) actually used per run + # ic_used = [float(sol["y"].entries[0]) for sol in sols] + # assert ic_used == [1.0, 2.0, 3.0] diff --git a/tests/unit/test_solvers/test_casadi_algebraic_solver.py b/tests/unit/test_solvers/test_casadi_algebraic_solver.py index 36a9d87438..f9775e44fc 100644 --- a/tests/unit/test_solvers/test_casadi_algebraic_solver.py +++ b/tests/unit/test_solvers/test_casadi_algebraic_solver.py @@ -68,7 +68,8 @@ def test_algebraic_root_solver_reuse(self): def test_root_find_fail(self): class Model: - y0 = np.array([2]) + y0_list = [np.array([2])] + y0S_list = None t = casadi.MX.sym("t") y = casadi.MX.sym("y") p = casadi.MX.sym("p") @@ -98,7 +99,8 @@ def algebraic_eval(self, t, y, inputs): # Model returns Nan class NaNModel: - y0 = np.array([-2]) + y0_list = [np.array([-2])] + y0S_list = None t = casadi.MX.sym("t") y = casadi.MX.sym("y") p = casadi.MX.sym("p") diff --git a/tests/unit/test_solvers/test_scipy_solver.py b/tests/unit/test_solvers/test_scipy_solver.py index b898628dc4..f66f9eec54 100644 --- a/tests/unit/test_solvers/test_scipy_solver.py +++ b/tests/unit/test_solvers/test_scipy_solver.py @@ -334,7 +334,7 @@ def test_model_solver_multiple_inputs_discontinuity_error(self): ): solver.solve(model, t_eval, inputs=inputs_list, nproc=2) - def test_model_solver_multiple_inputs_initial_conditions_error(self): + def test_model_solver_multiple_inputs_initial_conditions(self): # Create model model = pybamm.BaseModel() model.convert_to_format = "casadi" @@ -353,12 +353,11 @@ def test_model_solver_multiple_inputs_initial_conditions_error(self): ninputs = 8 inputs_list = [{"rate": 0.01 * (i + 1)} for i in range(ninputs)] - with pytest.raises( - pybamm.SolverError, - match="Input parameters cannot appear in expression " - "for initial conditions.", - ): - solver.solve(model, t_eval, inputs=inputs_list, nproc=2) + solutions = solver.solve(model, t_eval, inputs=inputs_list, nproc=2) + + # Extract y(0) actually used per run + ic_used = [float(sol["var"].entries[0][0]) for sol in solutions] + assert ic_used == [0.02, 0.04, 0.06, 0.08, 0.1, 0.12, 0.14, 0.16] def test_model_solver_with_event_with_casadi(self): # Create model From d9b6ad7f8a00b9d4dec44d9a90ed8348809f8f46 Mon Sep 17 00:00:00 2001 From: Pip Liggins Date: Thu, 16 Oct 2025 17:21:14 +0100 Subject: [PATCH 06/42] Fix all tests except jax --- src/pybamm/solvers/base_solver.py | 7 + tests/integration/test_solvers/test_idaklu.py | 41 ++---- tests/unit/test_solvers/test_base_solver.py | 31 ---- tests/unit/test_solvers/test_idaklu_solver.py | 137 +++--------------- 4 files changed, 41 insertions(+), 175 deletions(-) diff --git a/src/pybamm/solvers/base_solver.py b/src/pybamm/solvers/base_solver.py index a80d14d2fe..9b88a76db6 100644 --- a/src/pybamm/solvers/base_solver.py +++ b/src/pybamm/solvers/base_solver.py @@ -1682,6 +1682,13 @@ def _set_up_model_inputs(model, inputs): raise pybamm.SolverError(f"No value provided for input '{name}'") inputs_in_model[name] = inputs[name] + missing_inputs = set(inputs) - set(inputs_in_model) + if missing_inputs: + warnings.warn( + f"The following inputs are not used in the model: {missing_inputs}", + pybamm.SolverWarning, + stacklevel=2, + ) inputs = inputs_in_model ordered_inputs_names = list(inputs.keys()) diff --git a/tests/integration/test_solvers/test_idaklu.py b/tests/integration/test_solvers/test_idaklu.py index 20738d2477..13b7b0cb48 100644 --- a/tests/integration/test_solvers/test_idaklu.py +++ b/tests/integration/test_solvers/test_idaklu.py @@ -212,23 +212,20 @@ def test_with_experiments(self): ) @pytest.mark.parametrize( - "model_cls, make_ics", + "model_cls", [ - (pybamm.lithium_ion.SPM, lambda y0: [y0, 2 * y0]), - ( - pybamm.lithium_ion.DFN, - lambda y0: [y0, y0 * (1 + 0.01 * np.ones_like(y0))], - ), + pybamm.lithium_ion.SPM, + pybamm.lithium_ion.DFN, ], + ids=["SPM", "DFN"], ) - def test_multiple_initial_conditions_against_independent_solves( - self, model_cls, make_ics - ): + def test_multiple_initial_conditions_against_independent_solves(self, model_cls): model = model_cls() geom = model.default_geometry - pv = model.default_parameter_values - pv.process_model(model) - pv.process_geometry(geom) + param = model.default_parameter_values + param.update({"Current function [A]": "[input]"}) + param.process_model(model) + param.process_geometry(geom) mesh = pybamm.Mesh(geom, model.default_submesh_types, model.default_var_pts) disc = pybamm.Discretisation(mesh, model.default_spatial_methods) disc.process_model(model) @@ -236,27 +233,18 @@ def test_multiple_initial_conditions_against_independent_solves( t_eval = np.array([0, 1]) solver = pybamm.IDAKLUSolver() - base_sol = solver.solve(model, t_eval) - y0_base = base_sol.y[:, 0] - - ics = make_ics(y0_base) - inputs = [{}] * len(ics) + inputs = [{"Current function [A]": value} for value in [0.1, 2.0]] multi_sols = solver.solve( model, t_eval, inputs=inputs, - initial_conditions=ics, ) assert isinstance(multi_sols, list) and len(multi_sols) == 2 indep_sols = [] - for ic in ics: - sol_indep = solver.solve( - model, t_eval, inputs=[{}], initial_conditions=[ic] - ) - if isinstance(sol_indep, list): - sol_indep = sol_indep[0] + for ic in inputs: + sol_indep = solver.solve(model, t_eval, inputs=ic) indep_sols.append(sol_indep) if model_cls is pybamm.lithium_ion.SPM: @@ -271,11 +259,6 @@ def test_multiple_initial_conditions_against_independent_solves( np.testing.assert_allclose(sol_vec.t, sol_ind.t, rtol=1e-12, atol=0) np.testing.assert_allclose(sol_vec.y, sol_ind.y, rtol=rtol, atol=atol) - if model_cls is pybamm.lithium_ion.SPM: - np.testing.assert_allclose( - sol_vec.y[:, 0], ics[idx], rtol=1e-8, atol=1e-10 - ) - def test_outvars_with_experiments_multi_simulation(self): model = pybamm.lithium_ion.SPM() diff --git a/tests/unit/test_solvers/test_base_solver.py b/tests/unit/test_solvers/test_base_solver.py index c5557dc748..d0d3732df7 100644 --- a/tests/unit/test_solvers/test_base_solver.py +++ b/tests/unit/test_solvers/test_base_solver.py @@ -449,34 +449,3 @@ def test_on_extrapolation_and_on_failure_settings(self): ValueError, match="on_failure must be 'warn', 'raise', or 'ignore'" ): base_solver.on_failure = "invalid" - - # need to test this outside the base solver - # def test_solver_multiple_inputs_initial_conditions(self): - # y = pybamm.Variable("y") - # y0 = pybamm.InputParameter("y0") - # k = pybamm.InputParameter("k") - - # model = pybamm.BaseModel() - # model.rhs = {y: -k * y} - # model.initial_conditions = {y: y0} - # model.variables = {"y": y} - - # disc = pybamm.Discretisation() - # disc.process_model(model) - - # t_eval = np.linspace(0.0, 1.0, 6) - - # # Three different ICs so each run is clearly distinct - # inputs_list = [ - # {"y0": 1.0, "k": 0.5}, - # {"y0": 2.0, "k": 1.0}, - # {"y0": 3.0, "k": 1.5}, - # ] - - # solver = pybamm.BaseSolver() - - # sols = solver.solve(model, t_eval=t_eval, inputs=inputs_list) - - # # Extract y(0) actually used per run - # ic_used = [float(sol["y"].entries[0]) for sol in sols] - # assert ic_used == [1.0, 2.0, 3.0] diff --git a/tests/unit/test_solvers/test_idaklu_solver.py b/tests/unit/test_solvers/test_idaklu_solver.py index c2e1e860c9..76d5943757 100644 --- a/tests/unit/test_solvers/test_idaklu_solver.py +++ b/tests/unit/test_solvers/test_idaklu_solver.py @@ -414,11 +414,11 @@ def test_ida_roberts_consistent_initialization(self): # Set up and model consistently initialize the model solver.set_up(model) t0 = 0.0 - solver._set_consistent_initialization(model, t0, inputs_dict={}) + solver._set_consistent_initialization(model, t0, inputs_list=[{}]) # u(t0) = 0, v(t0) = 1 np.testing.assert_allclose( - model.y0full, + model.y0full[0], [0, 1], rtol=1e-7, atol=1e-6, @@ -426,7 +426,7 @@ def test_ida_roberts_consistent_initialization(self): # u'(t0) = 0.1 * v(t0) = 0.1 # Since v is algebraic, the initial derivative is set to 0 np.testing.assert_allclose( - model.ydot0full, + model.ydot0full[0], [0.1, 0], rtol=1e-7, atol=1e-6, @@ -1178,8 +1178,9 @@ def test_multiple_initial_conditions_dict(self): model = pybamm.BaseModel() model.convert_to_format = None u = pybamm.Variable("u") + u0 = pybamm.InputParameter("u0") model.rhs = {u: -u} - model.initial_conditions = {u: 1} + model.initial_conditions = {u: u0} model.variables = {"u": u} disc = pybamm.Discretisation() @@ -1188,16 +1189,14 @@ def test_multiple_initial_conditions_dict(self): solver = pybamm.IDAKLUSolver(options={"num_threads": 1}) n_sims = 3 - initial_conditions = [{"u": i + 1} for i in range(n_sims)] - inputs = [{} for _ in range(n_sims)] + initial_condition_inputs = [{"u0": i + 1} for i in range(n_sims)] t_eval = np.array([0, 1]) t_interp = np.linspace(0, 1, 10) solutions = solver.solve( model, t_eval, - inputs=inputs, - initial_conditions=initial_conditions, + inputs=initial_condition_inputs, t_interp=t_interp, ) @@ -1216,8 +1215,9 @@ def test_single_initial_condition_dict(self): model = pybamm.BaseModel() model.convert_to_format = "casadi" u = pybamm.Variable("u") + u0 = pybamm.InputParameter("u0") model.rhs = {u: -u} - model.initial_conditions = {u: 1} + model.initial_conditions = {u: u0} model.variables = {"u": u} disc = pybamm.Discretisation() @@ -1225,12 +1225,12 @@ def test_single_initial_condition_dict(self): solver = pybamm.IDAKLUSolver() - initial_condition = {"u": 5} + initial_condition_input = {"u0": 5} t_eval = np.array([0, 1]) t_interp = np.linspace(0, 1, 10) solution = solver.solve( - model, t_eval, initial_conditions=initial_condition, t_interp=t_interp + model, t_eval, initial_condition_input, t_interp=t_interp ) np.testing.assert_allclose(solution["u"](0), 5) @@ -1238,59 +1238,14 @@ def test_single_initial_condition_dict(self): solution["u"](t_eval), 5 * np.exp(-t_eval), rtol=1e-3, atol=1e-5 ) - inputs = [{} for _ in range(3)] - solutions = solver.solve( - model, t_eval, inputs=inputs, initial_conditions=initial_condition - ) - - assert len(solutions) == 3 - for solution in solutions: - np.testing.assert_allclose(solution["u"](0), 5) - np.testing.assert_allclose( - solution["u"](t_eval), 5 * np.exp(-t_eval), rtol=1e-3, atol=1e-5 - ) - - def test_initial_condition_array(self): - model = pybamm.BaseModel() - u = pybamm.Variable("u") - model.rhs = {u: -u} - model.initial_conditions = {u: 1} - model.variables = {"u": u} - - disc = pybamm.Discretisation() - disc.process_model(model) - - solver = pybamm.IDAKLUSolver() - t_eval = np.array([0, 1]) - t_interp = np.linspace(0, 1, 10) - - ics = [np.array([2.0]), np.array([4.0]), np.array([6.0])] - inputs = [{} for _ in ics] - - solutions = solver.solve( - model, - t_eval, - t_interp=t_interp, - inputs=inputs, - initial_conditions=ics, - ) - - assert len(solutions) == len(ics) - - for ic_array, sol in zip(ics, solutions, strict=False): - start = ic_array.item() - got0 = sol["u"](0) - np.testing.assert_allclose(got0, start, rtol=1e-3, atol=1e-5) - got_curve = sol["u"](t_eval) - expected_curve = start * np.exp(-t_eval) - np.testing.assert_allclose(got_curve, expected_curve, rtol=1e-3, atol=1e-5) - def test_multiple_variables(self): model = pybamm.BaseModel() u = pybamm.Variable("u") v = pybamm.Variable("v") + u0 = pybamm.InputParameter("u0") + v0 = pybamm.InputParameter("v0") model.rhs = {u: -u, v: -2 * v} - model.initial_conditions = {u: 1, v: 2} + model.initial_conditions = {u: u0, v: v0} model.variables = {"u": u, "v": v} disc = pybamm.Discretisation() @@ -1299,7 +1254,7 @@ def test_multiple_variables(self): # Use default solver tolerances solver = pybamm.IDAKLUSolver() - initial_conditions = [{"u": 3, "v": 4}, {"u": 5, "v": 6}] + initial_conditions = [{"u0": 3, "v0": 4}, {"u0": 5, "v0": 6}] t_eval = np.array([0, 1]) t_interp = np.linspace(0, 1, 10) @@ -1307,8 +1262,7 @@ def test_multiple_variables(self): solutions = solver.solve( model, t_eval, - inputs=[{}, {}], - initial_conditions=initial_conditions, + inputs=initial_conditions, t_interp=t_interp, ) @@ -1332,11 +1286,12 @@ def test_multiple_variables(self): solutions[1]["v"](t_eval), 6 * np.exp(-2 * t_eval), rtol=1e-3, atol=1e-5 ) - def test_error_variable_not_found(self): + def test_warning_variable_not_used(self): model = pybamm.BaseModel() u = pybamm.Variable("u") + u0 = pybamm.InputParameter("u0") model.rhs = {u: -u} - model.initial_conditions = {u: 1} + model.initial_conditions = {u: u0} model.variables = {"u": u} disc = pybamm.Discretisation() @@ -1344,60 +1299,12 @@ def test_error_variable_not_found(self): solver = pybamm.IDAKLUSolver() - initial_condition = {"nonexistent_variable": 5} + initial_condition = {"nonexistent_variable": 5, "u0": 1} t_eval = np.linspace(0, 1, 10) - with pytest.raises( - ValueError, match="Variable 'nonexistent_variable' not found in model" - ): - solver.solve(model, t_eval, initial_conditions=initial_condition) - - def test_error_initial_conditions_type(self): - model = pybamm.BaseModel() - u = pybamm.Variable("u") - model.rhs = {u: -u} - model.initial_conditions = {u: 1} - model.variables = {"u": u} - - disc = pybamm.Discretisation() - disc.process_model(model) - - solver = pybamm.IDAKLUSolver() - - initial_condition = "invalid_type" - - t_eval = np.linspace(0, 1, 10) - - with pytest.raises( - TypeError, match="Initial conditions must be dict or numpy array" - ): - solver.solve(model, t_eval, initial_conditions=initial_condition) - - def test_error_initial_conditions_count_mismatch(self): - model = pybamm.BaseModel() - u = pybamm.Variable("u") - model.rhs = {u: -u} - model.initial_conditions = {u: 1} - model.variables = {"u": u} - - disc = pybamm.Discretisation() - disc.process_model(model) - - solver = pybamm.IDAKLUSolver() - - initial_conditions = [{"u": 2}, {"u": 3}] - inputs = [{}, {}, {}] - - t_eval = np.linspace(0, 1, 10) - - with pytest.raises( - ValueError, - match="Number of initial conditions must match number of input sets", - ): - solver.solve( - model, t_eval, inputs=inputs, initial_conditions=initial_conditions - ) + with pytest.warns(pybamm.SolverWarning, match="not used in the model"): + solver.solve(model, t_eval, inputs=initial_condition) def test_interpolant_extrapolate(self): x = np.linspace(0, 2) From 005324069f0e6b51b739411eea373b431a7f908c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 16 Oct 2025 16:30:19 +0000 Subject: [PATCH 07/42] style: pre-commit fixes --- src/pybamm/solvers/base_solver.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pybamm/solvers/base_solver.py b/src/pybamm/solvers/base_solver.py index 9b88a76db6..18065e977d 100644 --- a/src/pybamm/solvers/base_solver.py +++ b/src/pybamm/solvers/base_solver.py @@ -970,9 +970,9 @@ def solve( # If the new initial conditions are different # and cannot be evaluated directly, set up again self.set_up(model, model_inputs_list, t_eval, ics_only=True) - self._model_set_up[model][ - "initial conditions" - ] = model.concatenated_initial_conditions + self._model_set_up[model]["initial conditions"] = ( + model.concatenated_initial_conditions + ) else: # Set the standard initial conditions self._set_initial_conditions(model, t_eval[0], model_inputs_list) From ad9eaa8a37719d2def2eadd3049b360510e03afb Mon Sep 17 00:00:00 2001 From: Pip Liggins Date: Fri, 24 Oct 2025 11:01:10 +0100 Subject: [PATCH 08/42] Update jax solver --- src/pybamm/solvers/jax_solver.py | 37 +++++++++++-------- tests/unit/test_solvers/test_jax_solver.py | 42 ++++++++++++++++++++-- 2 files changed, 61 insertions(+), 18 deletions(-) diff --git a/src/pybamm/solvers/jax_solver.py b/src/pybamm/solvers/jax_solver.py index 8bbbb98848..222557e2bb 100644 --- a/src/pybamm/solvers/jax_solver.py +++ b/src/pybamm/solvers/jax_solver.py @@ -99,8 +99,9 @@ def get_solve(self, model, t_eval): Returns ------- function - A function with signature `f(inputs)`, where inputs are a dict containing - any input parameters to pass to the model when solving + A function with signature `f(inputs, y0)`, where inputs are a dict containing + any input parameters to pass to the model when solving, and y0 is the initial state vector + (i.e. one entry from model.y0_list) """ if model not in self._cached_solves: @@ -127,8 +128,9 @@ def create_solve(self, model, t_eval): Returns ------- function - A function with signature `f(inputs)`, where inputs are a dict containing - any input parameters to pass to the model when solving + A function with signature `f(inputs, y0)`, where inputs are a dict containing + any input parameters to pass to the model when solving, and y0 is the initial state vector + (i.e. one entry from model.y0_list) """ if model.convert_to_format != "jax": @@ -148,8 +150,6 @@ def create_solve(self, model, t_eval): " end-time" ) - # Initial conditions, make sure they are an 0D array - y0 = jnp.array(model.y0).reshape(-1) mass = None if self.method == "BDF": mass = model.mass_matrix.entries.toarray() @@ -162,7 +162,9 @@ def rhs_dae(y, t, inputs): [model.rhs_eval(t, y, inputs), model.algebraic_eval(t, y, inputs)] ) - def solve_model_rk45(inputs): + def solve_model_rk45(inputs, y0): + # Initial conditions, make sure they are an 0D array + y0 = jnp.array(y0).reshape(-1) y = odeint( rhs_ode, y0, @@ -174,7 +176,9 @@ def solve_model_rk45(inputs): ) return jnp.transpose(y) - def solve_model_bdf(inputs): + def solve_model_bdf(inputs, y0): + # Initial conditions, make sure they are an 0D array + y0 = jnp.array(y0).reshape(-1) y = pybamm.jax_bdf_integrate( rhs_dae, y0, @@ -224,6 +228,8 @@ def _integrate(self, model, t_eval, inputs=None, t_interp=None, nproc=None): inputs = [inputs] inputs = inputs or [{}] + y0_list = model.y0_list + timer = pybamm.Timer() if model not in self._cached_solves: self._cached_solves[model] = self.create_solve(model, t_eval) @@ -233,12 +239,12 @@ def _integrate(self, model, t_eval, inputs=None, t_interp=None, nproc=None): if len(inputs) <= 1 or platform.startswith("cpu"): # cpu execution runs faster when multithreaded async def solve_model_for_inputs(): - async def solve_model_async(inputs_v): - return self._cached_solves[model](inputs_v) + async def solve_model_async(inputs_v, y0): + return self._cached_solves[model](inputs_v, y0) coro = [] - for inputs_v in inputs: - coro.append(asyncio.create_task(solve_model_async(inputs_v))) + for inputs_v, y0 in zip(inputs, y0_list, strict=False): + coro.append(asyncio.create_task(solve_model_async(inputs_v, y0))) return await asyncio.gather(*coro) y = asyncio.run(solve_model_for_inputs()) @@ -255,15 +261,16 @@ async def solve_model_async(inputs_v): inputs_v = { key: jnp.array([dic[key] for dic in inputs]) for key in inputs[0] } - y.extend(jax.vmap(self._cached_solves[model])(inputs_v)) + # TODO: not sure about this one - need to check that y0_list broadcasts correctly + y.extend(jax.vmap(self._cached_solves[model])(inputs_v, model.y0_list)) else: # Unknown platform, use serial execution as fallback print( f'Unknown platform requested: "{platform}", ' "falling back to serial execution" ) - for inputs_v in inputs: - y.append(self._cached_solves[model](inputs_v)) + for inputs_v, y0 in zip(inputs, y0_list, strict=False): + y.append(self._cached_solves[model](inputs_v, y0)) # This code block implements single-program multiple-data execution # using pmap across multiple XLAs. It is currently commented out diff --git a/tests/unit/test_solvers/test_jax_solver.py b/tests/unit/test_solvers/test_jax_solver.py index 4d93aa443a..a8bb6eeacd 100644 --- a/tests/unit/test_solvers/test_jax_solver.py +++ b/tests/unit/test_solvers/test_jax_solver.py @@ -109,7 +109,7 @@ def test_solver_sensitivities(self): # create a dummy "model" where we calculate the sum of the time series def solve_model(rate, solve=solve): - return jax.numpy.sum(solve({"rate": rate})) + return jax.numpy.sum(solve({"rate": rate}, model.y0_list[0])) # check answers with finite difference eval_plus = solve_model(rate + h) @@ -230,11 +230,11 @@ def test_get_solve(self): solver.solve(model, t_eval, inputs={"rate": 0.1}) solver = solver.get_solve(model, t_eval) - y = solver({"rate": 0.1}) + y = solver({"rate": 0.1}, model.y0_list[0]) np.testing.assert_allclose(y[0], np.exp(-0.1 * t_eval), rtol=1e-6, atol=1e-6) - y = solver({"rate": 0.2}) + y = solver({"rate": 0.2}, model.y0_list[0]) np.testing.assert_allclose(y[0], np.exp(-0.2 * t_eval), rtol=1e-6, atol=1e-6) @@ -273,3 +273,39 @@ def test_model_solver_multiple_inputs_jax_format(self, subtests): np.testing.assert_allclose( solution.y[0], np.exp(-0.01 * (i + 1) * solution.t) ) + + def test_model_solver_multiple_input_params_jax_format(self, subtests): + # Create model + model = pybamm.BaseModel() + model.convert_to_format = "jax" + u = pybamm.Variable("u") + v = pybamm.Variable("v") + u0 = pybamm.InputParameter("u0") + v0 = pybamm.InputParameter("v0") + model.rhs = {u: -u, v: -2 * v} + model.initial_conditions = {u: u0, v: v0} + model.variables = {"u": u, "v": v} + # create discretisation + mesh = get_mesh_for_testing() + spatial_methods = {"macroscale": pybamm.FiniteVolume()} + disc = pybamm.Discretisation(mesh, spatial_methods) + disc.process_model(model) + + solver = pybamm.JaxSolver(rtol=1e-8, atol=1e-8, method="RK45") + t_eval = np.linspace(0, 10, 100) + initial_conditions = [{"u0": 3, "v0": 4}, {"u0": 5, "v0": 6}] + + single_solutions = [] + for ic in initial_conditions: + sol = solver.solve(model, t_eval, inputs=ic) + single_solutions.append(sol) + + solutions = solver.solve(model, t_eval, inputs=initial_conditions, nproc=2) + for i, ic in enumerate(initial_conditions): + with subtests.test(i=i): + multi_sol = solutions[i] + np.testing.assert_equal(multi_sol["u"](0), ic["u0"]) + np.testing.assert_equal(multi_sol["v"](0), ic["v0"]) + + single_sol = single_solutions[i] + np.testing.assert_array_equal(single_sol.y, multi_sol.y) From ceee46c5f81409abad99d939eb7e0eddcd3cdbc4 Mon Sep 17 00:00:00 2001 From: Pip Liggins Date: Fri, 24 Oct 2025 11:36:38 +0100 Subject: [PATCH 09/42] Add a y0 property to BaseModel for backwards compatibility --- src/pybamm/models/base_model.py | 11 +++++++++++ tests/unit/test_solvers/test_jax_solver.py | 6 +++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/pybamm/models/base_model.py b/src/pybamm/models/base_model.py index 6cf682b2a2..3bfe6c4f16 100644 --- a/src/pybamm/models/base_model.py +++ b/src/pybamm/models/base_model.py @@ -468,6 +468,17 @@ def algebraic_root_solver(self): def algebraic_root_solver(self, algebraic_root_solver): self._algebraic_root_solver = algebraic_root_solver + @property + def y0(self): + if not hasattr(self, "y0_list") or self.y0_list is None: + return None + elif len(self.y0_list) == 1: + return self.y0_list[0] + else: + raise ValueError( + "Model contains multiple initial states. Access using y0_list instead." + ) + def get_parameter_info(self, by_submodel=False): """ Extracts the parameter information and returns it as a dictionary. diff --git a/tests/unit/test_solvers/test_jax_solver.py b/tests/unit/test_solvers/test_jax_solver.py index a8bb6eeacd..405b56416c 100644 --- a/tests/unit/test_solvers/test_jax_solver.py +++ b/tests/unit/test_solvers/test_jax_solver.py @@ -109,7 +109,7 @@ def test_solver_sensitivities(self): # create a dummy "model" where we calculate the sum of the time series def solve_model(rate, solve=solve): - return jax.numpy.sum(solve({"rate": rate}, model.y0_list[0])) + return jax.numpy.sum(solve({"rate": rate}, model.y0)) # check answers with finite difference eval_plus = solve_model(rate + h) @@ -230,11 +230,11 @@ def test_get_solve(self): solver.solve(model, t_eval, inputs={"rate": 0.1}) solver = solver.get_solve(model, t_eval) - y = solver({"rate": 0.1}, model.y0_list[0]) + y = solver({"rate": 0.1}, model.y0) np.testing.assert_allclose(y[0], np.exp(-0.1 * t_eval), rtol=1e-6, atol=1e-6) - y = solver({"rate": 0.2}, model.y0_list[0]) + y = solver({"rate": 0.2}, model.y0) np.testing.assert_allclose(y[0], np.exp(-0.2 * t_eval), rtol=1e-6, atol=1e-6) From ed97506532bab9954bf7dccc421479a649512db1 Mon Sep 17 00:00:00 2001 From: Pip Liggins Date: Fri, 24 Oct 2025 14:26:25 +0100 Subject: [PATCH 10/42] Test that events are caught all the way through an inputs list --- src/pybamm/solvers/base_solver.py | 10 +----- src/pybamm/solvers/jax_solver.py | 1 - tests/unit/test_solvers/test_base_solver.py | 31 +++++++++++++++++++ tests/unit/test_solvers/test_casadi_solver.py | 28 +++++++++++------ tests/unit/test_solvers/test_scipy_solver.py | 14 ++++++--- 5 files changed, 61 insertions(+), 23 deletions(-) diff --git a/src/pybamm/solvers/base_solver.py b/src/pybamm/solvers/base_solver.py index 18065e977d..066a2b2834 100644 --- a/src/pybamm/solvers/base_solver.py +++ b/src/pybamm/solvers/base_solver.py @@ -861,14 +861,6 @@ def solve( t_interp : None, list or ndarray, optional The times (in seconds) at which to interpolate the solution. Defaults to None. Only valid for solvers that support intra-solve interpolation (`IDAKLUSolver`). - initial_conditions : dict, numpy.ndarray, or list, optional - Override the model's default `y0`. Can be: - - - a dict mapping variable names → values - - a 1D array of length `n_states` - - a list of such overrides (one per parallel solve) - - Only valid for IDAKLU solver. Returns ------- :class:`pybamm.Solution` or list of :class:`pybamm.Solution` objects. @@ -1184,7 +1176,7 @@ def _check_events_with_initialization(t_eval, model, y0, inputs_dict): idxs = np.where(events_eval < 0)[0] event_names = [termination_events[idx].name for idx in idxs] raise pybamm.SolverError( - f"Events {event_names} are non-positive at initial conditions" + f"Events {event_names} are non-positive at initial conditions with inputs {inputs_dict}" ) def _set_sens_initial_conditions_from( diff --git a/src/pybamm/solvers/jax_solver.py b/src/pybamm/solvers/jax_solver.py index 222557e2bb..afbb36e9cf 100644 --- a/src/pybamm/solvers/jax_solver.py +++ b/src/pybamm/solvers/jax_solver.py @@ -261,7 +261,6 @@ async def solve_model_async(inputs_v, y0): inputs_v = { key: jnp.array([dic[key] for dic in inputs]) for key in inputs[0] } - # TODO: not sure about this one - need to check that y0_list broadcasts correctly y.extend(jax.vmap(self._cached_solves[model])(inputs_v, model.y0_list)) else: # Unknown platform, use serial execution as fallback diff --git a/tests/unit/test_solvers/test_base_solver.py b/tests/unit/test_solvers/test_base_solver.py index d0d3732df7..bd8effb049 100644 --- a/tests/unit/test_solvers/test_base_solver.py +++ b/tests/unit/test_solvers/test_base_solver.py @@ -2,6 +2,8 @@ # Tests for the Base Solver class # +import re + import casadi import numpy as np import pytest @@ -449,3 +451,32 @@ def test_on_extrapolation_and_on_failure_settings(self): ValueError, match="on_failure must be 'warn', 'raise', or 'ignore'" ): base_solver.on_failure = "invalid" + + @pytest.mark.parametrize("format", ["casadi", "python", "jax"]) + def test_events_fail_on_initialisation_multiple_input_params(self, format): + # Test that events fail on initialisation when multiple input parameters are used + # if it's not the first set of parameters + model = pybamm.BaseModel() + model.convert_to_format = format + u = pybamm.Variable("u") + u0 = pybamm.InputParameter("u0") + model.rhs = {u: -u} + model.initial_conditions = {u: u0} + model.events.append(pybamm.Event("test event", u - 0.2)) + model.variables = {"u": u} + + disc = pybamm.Discretisation() + disc.process_model(model) + + solver = pybamm.BaseSolver() + + initial_condition_input = [{"u0": 5}, {"u0": 0.1}] + t_eval = np.array([0, 1]) + + with pytest.raises( + pybamm.SolverError, + match=re.escape( + "Events ['test event'] are non-positive at initial conditions with inputs {'u0': 0.1}" + ), + ): + solver.solve(model, t_eval, initial_condition_input) diff --git a/tests/unit/test_solvers/test_casadi_solver.py b/tests/unit/test_solvers/test_casadi_solver.py index ba9ef0fa69..2aa2158f47 100644 --- a/tests/unit/test_solvers/test_casadi_solver.py +++ b/tests/unit/test_solvers/test_casadi_solver.py @@ -467,15 +467,25 @@ def test_model_solver_dae_inputs_in_initial_conditions(self): # 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_allclose( - solution.y.full()[0], 0.1 * np.exp(-solution.t), rtol=1e-6, atol=1e-5 - ) - np.testing.assert_allclose( - solution.y.full()[-1], 0.1 * np.exp(-solution.t), rtol=1e-6, atol=1e-5 - ) + inputs_list = [ + {"rate": -1, "ic 1": 0.1, "ic 2": 2}, + {"rate": -2, "ic 1": 0.2, "ic 2": 4}, + ] + solutions = solver.solve(model, t_eval, inputs=inputs_list) + for i, ipts in enumerate(inputs_list): + np.testing.assert_equal(solutions[i]["var1"](0), ipts["ic 1"]) + np.testing.assert_allclose( + solutions[i].y.full()[0], + ipts["ic 1"] * np.exp(ipts["rate"] * solutions[i].t), + rtol=1e-6, + atol=1e-5, + ) + np.testing.assert_allclose( + solutions[i].y.full()[-1], + ipts["ic 1"] * np.exp(ipts["rate"] * solutions[i].t), + rtol=1e-6, + atol=1e-5, + ) # Solve again with different initial conditions solution = solver.solve( diff --git a/tests/unit/test_solvers/test_scipy_solver.py b/tests/unit/test_solvers/test_scipy_solver.py index f66f9eec54..79b7cb900f 100644 --- a/tests/unit/test_solvers/test_scipy_solver.py +++ b/tests/unit/test_solvers/test_scipy_solver.py @@ -428,10 +428,16 @@ def test_model_solver_inputs_in_initial_conditions(self): # Solve solver = pybamm.ScipySolver(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}) - np.testing.assert_allclose( - solution.y[0], 0.1 * np.exp(-solution.t), rtol=1e-6, atol=1e-5 - ) + inputs_list = [{"rate": -1, "ic 1": 0.1}, {"rate": -2, "ic 1": 0.2}] + solutions = solver.solve(model, t_eval, inputs=inputs_list) + for i, ic in enumerate(inputs_list): + np.testing.assert_allclose( + solutions[i].y[0], + ic["ic 1"] * np.exp(ic["rate"] * solutions[i].t), + rtol=1e-6, + atol=1e-5, + ) + np.testing.assert_equal(solutions[i]["var1"](0), ic["ic 1"]) # Solve again with different initial conditions solution = solver.solve(model, t_eval, inputs={"rate": -0.1, "ic 1": 1}) From ba5d56b63bd7ad7d24e45815b9b5c0bf8b7f45b0 Mon Sep 17 00:00:00 2001 From: Pip Liggins Date: Fri, 24 Oct 2025 15:07:07 +0100 Subject: [PATCH 11/42] Make my zips strict --- src/pybamm/solvers/base_solver.py | 10 +++++----- src/pybamm/solvers/idaklu_solver.py | 4 ++-- src/pybamm/solvers/jax_solver.py | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/pybamm/solvers/base_solver.py b/src/pybamm/solvers/base_solver.py index 066a2b2834..a9d4c54723 100644 --- a/src/pybamm/solvers/base_solver.py +++ b/src/pybamm/solvers/base_solver.py @@ -798,7 +798,7 @@ def _integrate(self, model, t_eval, inputs_list=None, t_interp=None, nproc=1): y0_list, y0S_list, inputs_list, - strict=False, + strict=True, ), ) p.close() @@ -976,7 +976,7 @@ def solve( timer.reset() # Check initial conditions don't violate events - for y0, inpts in zip(model.y0_list, model_inputs_list, strict=False): + for y0, inpts in zip(model.y0_list, model_inputs_list, strict=True): self._check_events_with_initialization(t_eval, model, y0, inpts) # Process discontinuities @@ -1427,7 +1427,7 @@ def step( else: model.y0_list = [] - for soln, inputs in zip(old_solutions, model_inputs_list, strict=False): + for soln, inputs in zip(old_solutions, model_inputs_list, strict=True): _, concatenated_initial_conditions = model.set_initial_conditions_from( soln, return_type="ics" ) @@ -1447,7 +1447,7 @@ def step( self._set_consistent_initialization(model, t_start_shifted, model_inputs_list) # Check consistent initialization doesn't violate events - for y0, inpts in zip(model.y0_list, model_inputs_list, strict=False): + for y0, inpts in zip(model.y0_list, model_inputs_list, strict=True): self._check_events_with_initialization(t_eval, model, y0, inpts) # Step @@ -1480,7 +1480,7 @@ def step( ret = solutions else: ret = [ - old_s + s for (old_s, s) in zip(old_solutions, solutions, strict=False) + old_s + s for (old_s, s) in zip(old_solutions, solutions, strict=True) ] if len(ret) == 1: diff --git a/src/pybamm/solvers/idaklu_solver.py b/src/pybamm/solvers/idaklu_solver.py index f1fb8f1ed0..85881e0336 100644 --- a/src/pybamm/solvers/idaklu_solver.py +++ b/src/pybamm/solvers/idaklu_solver.py @@ -796,7 +796,7 @@ def handle_y0(y0): if model.len_rhs > 0: ydot0_list = [ self._rhs_dot_consistent_initialization(y0, model, time, inputs_dict) - for y0, inputs_dict in zip(y0_list, inputs_list, strict=False) + for y0, inputs_dict in zip(y0_list, inputs_list, strict=True) ] else: ydot0_list = [np.zeros_like(y0) for y0 in y0_list] @@ -807,7 +807,7 @@ def handle_y0(y0): y0full = [] ydot0full = [] for y0, ydot0, y0S, inputs_dict in zip( - y0_list, ydot0_list, y0S_list, inputs_list, strict=False + y0_list, ydot0_list, y0S_list, inputs_list, strict=True ): y0f, ydot0f = self._sensitivity_consistent_initialization( y0, ydot0, y0S, time, inputs_dict diff --git a/src/pybamm/solvers/jax_solver.py b/src/pybamm/solvers/jax_solver.py index afbb36e9cf..d7991f9dab 100644 --- a/src/pybamm/solvers/jax_solver.py +++ b/src/pybamm/solvers/jax_solver.py @@ -243,7 +243,7 @@ async def solve_model_async(inputs_v, y0): return self._cached_solves[model](inputs_v, y0) coro = [] - for inputs_v, y0 in zip(inputs, y0_list, strict=False): + for inputs_v, y0 in zip(inputs, y0_list, strict=True): coro.append(asyncio.create_task(solve_model_async(inputs_v, y0))) return await asyncio.gather(*coro) @@ -268,7 +268,7 @@ async def solve_model_async(inputs_v, y0): f'Unknown platform requested: "{platform}", ' "falling back to serial execution" ) - for inputs_v, y0 in zip(inputs, y0_list, strict=False): + for inputs_v, y0 in zip(inputs, y0_list, strict=True): y.append(self._cached_solves[model](inputs_v, y0)) # This code block implements single-program multiple-data execution From d06d9793a99be26a214f3dfd1ec113f71fbaad52 Mon Sep 17 00:00:00 2001 From: Pip Liggins Date: Fri, 24 Oct 2025 15:18:21 +0100 Subject: [PATCH 12/42] inputs to _integrate must be list[dict] --- src/pybamm/solvers/base_solver.py | 19 ++++++++++++------- src/pybamm/solvers/idaklu_solver.py | 11 ++++++++--- src/pybamm/solvers/jax_solver.py | 15 ++++++++++----- .../test_casadi_algebraic_solver.py | 6 +++--- 4 files changed, 33 insertions(+), 18 deletions(-) diff --git a/src/pybamm/solvers/base_solver.py b/src/pybamm/solvers/base_solver.py index a9d4c54723..d34493f08d 100644 --- a/src/pybamm/solvers/base_solver.py +++ b/src/pybamm/solvers/base_solver.py @@ -750,7 +750,14 @@ def _solve_process_calculate_sensitivities_arg( return calculate_sensitivities_list, sensitivities_have_changed - def _integrate(self, model, t_eval, inputs_list=None, t_interp=None, nproc=1): + def _integrate( + self, + model: pybamm.BaseModel, + t_eval, + inputs_list: list[dict] | None = None, + t_interp=None, + nproc=1, + ): """ Solve a DAE model defined by residuals with initial conditions y0. @@ -760,12 +767,10 @@ def _integrate(self, model, t_eval, inputs_list=None, t_interp=None, nproc=1): The model whose solution to calculate. t_eval : numeric type The times at which to compute the solution - inputs_list : list of dict + inputs_list : list of dict, optional Any input parameters to pass to the model when solving """ - if isinstance(inputs_list, dict): - inputs_list = [inputs_list] inputs_list = inputs_list or [{}] y0S_list = ( @@ -962,9 +967,9 @@ def solve( # If the new initial conditions are different # and cannot be evaluated directly, set up again self.set_up(model, model_inputs_list, t_eval, ics_only=True) - self._model_set_up[model]["initial conditions"] = ( - model.concatenated_initial_conditions - ) + self._model_set_up[model][ + "initial conditions" + ] = model.concatenated_initial_conditions else: # Set the standard initial conditions self._set_initial_conditions(model, t_eval[0], model_inputs_list) diff --git a/src/pybamm/solvers/idaklu_solver.py b/src/pybamm/solvers/idaklu_solver.py index 85881e0336..e125be2b45 100644 --- a/src/pybamm/solvers/idaklu_solver.py +++ b/src/pybamm/solvers/idaklu_solver.py @@ -576,7 +576,14 @@ def supports_parallel_solve(self): def options(self): return self._options - def _integrate(self, model, t_eval, inputs_list=None, t_interp=None, nproc=None): + def _integrate( + self, + model, + t_eval, + inputs_list: list[dict] | None = None, + t_interp=None, + nproc=None, + ): """ Overloads the _integrate method from BaseSolver to use the IDAKLU solver """ @@ -584,8 +591,6 @@ def _integrate(self, model, t_eval, inputs_list=None, t_interp=None, nproc=None) # Shouldn't ever reach this point raise pybamm.SolverError("Unsupported IDAKLU solver configuration.") - if isinstance(inputs_list, dict): - inputs_list = [inputs_list] inputs_list = inputs_list or [{}] # stack inputs so that they are a 2D array of shape (number_of_inputs, number_of_parameters) diff --git a/src/pybamm/solvers/jax_solver.py b/src/pybamm/solvers/jax_solver.py index d7991f9dab..a68ad5799f 100644 --- a/src/pybamm/solvers/jax_solver.py +++ b/src/pybamm/solvers/jax_solver.py @@ -204,7 +204,14 @@ def supports_parallel_solve(self): def requires_explicit_sensitivities(self): return False - def _integrate(self, model, t_eval, inputs=None, t_interp=None, nproc=None): + def _integrate( + self, + model, + t_eval, + inputs_list: list[dict] | None = None, + t_interp=None, + nproc=None, + ): """ Solve a model defined by dydt with initial conditions y0. @@ -214,7 +221,7 @@ def _integrate(self, model, t_eval, inputs=None, t_interp=None, nproc=None): The model whose solution to calculate. t_eval : :class:`numpy.array`, size (k,) The times at which to compute the solution - inputs : dict, list[dict], optional + inputs : list[dict], optional Any input parameters to pass to the model when solving Returns @@ -224,9 +231,7 @@ def _integrate(self, model, t_eval, inputs=None, t_interp=None, nproc=None): various diagnostic messages. """ - if isinstance(inputs, dict): - inputs = [inputs] - inputs = inputs or [{}] + inputs = inputs_list or [{}] y0_list = model.y0_list diff --git a/tests/unit/test_solvers/test_casadi_algebraic_solver.py b/tests/unit/test_solvers/test_casadi_algebraic_solver.py index f9775e44fc..e0483d5aca 100644 --- a/tests/unit/test_solvers/test_casadi_algebraic_solver.py +++ b/tests/unit/test_solvers/test_casadi_algebraic_solver.py @@ -89,13 +89,13 @@ def algebraic_eval(self, t, y, inputs): pybamm.SolverError, match="Could not find acceptable solution", ): - solver._integrate(model, np.array([0]), {}) + solver._integrate(model, np.array([0]), [{}]) solver = pybamm.CasadiAlgebraicSolver(extra_options={"error_on_fail": False}) with pytest.raises( pybamm.SolverError, match="Could not find acceptable solution: solver terminated", ): - solver._integrate(model, np.array([0]), {}) + solver._integrate(model, np.array([0]), [{}]) # Model returns Nan class NaNModel: @@ -118,7 +118,7 @@ def algebraic_eval(self, t, y, inputs): pybamm.SolverError, match="Could not find acceptable solution: solver returned NaNs", ): - solver._integrate(model, np.array([0]), {}) + solver._integrate(model, np.array([0]), [{}]) def test_model_solver_with_time(self): # Create model From 2f97cc725efe52bca68ebc8642c7ec027ec15259 Mon Sep 17 00:00:00 2001 From: Pip Liggins Date: Mon, 27 Oct 2025 10:59:52 +0000 Subject: [PATCH 13/42] Refactor _integrate_single and add test for error --- src/pybamm/solvers/algebraic_solver.py | 6 +++++- src/pybamm/solvers/base_solver.py | 19 ++++++++++--------- src/pybamm/solvers/casadi_algebraic_solver.py | 8 ++++++-- src/pybamm/solvers/casadi_solver.py | 8 ++++++-- src/pybamm/solvers/dummy_solver.py | 6 +++++- src/pybamm/solvers/scipy_solver.py | 6 +++++- tests/unit/test_solvers/test_base_solver.py | 10 ++++++++++ 7 files changed, 47 insertions(+), 16 deletions(-) diff --git a/src/pybamm/solvers/algebraic_solver.py b/src/pybamm/solvers/algebraic_solver.py index 3daf29bdcc..ee029d3fdb 100644 --- a/src/pybamm/solvers/algebraic_solver.py +++ b/src/pybamm/solvers/algebraic_solver.py @@ -78,7 +78,7 @@ def tol(self): def tol(self, value): self._tol = value - def _integrate_single(self, model, t_eval, y0, y0S, inputs_dict, inputs_list=None): + def _integrate_single(self, model, t_eval, inputs_dict, y0, y0S): """ Calculate the solution of the algebraic equations through root-finding @@ -90,6 +90,10 @@ def _integrate_single(self, model, t_eval, y0, y0S, inputs_dict, inputs_list=Non The times at which to compute the solution inputs_dict : dict, optional Any input parameters to pass to the model when solving + y0 : array-like + The initial conditions for the model + y0S : array-like + The initial sensitivities for the model """ inputs_dict = inputs_dict or {} if model.convert_to_format == "casadi": diff --git a/src/pybamm/solvers/base_solver.py b/src/pybamm/solvers/base_solver.py index d34493f08d..8ed9e82ac4 100644 --- a/src/pybamm/solvers/base_solver.py +++ b/src/pybamm/solvers/base_solver.py @@ -784,10 +784,9 @@ def _integrate( new_solution = self._integrate_single( model, t_eval, + inputs_list[0], model.y0_list[0], y0S_list[0], - inputs_list[0], - inputs_list=inputs_list, ) new_solutions = [new_solution] else: @@ -800,9 +799,9 @@ def _integrate( zip( model_list, t_eval_list, + inputs_list, y0_list, y0S_list, - inputs_list, strict=True, ), ) @@ -811,9 +810,9 @@ def _integrate( return new_solutions - def _integrate_single(self, model, t_eval, y0, y0S, inputs_dict, inputs_list=None): + def _integrate_single(self, model, t_eval, inputs_dict, y0, y0S): """ - Solve a single batch for the DAE model defined by residuals with initial conditions y0. + Solve a single model instance with initial conditions y0. Parameters ---------- @@ -821,12 +820,14 @@ def _integrate_single(self, model, t_eval, y0, y0S, inputs_dict, inputs_list=Non The model whose solution to calculate. t_eval : numeric type The times at which to compute the solution - inputs_list : list of dict, optional + inputs_dict : dict, optional Any input parameters to pass to the model when solving - inputs : array, optional - The input parameters in array form, to pass to the model when solving + y0 : array-like + The initial conditions for the model + y0S : array-like + The initial sensitivities for the model """ - raise NotImplementedError + raise NotImplementedError("BaseSolver does not implement _integrate_single.") def solve( self, diff --git a/src/pybamm/solvers/casadi_algebraic_solver.py b/src/pybamm/solvers/casadi_algebraic_solver.py index c7e6bfd7fc..da3df29518 100644 --- a/src/pybamm/solvers/casadi_algebraic_solver.py +++ b/src/pybamm/solvers/casadi_algebraic_solver.py @@ -151,7 +151,7 @@ def set_up_root_solver(self, model, inputs_dict, t_eval): pybamm.logger.info(f"Finish building {self.name}") - def _integrate_single(self, model, t_eval, y0, y0S, inputs_dict, inputs_list=None): + def _integrate_single(self, model, t_eval, inputs_dict, y0, y0S): """ Calculate the solution of the algebraic equations through root-finding @@ -162,7 +162,11 @@ def _integrate_single(self, model, t_eval, y0, y0S, inputs_dict, inputs_list=Non t_eval : :class:`numpy.array`, size (k,) The times at which to compute the solution inputs_dict : dict, optional - Any input parameters to pass to the model when solving. + Any input parameters to pass to the model when solving + y0 : array-like + The initial conditions for the model + y0S : array-like + The initial sensitivities for the model """ # Record whether there are any symbolic inputs inputs_dict = inputs_dict or {} diff --git a/src/pybamm/solvers/casadi_solver.py b/src/pybamm/solvers/casadi_solver.py index 6cb64d7431..1862abaf16 100644 --- a/src/pybamm/solvers/casadi_solver.py +++ b/src/pybamm/solvers/casadi_solver.py @@ -140,9 +140,9 @@ def __init__( pybamm.citations.register("Andersson2019") - def _integrate_single(self, model, t_eval, y0, y0S, inputs_dict, inputs_list=None): + def _integrate_single(self, model, t_eval, inputs_dict, y0, y0S): """ - Solve a DAE model defined by residuals with initial conditions y0. + Solve a single DAE model defined by residuals with initial conditions y0. Parameters ---------- @@ -152,6 +152,10 @@ def _integrate_single(self, model, t_eval, y0, y0S, inputs_dict, inputs_list=Non The times at which to compute the solution inputs_dict : dict, optional Any input parameters to pass to the model when solving + y0 : array-like + The initial conditions for the model + y0S : array-like + The initial sensitivities for the model """ # casadi solver does not support sensitivity analysis diff --git a/src/pybamm/solvers/dummy_solver.py b/src/pybamm/solvers/dummy_solver.py index 65479edd1a..ed9f0f3d3f 100644 --- a/src/pybamm/solvers/dummy_solver.py +++ b/src/pybamm/solvers/dummy_solver.py @@ -13,7 +13,7 @@ def __init__(self): super().__init__() self.name = "Dummy solver" - def _integrate_single(self, model, t_eval, y0, y0S, inputs_dict, inputs_list=None): + def _integrate_single(self, model, t_eval, inputs_dict, y0, y0S): """ Solve an empty model. @@ -23,6 +23,10 @@ def _integrate_single(self, model, t_eval, y0, y0S, inputs_dict, inputs_list=Non The model whose solution to calculate. t_eval : :class:`numpy.array`, size (k,) The times at which to compute the solution + y0 : array-like + The initial conditions for the model + y0S : array-like + The initial sensitivities for the model inputs_dict : dict, optional Any input parameters to pass to the model when solving diff --git a/src/pybamm/solvers/scipy_solver.py b/src/pybamm/solvers/scipy_solver.py index 81b6fbd5b3..b92134507b 100644 --- a/src/pybamm/solvers/scipy_solver.py +++ b/src/pybamm/solvers/scipy_solver.py @@ -52,7 +52,7 @@ def __init__( self.name = f"Scipy solver ({method})" pybamm.citations.register("Virtanen2020") - def _integrate_single(self, model, t_eval, y0, y0S, inputs_dict, inputs_list=None): + def _integrate_single(self, model, t_eval, inputs_dict, y0, y0S): """ Solve a model defined by dydt with initial conditions y0. @@ -64,6 +64,10 @@ def _integrate_single(self, model, t_eval, y0, y0S, inputs_dict, inputs_list=Non The times at which to compute the solution inputs_dict : dict, optional Any input parameters to pass to the model when solving + y0 : array-like + The initial conditions for the model + y0S : array-like + The initial sensitivities for the model Returns ------- diff --git a/tests/unit/test_solvers/test_base_solver.py b/tests/unit/test_solvers/test_base_solver.py index bd8effb049..27939b9254 100644 --- a/tests/unit/test_solvers/test_base_solver.py +++ b/tests/unit/test_solvers/test_base_solver.py @@ -480,3 +480,13 @@ def test_events_fail_on_initialisation_multiple_input_params(self, format): ), ): solver.solve(model, t_eval, initial_condition_input) + + def test_integrate_single_error(self): + solver = pybamm.BaseSolver() + model = pybamm.BaseModel() + + with pytest.raises( + NotImplementedError, + match="BaseSolver does not implement _integrate_single.", + ): + solver._integrate_single(model, np.array([0, 1]), {}, np.array([1]), None) From 4a4b5fb08ac9e996eb7d9b37773981d39afdacc4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 27 Oct 2025 11:00:23 +0000 Subject: [PATCH 14/42] style: pre-commit fixes --- src/pybamm/solvers/base_solver.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pybamm/solvers/base_solver.py b/src/pybamm/solvers/base_solver.py index 8ed9e82ac4..ecd7042081 100644 --- a/src/pybamm/solvers/base_solver.py +++ b/src/pybamm/solvers/base_solver.py @@ -968,9 +968,9 @@ def solve( # If the new initial conditions are different # and cannot be evaluated directly, set up again self.set_up(model, model_inputs_list, t_eval, ics_only=True) - self._model_set_up[model][ - "initial conditions" - ] = model.concatenated_initial_conditions + self._model_set_up[model]["initial conditions"] = ( + model.concatenated_initial_conditions + ) else: # Set the standard initial conditions self._set_initial_conditions(model, t_eval[0], model_inputs_list) From d40cec3ad405e6ce483d89600851ccfb2a755c4b Mon Sep 17 00:00:00 2001 From: Pip Liggins Date: Mon, 27 Oct 2025 11:29:38 +0000 Subject: [PATCH 15/42] Fix example script error --- .../notebooks/solvers/dae-solver.ipynb | 52 +++++++------------ src/pybamm/solvers/idaklu_solver.py | 2 +- 2 files changed, 20 insertions(+), 34 deletions(-) diff --git a/docs/source/examples/notebooks/solvers/dae-solver.ipynb b/docs/source/examples/notebooks/solvers/dae-solver.ipynb index 40945253f5..c6a77738dd 100644 --- a/docs/source/examples/notebooks/solvers/dae-solver.ipynb +++ b/docs/source/examples/notebooks/solvers/dae-solver.ipynb @@ -11,20 +11,9 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m A new release of pip is available: \u001b[0m\u001b[31;49m24.1\u001b[0m\u001b[39;49m -> \u001b[0m\u001b[32;49m24.2\u001b[0m\n", - "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m To update, run: \u001b[0m\u001b[32;49mpip install --upgrade pip\u001b[0m\n", - "Note: you may need to restart the kernel to use updated packages.\n" - ] - } - ], + "outputs": [], "source": [ "%pip install \"pybamm[plot,cite]\" -q # install PyBaMM if it is not installed\n", "import os\n", @@ -53,7 +42,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 10, "metadata": {}, "outputs": [], "source": [ @@ -80,12 +69,12 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 11, "metadata": {}, "outputs": [ { "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAABQkAAAGGCAYAAADYVwfrAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/TGe4hAAAACXBIWXMAAA9hAAAPYQGoP6dpAACPC0lEQVR4nOzdd3hUZfrG8e/MpJNMCKSHSEI39CJNUdHQRBAb4KoUUVfXhugquEqxof4s6IqirAjYAFERBWNBERQEAVF6DTUNCMmQhBQy8/tjYCCSkAkkOSn357rmijnzzjnPGXbx9jnnvK/J4XA4EBERERERERERkVrLbHQBIiIiIiIiIiIiYiw1CUVERERERERERGo5NQlFRERERERERERqOTUJRUREREREREREajk1CUVERERERERERGo5NQlFRERERERERERqOTUJRUREREREREREajk1CUVERERERERERGo5D6MLcIfdbicpKYmAgABMJpPR5YiIiIhUaQ6Hg2PHjhEZGYnZXPOuCSsbioiIiLjP3WxYLZqESUlJREdHG12GiIiISLWyf/9+GjRoYHQZ5U7ZUERERKTsSsuG1aJJGBAQADhPxmq1GlyNiIiISNVms9mIjo52ZaiaRtlQRERExH3uZsNq0SQ89RiJ1WpVEBQRERFxU019FFfZUERERKTsSsuGNW+SGhERERERERERESkTNQlFRERERERERERqOTUJRUREREREREREarlqMSehiIiIVKzCwkIKCgqMLkPc5OnpicViMboMERERqaKU7WqX8sqGahKKiIjUYg6Hg5SUFDIyMowuRcqobt26hIeH19jFSURERKTslO1qr/LIhmoSioiI1GKnQmRoaCh+fn5qOFUDDoeDnJwc0tLSAIiIiDC4IhEREakqlO1qn/LMhmoSioiI1FKFhYWuEFm/fn2jy5Ey8PX1BSAtLY3Q0FA9eiwiIiLKdrVYeWXDMi9csmzZMgYMGEBkZCQmk4kFCxaU+pmlS5fSoUMHvL29adKkCTNnzjyPUiuBvRASl8OG+c6f9kKjKxIREakwp+ap8fPzM7gSOR+n/twqar6hyZMnc8kllxAQEEBoaCiDBg1i27ZtpX7u008/pUWLFvj4+NC6dWsWL15c5H2Hw8H48eOJiIjA19eX+Ph4duzYUSHncEGUC0VEpJpRtqvdyiMblrlJmJ2dTdu2bZk6dapb4xMTE+nfvz89e/Zk/fr1jB49mjvvvJNvv/22zMVWqM0LYUormHUtfDbK+XNKK+d2ERGRGkyPoVRPFf3n9vPPP3Pffffx22+/8f3331NQUEDv3r3Jzs4u8TMrVqzglltuYdSoUfzxxx8MGjSIQYMGsXHjRteYl156iTfeeINp06axatUq6tSpQ58+fcjNza3Q8ykT5UIREanGlO1qp/L4czc5HA7HhRTwxRdfMGjQoBLHPP744yxatKhIOBw6dCgZGRkkJCS4dRybzUZgYCCZmZlYrdbzLbdkmxfCvGHA37+Kk1/w4NkQN7D8jysiImKg3NxcEhMTiY2NxcfHx+hypIzO9edXEdnp0KFDhIaG8vPPP3P55ZcXO2bIkCFkZ2fz9ddfu7Z17dqVdu3aMW3aNBwOB5GRkTzyyCM8+uijAGRmZhIWFsbMmTMZOnSoW7VUaDZULhQRkWpK2a52K49sWOY7Cctq5cqVxMfHF9nWp08fVq5cWdGHdo+9EBIe5+wgyOltCWP1iImIiEgtdPvtt/P8889X+nGHDh3KK6+8UunHPZfMzEwA6tWrV+KY0nJfYmIiKSkpRcYEBgbSpUuXqpENlQtFRESkBPn5+TRp0oQVK1ZU+nFjYmJYs2ZNhR+rwpuEKSkphIWFFdkWFhaGzWbj+PHjxX4mLy8Pm81W5FVh9q4AW9I5BjjAdtA5TkRERGqNP//8k8WLF/Pggw+WOCY9PZ0HHniA5s2b4+vry0UXXcSDDz7oaqidaebMmWfNy7x06VJMJhMZGRlFtj/55JM899xzxe7HCHa7ndGjR3PppZfSqlWrEseVlPtSUlJc75/aVtKY4lRaNlQuFBERqXTuzoO8Z88eRowYUfkFnjRt2jRiY2Pp3r17iWP+/PNPbrnlFqKjo/H19eXiiy/m9ddfL3bsiBEj2LNnT5FtEydOpF27dkW2eXl58eijj/L4449f6CmUqsKbhOdj8uTJBAYGul7R0dEVd7Cs1PIdJyIiIjXCf//7X26++Wb8/f1LHJOUlERSUhIvv/wyGzduZObMmSQkJDBq1CjXmNdee41jx465fj927BivvfbaOY/dqlUrGjduzIcffnjhJ1IO7rvvPjZu3MicOXMMOX6lZUPlQhERkUpX2jzIH330Ebt27XKNdzgcTJ06laNHj1ZajQ6HgzfffLNIxivO2rVrCQ0N5cMPP2TTpk385z//Ydy4cbz55puA8wLz1KlTOXPmv127dvHRRx+dc7+33norv/zyC5s2bbrwkzmHCm8ShoeHk5paNEilpqZitVpdSzT/3bhx48jMzHS99u/fX3EF+oeVPqYs40RERKTC2e12Jk+eTGxsLL6+vrRt25b58+fjcDiIj4+nT58+rvCVnp5OgwYNGD9+PHD67r1FixbRpk0bfHx86Nq1a5H5kwsLC5k/fz4DBgw4Zx2tWrXis88+Y8CAATRu3JirrrqK5557jq+++ooTJ04AEBQURK9evfjll1/45Zdf6NWrF0FBQezZs4eePXu6xphMpiJXxwcMGGBYU+5M999/P19//TU//fQTDRo0OOfYknJfeHi46/1T20oaU5xKy4bKhSIiIpUuISGBESNG0LJlS9q2bcvMmTPZt28fa9euBSA2Npbhw4czbdo0Dhw4QN++fTl48CDe3t4AZGRkcOeddxISEoLVauWqq67izz//BJxzKoeHhxeZPmbFihV4eXmxZMkS4PTde++88w7R0dH4+fkxePDgIk90rF27ll27dtG/f/9znssdd9zB66+/zhVXXEGjRo247bbbGDlyJJ9//jkAPj4+HDx4kL59+3LgwAGmTZvGiBEjiI2NZebMmUyaNIk///wTk8mEyWRyPYUSFBTEpZdeWuHZ0KNC9w5069aNxYsXF9n2/fff061btxI/4+3t7frDrnANu4M1EmzJFD//jMn5fsOSbycVERGpCRwOB8cLjJlrzdfTUqYV2SZPnsyHH37ItGnTaNq0KcuWLeO2224jJCSEWbNm0bp1a9544w0eeugh7rnnHqKiolxNwlP+/e9/8/rrrxMeHs4TTzzBgAED2L59O56envz1119kZmbSqVOnMp/LqQmhPTycMWvEiBFcddVVdO7cGYDVq1dz0UUXUVhYyGeffcaNN97Itm3bzrqA2rlzZ5577jny8vIqLxedweFw8MADD/DFF1+wdOlSYmNjS/1Mt27dWLJkCaNHj3ZtOzP3xcbGEh4ezpIlS1yP0thsNlatWsW9995b4n4rLRuWkgsdmDApF4qISDVRnbLdmf4+D3L37t356aefiI+P59dff+Wrr76iX79+rvE333wzvr6+fPPNNwQGBvLOO+9w9dVXs337dkJCQpgxYwaDBg2id+/eNG/enNtvv53777+fq6++2rWPnTt3Mm/ePL766itsNhujRo3iX//6l+sOv+XLl9OsWTMCAgLO63xOnYufnx/PP/88ixcvZuDAgZw4cYIff/wRT09P2rdvz8aNG0lISOCHH34AnHM3n9K5c2eWL19e5uOXRZmbhFlZWezcudP1e2JiIuvXr6devXpcdNFFjBs3joMHDzJ79mwA7rnnHt58800ee+wx7rjjDn788UfmzZvHokWLyu8sLoTZAn1fPLmKnYkzA6EDk3Mdu74vOMeJiIjUYMcLCokb/60hx978dB/8vNyLJXl5eTz//PP88MMPruZTo0aN+OWXX3jnnXf4+OOPeeeddxg2bBgpKSksXryYP/74w9W0O2XChAn06tULgFmzZtGgQQO++OILBg8ezN69e7FYLISGhpbpPA4fPswzzzzD3Xff7dr24Ycf8uabb7quPA8ePJj777+f2267zRUYQ0NDqVu3bpF9RUZGkp+fT0pKCg0bNixTHeXhvvvu4+OPP+bLL78kICDANWdgYGCgq5k5bNgwoqKimDx5MgAPPfQQV1xxBa+88gr9+/dnzpw5rFmzhnfffRcAk8nE6NGjefbZZ2natCmxsbE89dRTREZGMmjQoEo/x7OcIxfaHWAyoVwoIiLVRnXJdmcqbh7kVatW8e9//5vu3bvj6enJlClTWLlyJU888QRr1qxh9erVpKWluS4ovvzyyyxYsID58+dz9913c80113DXXXdx66230qlTJ+rUqePKLqfk5uYye/ZsoqKiAOe0M/379+eVV14hPDycvXv3EhkZWebzWbFiBXPnznX1wHJzc3n++edZtWoVV155JZ06dSI+Pp7/+7//o3Pnzvj7++Ph4VHsExaRkZHs3bu3zDWURZkfN16zZg3t27enffv2AIwZM4b27du7rs4nJyezb98+1/jY2FgWLVrE999/T9u2bXnllVf43//+R58+fcrpFMpB3EAYPBusEUU2p5nqY795lvN9ERERqRJ27txJTk4OvXr1wt/f3/WaPXu2a76am2++meuvv54XXniBl19+maZNm561nzOfaqhXrx7Nmzdny5YtABw/fhxvb+8iV8Cff/75Isc7M++A8464/v37ExcXx8SJE13b09LS+P777+nRowc9evTg+++/Jy0trdTzPNWIy8nJcf/LKUdvv/02mZmZXHnllURERLhec+fOdY3Zt28fycnJrt+7d+/Oxx9/zLvvvut6BHzBggVFFjt57LHHeOCBB7j77ru55JJLyMrKIiEhAR8fn0o9vxKVkAtTqM/r9Z9SLhQREalAxc2DvGPHDt5//33uueceGjRoQEJCAmFhYeTk5PDnn3+SlZVF/fr1i+S0xMTEIvMYvvzyy5w4cYJPP/2Ujz766KwnFC666CJXgxCcOdFut7sWUDl+/PhZWaVfv36u47Vs2fKsc9m4cSPXXXcdEyZMoHfv3oAz14WFhZGQkECDBg245557mDFjBtu3by/1u/H19a3wXFjmtu6VV15ZZILFv/v7qn2nPvPHH3+U9VCVK24gtOgPe1eQezSJfy08yNLjTXmbS6hC7UwREZEK4+tpYfPTxvxbz9fT/TuzsrKyAFi0aFGRMAe4Al9OTg5r167FYrGwY8eOMtcTHBxMTk4O+fn5eHl5Ac6nIwYPHuwac+bV5GPHjtG3b18CAgL44osv8PT0dL03ZsyYIvsOCAg4a1tx0tPTAQgJCSlz/eXhXHnvlKVLl5617eabb+bmm28u8TMmk4mnn36ap59++kLKq1hn5EKyUjlEXa74JJeCgybiD2bSKiqw9H2IiIgYrLpku1NOzYO8bNmyIvMg33bbbQCulYBNJhP33Xcf4MyFERERxWaSM5/S2LVrF0lJSdjtdvbs2UPr1q3LVFtwcDAbNmwosu1///sfx48fByiS/QA2b97M1Vdfzd13382TTz7p2l6vXj1X7ac0btyYxo0bl1pDenp6hefCCp+TsFoxWyC2Bz6x0CJtKz8u3cX0Zbvp07LkibRFRERqCpPJdF6PhVS2uLg4vL292bdvH1dccUWxYx555BHMZjPffPMN11xzDf379+eqq64qMua3337joosuAuDo0aNs376diy++GMA1X97mzZtd/1yvXj3X48Fnstls9OnTB29vbxYuXFjiHXFnLkpyyqkGZGHh2fMFbdy4kQYNGhAcHFzs/qSCncyFACFAv41/sPDPJP63fDdThrY3tjYRERE3VJds5+48yDExMWfdmNahQwdSUlLw8PAgJiam2M/l5+dz2223MWTIEJo3b86dd97Jhg0bikwrs2/fPpKSklwXgX/77TfMZjPNmzcHoH379rz99ts4HA7XkyZ/v1h9yqZNm7jqqqsYPnw4zz33XInnXdxNdl5eXsXmQnBmw1NP9VaUCl/duLoa0T0GL4uZNXuPsnZv5S2rLSIiIucWEBDAo48+ysMPP8ysWbPYtWsX69at47///S+zZs1i0aJFzJgxg48++ohevXrx73//m+HDh3P0aNF/nz/99NMsWbKEjRs3MmLECIKDg13z4oWEhNChQwd++eWXc9Zis9no3bs32dnZvPfee9hsNlJSUkhJSSkx4J2pYcOGmEwmvv76aw4dOuS6SxKcE2SfejRFjHf35Y0A+OqvZJIyjhtcjYiISM1x33338eGHH/Lxxx+75kFOSUlx3aV3LvHx8XTr1o1Bgwbx3XffsWfPHlasWMF//vMf1qxZA8B//vMfMjMzeeONN3j88cdp1qwZd9xxR5H9+Pj4MHz4cP7880+WL1/Ogw8+yODBg11zA/bs2ZOsrCw2bdp0zno2btxIz5496d27N2PGjHGdy6FDh9z6LmJiYlxrfxw+fJi8vDzXe5WRDdUkLEGo1YdB7Z0d5P8t321wNSIiInKmZ555hqeeeorJkydz8cUX07dvXxYtWkRMTAyjRo1i4sSJdOjQAYBJkyYRFhbGPffcU2QfL7zwAg899BAdO3YkJSWFr776ynVnH8Cdd97pWtGuJOvWrWPVqlVs2LCBJk2aFJm7b//+/aWeR1RUFJMmTWLs2LGEhYVx//33A85JrRcsWMBdd91V1q9GKkirqEC6N65Pod3B+78mGl2OiIhIjeHOPMglMZlMLF68mMsvv5yRI0fSrFkzhg4dyt69ewkLC2Pp0qVMmTKFDz74AKvVitls5oMPPmD58uW8/fbbrv00adKEG264gWuuuYbevXvTpk0b3nrrLdf79evX5/rrry81G86fP59Dhw7x4YcfFjmXSy65xK3v4sYbb6Rv37707NmTkJAQPvnkEwBWrlxJZmYmN910k1v7OV8mhzsTzhjMZrMRGBhIZmYmVqu10o67PfUYvV9bhskEPz1yJTHBdSrt2CIiIhUtNzeXxMREYmNjq86iEZVg6dKl9OzZk6NHj561ovCZjh8/TvPmzZk7d26RRU4qw9tvv80XX3zBd999V+KYc/35GZWdKotR5/fTtjRGvv87/t4erBh3FVYfz9I/JCIiUklqa7a7UBMnTmTBggWsX7/+nOP++usvevXqxa5du/D396+c4k4aMmQIbdu25YknnihxTHlkQ91JeA7NwgLo2TwEhwPe+0VXjEVERGoTX19fZs+ezeHDhyv92J6envz3v/+t9OPKuV3ZLISmof5k5Z3gk1X7Sv+AiIiI1Bht2rThxRdfJDGxcvtD+fn5tG7dmocffrjCj6UmYSnuOjn/zKdr95OenW9wNSIiIlKZrrzySgYMGFDpx73zzjtdE2VL1WEymVzZ8P1f95B/wm5wRSIiIlKZRowYUeaVkS+Ul5cXTz75JL6+vhV+LDUJS9GtUX1aRVnJLbAze+Ueo8sRERGRC3TllVficDjO+aixSEmuaxdJSIA3KbZcFv6ZZHQ5IiIicoEmTpxY6qPGtYWahKUwmUzcfXljAGau2EN23gmDKxIRERERo3h7WBh5aQwA037ehd1e5af3FhEREXGLmoRu6N86gpj6fmTkFPDJas0/IyIiIlKb3da1IQE+HuxMy+K7zalGlyMiIiJSLtQkdIPFbOKfVzjvJvzf8kTyThQaXJGIiIiIGMXq48mwbg0BeHvpThwO3U0oIiIi1Z+ahG66oUMUYVbn/DNfrDtodDkiIiIiYqCRl8bi42nmzwOZ/LrziNHliIiIiFwwNQnd5O1h4a4eztXspv28i0LNPyMiIiJSawX7ezP0kosAeGvpToOrEREREblwahKWwS2dL6Kunyd7juSweEOy0eWIiIiIiIHuurwRHmYTK3Yd4Y99R40uR0REROSCqElYBnW8PRjRPQaAt5bu0vwzIiIiIrVYVF1fBrWPApzZUERERKQ6U5OwjEZ0j8HPy8KWZBtLtx0yuhwRERHj2QshcTlsmO/8adcCX1J73HNFY0wm+H5zKttSjhldjoiIyIVTtqu11CQso7p+XtzW1bmaneafERGRWm/zQpjSCmZdC5+Ncv6c0sq5vYLExMQwZcqUItvatWvHxIkTK+yYIiVpEupPv1bhgHPeahERkWrNgGz37rvvEhkZid1uL7L9uuuu44477qiw48rZ1CQ8D6Mui8XLYub3PUdZnZhudDkiIiLG2LwQ5g0DW1LR7bZk5/YKDJMiVcm/rmwCwMI/k9ifnmNwNSIiIufJoGx38803c+TIEX766SfXtvT0dBISErj11lsr5JhSPDUJz0OY1YebOjUAdDehiIjUUvZCSHgcKG5+3pPbEsbq8RSpFVpFBXJ5sxAK7Q7eWaa7CUVEpBoyMNsFBQXRr18/Pv74Y9e2+fPnExwcTM+ePcv9eFIyNQnP0z8vb4TZBEu3HWLjwUyjyxEREalce1ecfZW5CAfYDjrHidQC/7qyMQDz1hwgzZZrcDUiIiJlZHC2u/XWW/nss8/Iy8sD4KOPPmLo0KGYzWpbVSZ92+epYf06DGgbCcCbP+puQhERqWWyUst3XBmYzWYcjqJXuQsKCsr9OCJl0SW2Hh0bBpF/ws67y3YbXY6IiEjZGJjtAAYMGIDD4WDRokXs37+f5cuX61FjA6hJeAHu79kEkwkSNqWwJdlmdDkiIiKVxz+sfMeVQUhICMnJya7fbTYbiYmJ5X4ckbIwmUw8cJVzbsIPV+3l0LE8gysSEREpAwOzHYCPjw833HADH330EZ988gnNmzenQ4cOFXIsKZmahBegaVgA17SOAOC/P+4wuBoREZFK1LA7WCMBUwkDTGCNco4rZ1dddRUffPABy5cvZ8OGDQwfPhyLxVLuxxEpqyuahdA2ui65BXamL9fdhCIiUo0YmO1OufXWW1m0aBEzZszQXYQGUZPwAj14VVMAFm9IYVvKMYOrERERqSRmC/R98eQvfw+TJ3/v+4JzXDkbN24cV1xxBddeey39+/dn0KBBNG7cuNyPI1JWJpOJ0Vc7s+EHK/dyOEt3E4qISDVhYLY75aqrrqJevXps27aNf/zjHxV2HCmZmoQXqHl4AP1ahQPwhu4mFBGR2iRuIAyeDdaIotutkc7tcQMr5LBWq5U5c+aQmZnJvn37GD58OOvXr2fixIkVcjyRsriyeQhtGgRyvKBQdxOKiEj1YlC2O8VsNpOUlITD4aBRo0YVeiwpnofRBdQED17dlG82prB4QzI7Uo/RNCzA6JJEREQqR9xAaNHfudJdVqpznpqG3Sv0KrNIVWYymXjwqqbcOXsNH6zcyz8vb0y9Ol5GlyUiIuIeZbtaTXcSloOLI6z0aRmGwwFvaKVjERGpbcwWiO0BrW9y/lSIlFru6otDaRVlJSdfdxOKiEg1pGxXa6lJWE4ePDn/zNd/JbEzTXMTioiIiNRWp+4mBJi9Yg9Hs/MNrkhERESkdGoSlpOWkYH0inPeTfim7iYUERGRGmDZsmUMGDCAyMhITCYTCxYsOOf4ESNGYDKZznq1bNnSNWbixIlnvd+iRYsKPpPK1ysujLgIK9n5hbz3S6LR5YiIiIiUSk3CcvTQqbsJ/zxA0vrvYMN8SFwO9kKDKxMREREpu+zsbNq2bcvUqVPdGv/666+TnJzseu3fv5969epx8803FxnXsmXLIuN++eWXiijfUCaTyfWkyewVu8na+pOyoYiIiFRpWrikHLWKCuSxi7YxKPW/RC5IP/2GNdK5lHgFrwQkIiJyPhwOh9ElyHmojD+3fv360a9fP7fHBwYGEhgY6Pp9wYIFHD16lJEjRxYZ5+HhQXh4eLnVWVX1jgtjVL0NjMp+B/85yoYiIlI5lO1qp/L4c9edhOVp80LuTXuacNKLbrclw7xhsHmhMXWJiIgUw9PTE4CcnByDK5HzcerP7dSfY1X03nvvER8fT8OGDYts37FjB5GRkTRq1Ihbb72Vffv2GVRhxTJv/Yonc15QNhQRkUqhbFe7lUc21J2E5cVeCAmPY8KByfT3Nx2ACRLGOpcS18pAIiJSBVgsFurWrUtaWhoAfn5+mM7+l5hUMQ6Hg5ycHNLS0qhbty4WS9XMFUlJSXzzzTd8/PHHRbZ36dKFmTNn0rx5c5KTk5k0aRI9evRg48aNBAQEFLuvvLw88vLyXL/bbLYKrb1cnMyG4MCsbCgiIpVA2a52Ks9sqCZhedm7AmxJ5xjgANtB57jYHpVWloiIyLmceuTzVJiU6qNu3bpV+pHdWbNmUbduXQYNGlRk+5mPL7dp04YuXbrQsGFD5s2bx6hRo4rd1+TJk5k0aVJFllv+TmbDkv/TTNlQRETKn7Jd7VUe2VBNwvKSlVq+40RERCqByWQiIiKC0NBQCgoKjC5H3OTp6Vll7yAE5xXtGTNmcPvtt+Pl5XXOsXXr1qVZs2bs3LmzxDHjxo1jzJgxrt9tNhvR0dHlVm+FUDYUEREDKNvVTuWVDdUkLC/+YeU7TkREpBJZLJYq3XSS6uXnn39m586dJd4ZeKasrCx27drF7bffXuIYb29vvL29y7PEiqdsKCIiBlK2k/OhhUvKS8PuzpXqSnyoxATWKOc4ERERkWogKyuL9evXs379egASExNZv369a6GRcePGMWzYsLM+995779GlSxdatWp11nuPPvooP//8M3v27GHFihVcf/31WCwWbrnllgo9l0qnbCgiIiLVjJqE5cVsgb4vnvylaBi0O5zTU9P3BU1MLSIiItXGmjVraN++Pe3btwdgzJgxtG/fnvHjxwOQnJx81srEmZmZfPbZZyXeRXjgwAFuueUWmjdvzuDBg6lfvz6//fYbISEhFXsylU3ZUERERKoZk8PhcBhdRGlsNhuBgYFkZmZitVqNLufcNi90rmR3xiImSY76fBJ0L2MeelQrC4mIiEiFq1bZ6TxUq/MrIRt+Fz2aEXc+aGBhIiIiUlu4m500J2F5ixsILfo7V6rLSuUwdblqTh65KdBl5xEuaxpsdIUiIiIiUln+lg13Hq9D789PwC4zl6Vl0STU3+gKRURERAA9blwxzBaI7QGtbyK4dTy3dI0B4P++20Y1uHFTRERERMrTGdmwSed+XB0Xgd0Br/2w3ejKRERERFzUJKwE/7qyCb6eFv7cn8EPW9KMLkdEREREDDSmVzNMJlj0VzKbkjKNLkdEREQEUJOwUoQEeDPi0hgAXvluG3a77iYUERERqa0ujrBybZtIAF77XncTioiISNWgJmEl+efljQjw9mBryjG++iup9A+IiIiISI31cHxTLGYTP2xJY+3edKPLEREREVGTsLLU9fPin1c0AuDl77aRf8JucEUiIiIiYpRGIf7c3LEBAC98s1XzVouIiIjh1CSsRHdcFktogDf704/z0aq9RpcjIiIiIgYaHd8MH08zv+85qnmrRURExHBqElYiPy8PRsc3A+C/P+7kWG6BwRWJiIiIiFHCA32449JYAF5M2MqJQj1pIiIiIsZRk7CSDe7UgEYhdUjPzuedn3cbXY6IiIiIGOifVzSmrp8nO9OymL/2gNHliIiISC2mJmEl87CYeaxPCwD+98tu0my5BlckIiIiIkYJ9PXk/p5NAHjth+0czy80uCIRERGprdQkNECflmF0uKguuQV2Xvthh9HliIiIiIiBbu/WkKi6vqTa8pjxa6LR5YiIiEgtpSahAUwmE+OuuRiAeWv2szMty+CKRERERMQo3h4WHu3jnLd62tJdpGfnG1yRiIiI1Ebn1SScOnUqMTEx+Pj40KVLF1avXn3O8VOmTKF58+b4+voSHR3Nww8/TG5u7X7M9pKYesRfHEah3cH/fbvV6HJERERExEDXtY3i4ggrx/JOMPWnnUaXIyIiIrVQmZuEc+fOZcyYMUyYMIF169bRtm1b+vTpQ1paWrHjP/74Y8aOHcuECRPYsmUL7733HnPnzuWJJ5644OKru8f7Nsdsgm83pbJ2b7rR5YiIiIiIQcxmE2P7Oeet/mDlXvan5xhckYiIiNQ2ZW4Svvrqq9x1112MHDmSuLg4pk2bhp+fHzNmzCh2/IoVK7j00kv5xz/+QUxMDL179+aWW24p9e7D2qBpWAA3d4wGYPLirTgcDoMrEhERERGjXN40mEub1Ce/0M4r320zuhwRERGpZcrUJMzPz2ft2rXEx8ef3oHZTHx8PCtXriz2M927d2ft2rWupuDu3btZvHgx11xzTYnHycvLw2azFXnVVA/3aoa3h5k1e4/y7aZUo8sREREREYOYTCbG9nXOW71gfRIbDmQaXJGIiIjUJmVqEh4+fJjCwkLCwsKKbA8LCyMlJaXYz/zjH//g6aef5rLLLsPT05PGjRtz5ZVXnvNx48mTJxMYGOh6RUdHl6XMaiU80Ic7e8QCMPmbLeSdKDS4IhERERExSusGgVzXLhKAZ77erCdNREREpNJU+OrGS5cu5fnnn+ett95i3bp1fP755yxatIhnnnmmxM+MGzeOzMxM12v//v0VXaah7r2yCSEB3uw9ksPsFXuNLkdEREREDPRY3xZ4e5hZvSedhI3FX4gXERERKW9lahIGBwdjsVhITS36WGxqairh4eHFfuapp57i9ttv584776R169Zcf/31PP/880yePBm73V7sZ7y9vbFarUVeNZm/tweP9m4GwBs/7iA9O9/gikRERETEKFF1fbn78kYATP5mq540ERERkUpRpiahl5cXHTt2ZMmSJa5tdrudJUuW0K1bt2I/k5OTg9lc9DAWiwVAj0+c4aaO0cRFWDmWe4IpP2w3uhwRERERMdA9VzQmNMCbfek5zFqxx+hyREREpBYo8+PGY8aMYfr06cyaNYstW7Zw7733kp2dzciRIwEYNmwY48aNc40fMGAAb7/9NnPmzCExMZHvv/+ep556igEDBriahQIWs4knr3VOVP3Rqn3sSD1mcEUiIiIiYpQ63h482qc5AP9dspMjWXkGVyQiIiI1nUdZPzBkyBAOHTrE+PHjSUlJoV27diQkJLgWM9m3b1+ROweffPJJTCYTTz75JAcPHiQkJIQBAwbw3HPPld9Z1BDdGwfTKy6M7zen8tziLcwc2dnokkRERETEIDd1aMCsFXvYlGTjtR+28+yg1kaXJCIiIjWYyVENnvm12WwEBgaSmZlZ4+cnTDycTe/Xfqag0MGsOzpzRbMQo0sSERGRaqamZ6eafn5n+m33EYa++xtmEySMvpxmYQFGlyQiIiLVjLvZqcJXN5ayiQ2uw7BuMQA8+/VmThQWv7iLiIiIiNR8XRvVp0/LMOwOeHbRFqPLERERkRpMTcIq6MGrmlLXz5MdaVnMXb0HEpfDhvnOn3atbiciIiJSm4zrdzGeFhPLth9i6ZZkZUMRERGpEGWek1AqXqCfJ6OvbsrKRTO5+tsHgCOn37RGQt8XIW6gYfWJiIiISOWJCa7D8G4x7F8xl7h5D4BD2VBERETKn+4krKJuC/yLaV5TCD0zBALYkmHeMNi80JjCRERERKTSPdxgG297TSHYrmwoIiIiFUNNwqrIXojHd2MBMJv+/ubJdWYSxurxEhEREalQy5YtY8CAAURGRmIymViwYME5xy9duhSTyXTWKyUlpci4qVOnEhMTg4+PD126dGH16tUVeBY1gL2QOj8+gQllQxEREak4ahJWRXtXgC2JszKgiwNsB53jRERERCpIdnY2bdu2ZerUqWX63LZt20hOTna9QkNDXe/NnTuXMWPGMGHCBNatW0fbtm3p06cPaWlp5V1+zaFsKCIiIpVAcxJWRVmp5TtORERE5Dz069ePfv36lflzoaGh1K1bt9j3Xn31Ve666y5GjhwJwLRp01i0aBEzZsxg7NixF1JuzaVsKCIiIpVAdxJWRf5h5TtOREREpBK1a9eOiIgIevXqxa+//uranp+fz9q1a4mPj3dtM5vNxMfHs3LlyhL3l5eXh81mK/KqVZQNRUREpBKoSVgVNezuXKmuxIdKTGCNco4TERERqSIiIiKYNm0an332GZ999hnR0dFceeWVrFu3DoDDhw9TWFhIWFjRZlZYWNhZ8xaeafLkyQQGBrpe0dHRFXoeVY6yoYiIiFQCNQmrIrMF+r548peiYdDuODk9dd8XnONEREREqojmzZvzz3/+k44dO9K9e3dmzJhB9+7dee211y5ov+PGjSMzM9P12r9/fzlVXE2cKxuibCgiIiLlQ03CqipuIAyeDdaIIptTqM9bIeOd74uIiIhUcZ07d2bnzp0ABAcHY7FYSE0tOndeamoq4eHhJe7D29sbq9Va5FXrlJQNHfWZG/ussqGIiIhcMC1cUpXFDYQW/Z0r1WWlcrDQylWf5pO338TFW1O5qoXmnREREZGqbf369UREOBtbXl5edOzYkSVLljBo0CAA7HY7S5Ys4f777zewymrib9lwY6YPA792wFYzbZJsxEXWwuapiIiIlBs1Cas6swViewAQBYxI2sI7y3Yz6avNdG8cjI+nHisRERGRipGVleW6CxAgMTGR9evXU69ePS666CLGjRvHwYMHmT17NgBTpkwhNjaWli1bkpuby//+9z9+/PFHvvvuO9c+xowZw/Dhw+nUqROdO3dmypQpZGdnu1Y7llKckQ1bAf32rmPRhmQmLtzE3H92xWQqad5CERERkXNTk7CaeeDqpixYf5C9R3KY9vMuRsc3M7okERERqaHWrFlDz549Xb+PGTMGgOHDhzNz5kySk5PZt2+f6/38/HweeeQRDh48iJ+fH23atOGHH34oso8hQ4Zw6NAhxo8fT0pKCu3atSMhIeGsxUzEPU/0v5gft6axek86n607yE0dGxhdkoiIiFRTJofD4TC6iNLYbDYCAwPJzMysnXPQ/M2iv5K57+N1eFnMJIzuQaMQf6NLEhERkSqkpmenmn5+ZfXOz7uY/M1W6tXxYsmYKwiq42V0SSIiIlKFuJudtHBJNXRN63CuaBZCfqGdp77cSDXo84qIiIhIBbnjsliahwWQnp3PC99sNbocERERqabUJKyGTCYTT1/XEm8PM7/uPMLCP5OMLklEREREDOJpMfPc9a0AmLtmP7/vSTe4IhEREamO1CSsphrWr8MDVzUB4JmvN5OZU2BwRSIiIiJilE4x9Rh6STQA//liAwWFdoMrEhERkepGTcJq7O7LG9Mk1J/DWfm89K0eLRERERGpzcb2a0G9Ol5sT83if8sTjS5HREREqhk1CasxLw8zzw5yPlry8ep9rNt31OCKRERERMQodf28+M81FwPw+pLt7E/PMbgiERERqU7UJKzmujaqz40dGuBwwH++2MgJPVoiIiIiUmvd0CGKLrH1yC2wM2HhJi1wJyIiIm5Tk7AGeOKaFgT6erIl2cbMFXuMLkdEREREDGIymXju+lZ4Wkz8uDWNbzelGF2SiIiIVBNqEtYA9f29GdevBQCvfr+dA0f1aImIiIhIbdUkNIB/Xt4YgAkLN2HL1QJ3IiIiUjo1CWuIwZ2i6RxTj5z8Qp74YqMeLRERERGpxe6/qgkx9f1IteXxwjda4E5ERERKpyZhDWE2m5h8Y2u8PMws236IL/44aHRJIiIiImIQH08Lk29oA8DHq/bx2+4jBlckIiIiVZ2ahDVI4xB/Hrq6KQBPf72Zw1l5BlckIiIiIkbp1rg+t3S+CICxn/1FbkGhwRWJiIhIVaYmYQ1z9+WNiIuwkpFTwMSFm4wuR0REREQMNO6aFoRZvdlzJIcpP+wwuhwRERGpwtQkrGE8LWZeuqkNFrOJr/9K5vvNqUaXJCIiIiIGsfp48uyg1gBMX76bjQczDa5IREREqio1CWugVlGB3NkjFoAnF2zQinYiIiIitVivuDD6t4mg0O7gsfl/UVBoN7okERERqYLUJKyhHo5vphXtRERERASAiQNaUtfPk83JNqYv3210OSIiIlIFqUlYQ/l4WnjhxjNWtNuZBonLYcN850+7Jq4WERERqS1CArx5qn8cAFN+2MGu1ExlQxERESnCw+gCpOJ0beRc0S59zXwaffQAOI6cftMaCX1fhLiBxhUoIiIiIpXmhg5RLFh/EL9diwl8536wHz79prKhiIhIrac7CWu4pxrvZJrXFILtR4q+YUuGecNg80JjChMRERGRSmUymXitzT7e9pxCvcLDRd9UNhQREan11CSsyeyF+C15AgCz6e9vOpw/Esbq8RIRERGR2sBeSPDy8ZhMyoYiIiJyNjUJa7K9K8CWxFkZ0MUBtoPOcSIiIiJSsykbioiIyDmoSViTZaWW7zgRERERqb6UDUVEROQc1CSsyfzDyneciIiIiFRfyoYiIiJyDmoS1mQNuztXqivhoRIHJrBGOceJiIiISM2mbCgiIiLnoCZhTWa2QN8XT/5SNAzaHQAO6PuCc5yIiIiI1GzKhiIiInIOahLWdHEDYfBssEYU2ZxCfe7JH82P5i4GFSYiIiIile4c2fC+gofZVPcKgwoTERERo3kYXYBUgriB0KK/c6W6rFTwD+P9jXX59td9rPtsAwkP1aW+v7fRVYqIiIhIZfhbNnT4h/LMch++2XyInXPXs/D+y/Dx1N2EIiIitY3uJKwtzBaI7QGtb4LYHjzSN46mof4cOpbHuM834HA4jK5QRERERCrLGdnQFHs5z9zQlmB/L7anZvFSwjajqxMREREDqElYS/l4WpgytB2eFhPfbU5l7u/7jS5JREREqphly5YxYMAAIiMjMZlMLFiw4JzjP//8c3r16kVISAhWq5Vu3brx7bffFhkzceJETCZTkVeLFi0q8CzEHcH+3vzfTW0BmPFrIsu2HzK4IhEREalsahLWYi0jA3m0d3MAJn21mcTD2QZXJCIiIlVJdnY2bdu2ZerUqW6NX7ZsGb169WLx4sWsXbuWnj17MmDAAP74448i41q2bElycrLr9csvv1RE+VJGPVuEcnvXhgA8+umfpGfnG1yRiIiIVCbNSVjL3dWjEUu3HWLl7iOMnrue+fd0w9Oi3rGIiIhAv3796Nevn9vjp0yZUuT3559/ni+//JKvvvqK9u3bu7Z7eHgQHh5eXmVKOXrimotZseswuw5l88TnG3j7tg6YTKbSPygiIiLVnrpBtZzZbOKVwW2x+njw5/4M/rtkh9EliYiISA1ht9s5duwY9erVK7J9x44dREZG0qhRI2699Vb27dt3zv3k5eVhs9mKvKRi+HpZeH1oezwtJhI2pfDpmgNGlyQiIiKVRE1CIbKuL89d3xqAN3/ayZo96QZXJCIiIjXByy+/TFZWFoMHD3Zt69KlCzNnziQhIYG3336bxMREevTowbFjx0rcz+TJkwkMDHS9oqOjK6P8WqtVVCBjejmnpJn41Sb2aEoaERGRWkFNQgFgQNtIbmgfhd0BD89bz7HcAqNLEhERkWrs448/ZtKkScybN4/Q0FDX9n79+nHzzTfTpk0b+vTpw+LFi8nIyGDevHkl7mvcuHFkZma6Xvv3a8G1inb35Y3oEluPnPxCRs9dz4lCu9EliYiISAVTk1BcJl7Xkqi6vuxPP86ELzcZXY6IiIhUU3PmzOHOO+9k3rx5xMfHn3Ns3bp1adasGTt37ixxjLe3N1artchLKpbFbOLVIe0I8PFg/f4M3tCUNCIiIjXeeTUJp06dSkxMDD4+PnTp0oXVq1efc3xGRgb33XcfEREReHt706xZMxYvXnxeBUvFsfp4MmVoO8wm+PyPg3y2VnPQiIiISNl88sknjBw5kk8++YT+/fuXOj4rK4tdu3YRERFRCdVJWUTV9eXZQa0A+O9PO1mx67DBFYmIiEhFKnOTcO7cuYwZM4YJEyawbt062rZtS58+fUhLSyt2fH5+Pr169WLPnj3Mnz+fbdu2MX36dKKioi64eCl/l8TU46GrmwHw1Jcb2XUoy+CKRERExChZWVmsX7+e9evXA5CYmMj69etdC42MGzeOYcOGucZ//PHHDBs2jFdeeYUuXbqQkpJCSkoKmZmZrjGPPvooP//8M3v27GHFihVcf/31WCwWbrnllko9N3HPde2iuLljAxwOGD1nPUey8owuSURERCpImZuEr776KnfddRcjR44kLi6OadOm4efnx4wZM4odP2PGDNLT01mwYAGXXnopMTExXHHFFbRt2/aCi5eKcf9VTejWqD45+YXc//Ef5BYUGl2SiIiIGGDNmjW0b9+e9u3bAzBmzBjat2/P+PHjAUhOTi6yMvG7777LiRMnXE+QnHo99NBDrjEHDhzglltuoXnz5gwePJj69evz22+/ERISUrknJ26bdF1LmoT6k3Ysj0c+/RO73WF0SSIiIlIBTA6Hw+1/y+fn5+Pn58f8+fMZNGiQa/vw4cPJyMjgyy+/POsz11xzDfXq1cPPz48vv/ySkJAQ/vGPf/D4449jsVjcOq7NZiMwMJDMzEzNQVNJUm25XPP6co5k5zOsW0Oevq6V0SWJiIiIm2p6dqrp51cVbU2xcd2bv5J3ws4T17Tg7ssbG12SiIiIuMnd7FSmOwkPHz5MYWEhYWFhRbaHhYWRkpJS7Gd2797N/PnzKSwsZPHixTz11FO88sorPPvssyUeJy8vD5vNVuQllSvM6sPLg513e85euZeEjcX/+YqIiIhIzdci3Mr4AXEAvJSwjfX7M4wtSERERMpdha9ubLfbCQ0N5d1336Vjx44MGTKE//znP0ybNq3Ez0yePJnAwEDXKzo6uqLLlGL0bB7K3Zc3AuCx+X9y4GiOwRWJiIiIiFH+0fkirmkdzgm7gwc+WYctt8DokkRERKQclalJGBwcjMViITU1tcj21NRUwsPDi/1MREQEzZo1K/Jo8cUXX0xKSgr5+fnFfmbcuHFkZma6Xvv37y9LmVKOHu3dnLbRdbHlnuDBT/6goNBudEkiIiIiYgCTycTkG9rQIMiX/enHGffZBsowc5GIiIhUcWVqEnp5edGxY0eWLFni2ma321myZAndunUr9jOXXnopO3fuxG4/3Vzavn07EREReHl5FfsZb29vrFZrkZcYw8vDzJu3tCfAx4N1+zJ45bvtYC+ExOWwYb7zp10Lm4iIiIjUBoG+nrz5jw54mE0s2pDMR6v2KRuKiIjUEGV+3HjMmDFMnz6dWbNmsWXLFu69916ys7MZOXIkAMOGDWPcuHGu8ffeey/p6ek89NBDbN++nUWLFvH8889z3333ld9ZSIWKrufHize2ASBx+Sfk/l8czLoWPhvl/DmlFWxeaHCVIiIiIlIZ2kXX5bG+zQH47euZ5L/SUtlQRESkBvAo6weGDBnCoUOHGD9+PCkpKbRr146EhATXYib79u3DbD7de4yOjubbb7/l4Ycfpk2bNkRFRfHQQw/x+OOPl99ZSIW7pnUEL168h5t3T4Hjf3vTlgzzhsHg2RA30IjyRERERKQS3dWjESc2LeSelFch+29vKhuKiIhUSyZHNZhIxN2lmqUC2QtxvNYKjiVhKnaACayRMHoDmC3FjhAREZHKUdOzU00/v2rBXoj9ZDYs/tEkZUMREZGqwt3sVOGrG0sNsXcFphIbhAAOsB2EvSsqsSgRERERMcTeFZhLbBCCsqGIiEj1oyahuCcrtfQxZRknIiIiItWXsqGIiEiNoyahuMc/rHzHiYiIiEj1pWwoIiJS46hJKO5p2N05r0wJDxw7MIE1yjlORERERGo2ZUMREZEaR01CcY/ZAn1fPPlL0TBodwA4sPeZrImpRURERGoDN7IhfV9QNhQREalG1CQU98UNhMGzwRpRZHMK9bknfzRTDrYwqDARERERqXSlZMO52e2MqUtERETOi4fRBUg1EzcQWvR3rlSXlQr+YfyWfhHffrqRb3/cSVyklb6tIkrfj4iIiIhUf8Vkw892hfDtD7v4acEmmoYF0OGiIKOrFBERETeoSShlZ7ZAbA/XrzfEwqbkbN77JZEx8/4kNtif5uEBBhYoIiIiIpXmb9nwvoYONiVnk7AphXs+WMtXD1xGmNXHwAJFRETEHXrcWMrFuH4tuLRJfXLyC7lr9hoycvKNLklEREREDGA2m3h5cFuahfmTdiyPez5cS96JQqPLEhERkVKoSSjlwsNi5s1bOtAgyJd96Tk88MkfnCi0G12WiIiIiBjA39uD6cM6YfXx4I99GTy1YCMOh8PoskREROQc1CSUchNUx4vpwzrh62lh+Y7DvPTtNqNLEhERERGDNKxfhzf/0QGzCeatOcAHv+01uiQRERE5BzUJpVxdHGHl5ZvbAvDust18uf6gwRWJiIiIiFEubxbC2H4tAHj6q838tvuIwRWJiIhISdQklHLXv00E/7qyMQCPzf+L9fszjC1IRERERAxzV49GDGwbyQm7g399tI59R3KMLklERESKoSahVIhHejfn6hah5J2wc+esNRzMOG50SSIiIiJiAJPJxIs3tqF1VCDp2fncMet3bLkFRpclIiIif6MmoVQIi9nE67e0p0V4AIez8hg183ey8k4YXZaIiIiIGMDXy8L/hnci3OrDzrQs7vtonRa5ExERqWLUJJQK4+/twXsjLiHY35utKcd48JM/KLRrVTsRERGR2ijM6sP/hp9e5G7iV5u04rGIiEgVoiahVKiour78b3gnvD3M/Lg1jecWbTG6JBERERExSKuoQF4f2g6TCT78bR/v/7rH6JJERETkJDUJpcK1i67Lq4PbATDj10Q+/G2vsQWJiIiIiGF6twxn3MkVj59dtJkft6YaXJGIiIiAmoRSSfq3ieDR3s0AmLBwE8t3HDK4IhERERExyl09GjH0kmjsDnjg4z/YkmwzuiQREZFaT01CqTT39WzCDe2jKLQ7+NdH69iRnAGJy2HDfOdPe6HRJYqIiIhIJTCZTDx9XSu6NapPdn4ho2b+TlpGtrKhiIiIgdQklEpjMpmYfGNrOjUMonv+CqzvdIBZ18Jno5w/p7SCzQuNLlNEREROWrZsGQMGDCAyMhKTycSCBQtK/czSpUvp0KED3t7eNGnShJkzZ541ZurUqcTExODj40OXLl1YvXp1+RcvVZ6Xh5lpt3WkUXAdWh9bhun1NsqGIiIiBlKTUCqVt4eF97umMM1rCiGOI0XftCXDvGEKgyIiIlVEdnY2bdu2ZerUqW6NT0xMpH///vTs2ZP169czevRo7rzzTr799lvXmLlz5zJmzBgmTJjAunXraNu2LX369CEtLa2iTkOqsEA/T+b0OMTbXlOobz9c9E1lQxERkUplcjgcDqOLKI3NZiMwMJDMzEysVqvR5ciFsBfClFY4bEmYih1gAmskjN4AZkslFyciIlIzVER2MplMfPHFFwwaNKjEMY8//jiLFi1i48aNrm1Dhw4lIyODhIQEALp06cIll1zCm2++CYDdbic6OpoHHniAsWPHulWLsmENomwoIiJS4dzNTrqTUCrX3hVQYggEcIDtoHOciIiIVCsrV64kPj6+yLY+ffqwcuVKAPLz81m7dm2RMWazmfj4eNeY4uTl5WGz2Yq8pIZQNhQREaky1CSUypWVWr7jREREpMpISUkhLCysyLawsDBsNhvHjx/n8OHDFBYWFjsmJSWlxP1OnjyZwMBA1ys6OrpC6hcDKBuKiIhUGWoSSuXyDyt9TFnGiYiISI03btw4MjMzXa/9+/cbXZKUF2VDERGRKsPD6AKklmnY3TmvjC0ZOHs6TLsD8vzC8W3YvfJrExERkQsSHh5OamrRO75SU1OxWq34+vpisViwWCzFjgkPDy9xv97e3nh7e1dIzWIwN7LhCf8IvJQNRUREKpzuJJTKZbZA3xdP/lJ09plTsfDfWf9gZWJGZVYlIiIi5aBbt24sWbKkyLbvv/+ebt26AeDl5UXHjh2LjLHb7SxZssQ1RmqZc2RD+8mfY3NuZfuhnEotS0REpDZSk1AqX9xAGDwbrBFFt1ujeCd8Il8XdOLOWb/z14EMQ8oTERERp6ysLNavX8/69esBSExMZP369ezbtw9wPgY8bNgw1/h77rmH3bt389hjj7F161beeust5s2bx8MPP+waM2bMGKZPn86sWbPYsmUL9957L9nZ2YwcObJSz02qkHNkw5cC/8Pnxztw+3ur2J+uRqGIiEhFMjkcjrPv669i3F2qWaoZe6FzpbqsVOc8Mw27k1sII9//nZW7jxDk58mn93SjSWiA0ZWKiIhUK+WVnZYuXUrPnj3P2j58+HBmzpzJiBEj2LNnD0uXLi3ymYcffpjNmzfToEEDnnrqKUaMGFHk82+++Sb/93//R0pKCu3ateONN96gS5cubtelbFhDFZMNM3ILGfLOb2xLPcZF9fyYf083Qq0+RlcqIiJSrbibndQklConK+8Et07/jT8PZBJu9eHTe7oRXc/P6LJERESqjZqenWr6+UlRqbZcbp62kn3pObQID2DO3V2p6+dldFkiIiLVhrvZSY8bS5Xj7+3BzJGdaRrqT4otl9vfW8WhY3lGlyUiIiIiBgiz+vDhqC6EBnizNeUYI2f+Tk7+CaPLEhERqXHUJJQqKaiOFx+M6kJUXV/2HMlh2IzVZB4vMLosERERETHARfX9+GBUFwJ9PfljXwb//GAteScKjS5LRESkRlGTUKqs8EAfPrqzC8H+3mxJtnGHrhqLiIiI1FrNwwN4f+Ql+HlZWL7jMKPnrOdEob30D4qIiIhb1CSUKi0muA4fjOqM1ceDtXuPctfsNeQW6KqxiIiISG3U4aIg3r29E14WM99sTOGx+X9RaK/yU6yLiIhUC2oSSpV3cYSV90d2po6XhV93HuHuD9aqUSgiIiJSS13WNJg3bmmPxWzi8z8OMu7zv7CrUSgiInLB1CSUaqFjwyBmjLgEX08Ly7Yf4r6P1pF/Qo+XiIiIiNRGfVuF8/rQdphNMG/NAZ78ciMOhxqFIiIiF0JNQqk2ujSqz3vDO+HtYWbJ1jQe+GQdBZqHRkRERKRWurZNJK8ObofJBB+v2sfEhZvUKBQREbkAahJKtdK9STDTh3XCy8PMt5tST09YbS+ExOWwYb7zp12PI4uIiIjUdIPaR/HSjW0wmWDWyr08u2iLs1GobCgiIlJmHkYXIFJWlzcL4Z3bOnL3B2tYtCGZDjnLuePYNEy2pNODrJHQ90WIG2hcoSIiIiJS4W7uFM0Ju4Nxn2/gvV8SaZn5M9envqFsKCIiUka6k1CqpZ4tQnnr1o5cY/mdkQfGw5khEMCWDPOGweaFxhQoIiIiIpXmls4X8cx1LeljXs2g7WOVDUVERM6DmoRSbfVqEcwr1k8AMJ317sn5aBLG6vESERERkVrg9i7RvBKgbCgiInK+1CSU6mvvCnyPp2A+OwWe5ADbQdi7ojKrEhEREREj7F2Bf16qsqGIiMh5UpNQqq+s1PIdJyIiIiLVl7KhiIjIBVGTUKov/7DyHSciIiIi1ZeyoYiIyAVRk1Cqr4bdnSvVFTPrDIAdsFujnONEREREpGYrJRs6AIeyoYiISInUJJTqy2yBvi+e/KVoGLQ7AAe8ZhlJdoGj0ksTERERkUpWSjZ0OOA9/39S4Chx0kIREZFaTU1Cqd7iBsLg2WCNKLK5oE4EY3iE/ybH8Y//reJodr5BBYqIiIhIpSkhG+b5hXP/iYd5dncT7vlgLbkFWuFYRETk70wOh6PK32Zls9kIDAwkMzMTq9VqdDlSFdkLnSvVZaU655lp2J31B48x4v3VZOQU0CzMnw9GdSHM6mN0pSIiIhWupmenmn5+Ug6KyYY/bj/MvR+uI++EnS6x9fjf8E4E+HgaXamIiEiFczc7qUkoNdr21GPc/t4qUm15NAjy5cNRXYgJrmN0WSIiIhWqpmenmn5+UnFW7T7CnbPWcCzvBK2irMwa2Zn6/t5GlyUiIlKh3M1O5/W48dSpU4mJicHHx4cuXbqwevVqtz43Z84cTCYTgwYNOp/DipRZs7AA5t/TnZj6fhw4epybpq1kS7LN6LJERERExABdGtXnk7u7Ur+OFxsP2rj5nZUczDhudFkiIiJVQpmbhHPnzmXMmDFMmDCBdevW0bZtW/r06UNaWto5P7dnzx4effRRevTocd7FipyP6Hp+zLunGy3CAziclceQd1aydm+60WWJiIiIiAFaRQXy6T3diAz0YfehbG5+ewW7DmUZXZaIiIjhytwkfPXVV7nrrrsYOXIkcXFxTJs2DT8/P2bMmFHiZwoLC7n11luZNGkSjRo1uqCCRc5HaIAPc//ZjU4Ng7DlnuDW/61i6bZzN7ZFREREpGZqFOLP/Hu70zikDkmZudw8bSUbDmQaXZaIiIihytQkzM/PZ+3atcTHx5/egdlMfHw8K1euLPFzTz/9NKGhoYwaNcqt4+Tl5WGz2Yq8RC5UoK8nH4zqwhXNQsgtsHPnrDV8uma/0WWJiIiIiAEi6/oy75/daB0VSHp2PkPfXcnP2w8ZXZaIiIhhytQkPHz4MIWFhYSFhRXZHhYWRkpKSrGf+eWXX3jvvfeYPn2628eZPHkygYGBrld0dHRZyhQpka+XhenDOjGoXSQn7A7+Pf8v/rtkB9Vg/R4RERERKWf1/b35+K4uXNqkPtn5hdwx83fm6SKyiIjUUue1cIm7jh07xu2338706dMJDg52+3Pjxo0jMzPT9dq/X/+ilvLj5WHm1cHtuPfKxgC88v12nvhiAycK7WAvhMTlsGG+86e90OBqRURERKQiBfh48v6IzlzfPopCu4PH5v/F6z+cvIisbCgiIrWIR1kGBwcHY7FYSE1NLbI9NTWV8PDws8bv2rWLPXv2MGDAANc2u93uPLCHB9u2baNx48Znfc7b2xtvb++ylCZSJmazicf7tiAy0IcJCzfxyer9RCX/wL9yp2M+lnR6oDUS+r4IcQONK1ZEREREKpTzInJbIgJ9eGvpLl77YTv19iVw29G3MCkbiohILVGmOwm9vLzo2LEjS5YscW2z2+0sWbKEbt26nTW+RYsWbNiwgfXr17teAwcOpGfPnqxfv16PEYvhbu8Ww7TbOjLAcw3/SptUNAQC2JJh3jDYvNCYAkVERAw2depUYmJi8PHxoUuXLqxevbrEsVdeeSUmk+msV//+/V1jRowYcdb7ffv2rYxTETknk8nEY31b8MygVvSzrObWvU+CsqGIiNQiZbqTEGDMmDEMHz6cTp060blzZ6ZMmUJ2djYjR44EYNiwYURFRTF58mR8fHxo1apVkc/XrVsX4KztIkbpfXEIVwZ8AjlgOutdB2CChLHQoj+YLZVfoIiIiEHmzp3LmDFjmDZtGl26dGHKlCn06dOHbdu2ERoaetb4zz//nPz8fNfvR44coW3bttx8881FxvXt25f333/f9bueIJGq5PbODbj55znKhiIiUuuUuUk4ZMgQDh06xPjx40lJSaFdu3YkJCS4FjPZt28fZnOFTnUoUr72rsArJ/kcAxxgOwh7V0Bsj0orS0RExGivvvoqd911l+ti8LRp01i0aBEzZsxg7NixZ42vV69ekd/nzJmDn5/fWU1Cb2/vYqeqEakS9q7A53hKcR3Ck5QNRUSkZipzkxDg/vvv5/777y/2vaVLl57zszNnzjyfQ4pUnKzU0seUZZyIiEgNkJ+fz9q1axk3bpxrm9lsJj4+npUrV7q1j/fee4+hQ4dSp06dItuXLl1KaGgoQUFBXHXVVTz77LPUr1+/XOsXOW/KhiIiUkudV5NQpEbxDyvfcSIiIjXA4cOHKSwsdD0tckpYWBhbt24t9fOrV69m48aNvPfee0W29+3blxtuuIHY2Fh27drFE088Qb9+/Vi5ciUWS/GPbubl5ZGXl+f63WaznccZibhJ2VBERGopNQlFGnZ3rlRnS8Y5z0xRdgcc9QjBK+wSAiq/OhERkWrpvffeo3Xr1nTu3LnI9qFDh7r+uXXr1rRp04bGjRuzdOlSrr766mL3NXnyZCZNmlSh9Yq4uJENbV6h+EV1xavyqxMREakwmjxQxGyBvi+e/KXo5DOOk78/cfxWbnpnNfvTcyq5OBEREWMEBwdjsVhITS36SGVqamqp8wlmZ2czZ84cRo0aVepxGjVqRHBwMDt37ixxzLhx48jMzHS99u/f795JiJwPN7Lh49n/4Pb313A0Ox8REZGaQk1CEYC4gTB4Nlgjimw2WSPZFz+NP+r0YFvqMQZN/ZU1e9INKlJERKTyeHl50bFjR5YsWeLaZrfbWbJkCd26dTvnZz/99FPy8vK47bbbSj3OgQMHOHLkCBERESWO8fb2xmq1FnmJVKhzZMNNPd7kV8/urEpMZ9Bbv7Iz7ZhBRYqIiJQvk8PhOPse+irGZrMRGBhIZmamQqFULHuhc6W6rFTnPDMNu4PZQnLmce6ctYZNSTY8LSYmDmzJPzpfhMlU4rJ3IiIihimv7DR37lyGDx/OO++8Q+fOnZkyZQrz5s1j69athIWFMWzYMKKiopg8eXKRz/Xo0YOoqCjmzJlTZHtWVhaTJk3ixhtvJDw8nF27dvHYY49x7NgxNmzYgLe3d6Wen0ipSsiG21OPccfM3zlw9Dj+3h68MrgtfVpqxW4REama3M1OmpNQ5ExmC8T2OGtzRKAvn97TjX9/+heLNiTzny82suFAJpOua4m3R/GTrIuIiFR3Q4YM4dChQ4wfP56UlBTatWtHQkKCazGTffv2YTYXfTBl27Zt/PLLL3z33Xdn7c9isfDXX38xa9YsMjIyiIyMpHfv3jzzzDNuNwhFKlUJ2bBZWABf3ncp//poHasS0/nnB2t58KomjI5vhtmsi8giIlI96U5CkTJwOBy8s2w3LyVsxe6AdtF1efu2DkQE+hpdmoiIiEtNz041/fyk+igotPP84i28/+seAK5qEcprQ9oR6OtpbGEiIiJncDc7aU5CkTIwmUzcc0VjZo7sTKCvJ+v3ZzDgv7+wOlHzFIqIiIjUNp4WMxMGtOTVwW3x9jDz49Y0rnvzF7anap5CERGpftQkFDkPlzcL4av7L6NFeACHs/L5x/TfmL1yD9XgxlwRERERKWc3dGjAZ/d2J6quL3uO5DBo6q98syHZ6LJERETKRE1CkfN0UX0/Pv9Xdwa2jeSE3cH4LzfxyLw/yck/4RxgL4TE5bBhvvOnvdDYgkVERESkwrSKCuSrBy6je+P65OQXcu9H65j8zRYKCu3OAcqGIiJSxWlOQpEL5HA4eO+XRJ5fvAW7A5qG+jO7WwoRKyeCLen0QGsk9H0R4gYaVquIiNQONT071fTzk+rtRKGdFxO2Mn15IgCXxATxbqdkgpY9qWwoIiKG0JyEIpXEZDJxZ49GfHxXV0IDvGl0+EfCEu7GcWYIBLAlw7xhsHmhMYWKiIiISIXzsJj5T/843rq1AwHeHtTb9y2BX9+hbCgiIlWemoQi5aRro/osur87z/t+CIDprBEnb9pNGKvHS0RERERquGtaR/DVfd141vtDcCgbiohI1acmoUg5CklfS/3Cw5jPToEnOcB2EPauqMyyRERERMQAMdl/EuJQNhQRkepBTUKR8pSVWr7jRERERKT6UjYUEZFqRE1CkfLkH1a+40RERESk+lI2FBGRakRNQpHy1LC7c6W6YmadAbA7INUUzHpzXOXWJSIiIiKVz41seMgczG6/NpVbl4iISDHUJBQpT2YL9H3x5C9Fw6ADEyYTjM+7jZveWcVbS3dSaHdUfo0iIiIiUjlKyYaY4Mnc27h26krm/b4fh0PZUEREjKMmoUh5ixsIg2eDNaLIZpM1kpxBM/FodR0n7A5eStjGrf/7jeTM4wYVKiIiIiIV7hzZMPPa97DF9CMnv5DHPvuL+z/+g8ycAoMKFRGR2s7kqAaXq2w2G4GBgWRmZmK1Wo0uR8Q99kLnSnVZqc55Zhp2B7MFh8PBp2sPMHHhJnLyCwn09WTyDa25pnVE6fsUERFxQ03PTjX9/KSGKiEbFtodvLNsF69+t50TdgeRgT68OqQdXRvVN7piERGpIdzNTmoSihgk8XA2D835g78OZAJwXbtInh7YikA/T4MrExGR6q6mZ6eafn5SO/25P4OH5vzBniM5ANxxaSyP9W2Oj6fF4MpERKS6czc76XFjEYPEBtdh/j3dua9nY8wm+HJ9Er2n/MxP29KMLk1EREREKlnb6Lp8/WAPhl4SDcCMXxO55o3lrN+fYWxhIiJSa6hJKGIgLw8z/+7Tgs/u7U6j4Dqk2vIY+f7vjPv8L7LyTjgH2QshcTlsmO/8aS80tmgRERERqRD+3h68cGMb3h9xCaEB3uw+lM0Nb/3Ky99uI/+E3TlI2VBERCqIHjcWqSKO5xfyf99uY8aviQA0CPLlvc7JNP/jWbAlnR5ojXSukhc30KBKRUSkqqvp2ammn58IQEZOPhMWbuLL9c4c2CI8gOmXJBO9aqKyoYiIlIkeNxapZny9LIwfEMcnd3WlQZAvLTN/punSf+E4MwQC2JJh3jDYvNCYQkVERESkwtX18+L1oe1569YO1KvjRcO0JUR9d7eyoYiIVBg1CUWqmG6N65Pw4KW8WOcjAExnjTh582/CWD1eIiIiIlLDXdM6gm8fvJTJvsqGIiJSsdQkFKmC/FNWU7fgEOazU+BJDrAdhL0rKrMsERERETFASPpa6hUqG4qISMVSk1CkKspKLd9xIiIiIlJ9KRuKiEglUJNQpCryD3NrWLopqIILERERERHDuZkNszzrV3AhIiJSk6lJKFIVNezuXKmumFlnAOwOSHLUp+en+Xywcg+F9iq/SLmIiIiInC83s+FVn+bz9V9JOBzKhiIiUnZqEopURWYL9H3x5C9/D4MmTCYTswPvITPPzlNfbuL6t37lrwMZlVykiIiIiFQKN7LhO753kZZ9gvs//oNhM1az+1BWZVcpIiLVnJqEIlVV3EAYPBusEUW3WyMxDZ7Nv0f/m0kDWxLg7cFfBzK5buqvPLlgA5k5BcbUKyIiIiIVp5RsOO6Rx3jo6qZ4eZhZvuMwfacs55XvtpFboBWPRUTEPSZHNbgX3WazERgYSGZmJlar1ehyRCqXvdC5Ul1WqnM+mobdnVeTT0o7lsvkxVv54o+DANSv48W4ay7mxg5RmEwlLoEnIiI1WE3PTjX9/ETOqZRsuOdwNhMWbuLn7YcAiK7ny8QBLbn6YvfmNRQRkZrH3eykJqFIDbFy1xGe+nIjO9Ocj5Z0jqnHM4Na0Tw84PSgUkKliIjUDDU9O9X08xO5UA6Hg283pTDpq80kZ+YC0CsujAkD4mgQ5Hd6oLKhiEit4G520uPGIjVEt8b1WfxgD8b2a4Gvp4XVe9K55o3lTFy4iYycfNi8EKa0glnXwmejnD+ntHJuFxERKcHUqVOJiYnBx8eHLl26sHr16hLHzpw5E5PJVOTl4+NTZIzD4WD8+PFERETg6+tLfHw8O3bsqOjTEKlVTCYTfVtF8MOYK/jn5Y3wMJv4fnMq8a/+zJQftnM8v1DZUEREzqImoUgN4uVh5p4rGvPDI1fQp2UYhXYHM1fs4en/exHHvGE4bElFP2BLhnnDFAZFRKRYc+fOZcyYMUyYMIF169bRtm1b+vTpQ1paWomfsVqtJCcnu1579+4t8v5LL73EG2+8wbRp01i1ahV16tShT58+5ObmVvTpiNQ6dbw9GHfNxSx+qAedY+uRW2Bnyg87ePqlF5QNRUTkLGoSitRAUXV9eef2Tnw4qgstQn151D4Dh8Nx1lp4cHK2gYSxzsdNREREzvDqq69y1113MXLkSOLi4pg2bRp+fn7MmDGjxM+YTCbCw8Ndr7Cw0/OgORwOpkyZwpNPPsl1111HmzZtmD17NklJSSxYsKASzkikdmoWFsDcu7vy5j/aEx3oxQMF/1M2FBGRs6hJKFKDXdY0mEXXeRBpSsdc4homDrAddM5HIyIiclJ+fj5r164lPj7etc1sNhMfH8/KlStL/FxWVhYNGzYkOjqa6667jk2bNrneS0xMJCUlpcg+AwMD6dKlyzn3KSIXzmQycW2bSJbc7KVsKCIixVKTUKSGs+SU/EhYEVmpFVuIiIhUK4cPH6awsLDInYAAYWFhpKSkFPuZ5s2bM2PGDL788ks+/PBD7HY73bt358CBAwCuz5VlnwB5eXnYbLYiLxE5P17HD7k3UNlQRKTWUZNQpKbzDyt9DJDvG1LBhYiISE3XrVs3hg0bRrt27bjiiiv4/PPPCQkJ4Z133rmg/U6ePJnAwEDXKzo6upwqFqmF3MyGhX6hFVyIiIhUNWoSitR0DbuDNRKKmXUGwO6AJEd9es7LY96a/RTaHZVbn4iIVEnBwcFYLBZSU4veTZSamkp4eLhb+/D09KR9+/bs3LkTwPW5su5z3LhxZGZmul779+8vy6mIyJnczIZ9vyjg200pOBzKhiIitYWahCI1ndkCfV88+UvRMOjAhMlk4k2vURy0FfDY/L/o9/oylmxJVSAUEanlvLy86NixI0uWLHFts9vtLFmyhG7durm1j8LCQjZs2EBERAQAsbGxhIeHF9mnzWZj1apV59ynt7c3Vqu1yEtEzpMb2fAV80h2HM7lnx+s5aZpK1mzJ73y6xQRkUqnJqFIbRA3EAbPBmtEkc0maySmwbMZ/9hY/nPNxQT6erI9NYtRs9Yw5N3fWLfvqEEFi4hIVTBmzBimT5/OrFmz2LJlC/feey/Z2dmMHDkSgGHDhjFu3DjX+KeffprvvvuO3bt3s27dOm677Tb27t3LnXfeCTgXThg9ejTPPvssCxcuZMOGDQwbNozIyEgGDRpkxCmK1E6lZMMJj4/lvp6N8fE0s3bvUW6atpK7Zq9hR+oxgwoWEZHK4GF0ASJSSeIGQov+zpXqslKd89E07A5mCz7AXZc3YnCnaN76eSfv/7qH1Ynp3PDWCq5qEcqYXs1oFRVYdH/2wmL3JSIiNceQIUM4dOgQ48ePJyUlhXbt2pGQkOBaeGTfvn2YzaevOR89epS77rqLlJQUgoKC6NixIytWrCAuLs415rHHHiM7O5u7776bjIwMLrvsMhISEvDx8an08xOp1c6RDa3Av/u0YFi3GKb8sJ25v+/n+82p/LAllevaRvJQfDNig+uc3pdyoYhIjWByVINnCm02G4GBgWRmZurxEpFKkJRxnCk/bOezdQddcxT2jgtjTO9mtAi3wuaFkPA42JJOf8ga6Xx0JW6gQVWLiMgpNT071fTzE6lqdqYd4/++3ca3m5zziVrMJm5oH8WDVzclOuUH5UIRkSrO3eykJqGIlCjxcDav/7CdL/9M4tTfFE/E7uCu5ImY+PtfHSfntBk8W4FQRMRgNT071fTzE6mqNhzI5LUftvPj1jQArrH8zlTP14C/z26oXCgiUpW4m500J6GIlCg2uA5Thrbnu9GX0791BGbsXJv0egmLmpzcljDW+ciJiIiIiNQorRsEMmPEJXz+r+5c3iSIJz1m4XAUt06ycqGISHWkJqGIlKppWABTb+3ATzd7EWlKx3x2EjzJAbaDzjlpRERERKRG6nBRELOvLlQuFBGpYdQkFBG3NfRyc0W7rNSKLUREREREjOVu3lMuFBGpNs6rSTh16lRiYmLw8fGhS5curF69usSx06dPp0ePHgQFBREUFER8fPw5x4tIFeYf5tawzcd8K7gQERERETGUm7lwyiobm5NsFVyMiIiUhzI3CefOncuYMWOYMGEC69ato23btvTp04e0tLRixy9dupRbbrmFn376iZUrVxIdHU3v3r05ePDgBRcvIpWsYXfnanXFzDwDYHdAkqM+1y60c/O0FSzdllbC/IUiIiIiUq2Vlgtx5sI3doZwzRvLGTXzd9buPVqpJYqISNmUeXXjLl26cMkll/Dmm28CYLfbiY6O5oEHHmDs2LGlfr6wsJCgoCDefPNNhg0b5tYxtYKdSBWyeSHMO/X/3TP/+nCud/xxw2eZtLMx+YV2AFqEBzDqslgGtovE28NS2dWKiNRKNT071fTzE6k2zpELAQ70eocX9zXj67+SOPVfnR0bBnFXj0b0igvDUvKEhiIiUo4qZHXj/Px81q5dS3x8/OkdmM3Ex8ezcuVKt/aRk5NDQUEB9erVK8uhRaSqiBsIg2eDNaLodmskpsGzuXXk/Sx7rCejLovFz8vC1pRj/Hv+X/R48SfeWrqTzJyCop+zF0Lictgw3/lTK+CJiIiIVA/nyIUMnk2DS4fw31vas2TMFQzu1ABPi4m1e49yz4drufqVpXywcg/H8/+W/ZQNRUQMU6Y7CZOSkoiKimLFihV069bNtf2xxx7j559/ZtWqVaXu41//+hfffvstmzZtwsfHp9gxeXl55OXluX632WxER0frarFIVWIvdK5Wl5XqnJOmYXcwF71TMDOngI9X72PmikRSbc7/T/t5WRhySTR3XBpLdMoPkPA42JJOf8gaCX1fdIZOERE5LzX9Truafn4i1Y4buRAgzZbLrJV7+PC3fWQed144DvLz5PauDbm9Wwwh+79VNhQRqQDuZiePSqyJF154gTlz5rB06dISG4QAkydPZtKkSZVYmYiUmdkCsT3OOSTQz5N7r2zMqMti+erPJKYv383WlGO8/+seUn6bx1ueU4C/zWRjS3Y+tjJ4tsKgiIiISHXgRi4ECLX68O8+LfjXlU34dM1+3vs1kf3px3njx53sXj6H/1peBZQNRUSMUqbHjYODg7FYLKSmFl3GPjU1lfDw8HN+9uWXX+aFF17gu+++o02bNuccO27cODIzM12v/fv3l6VMEalivDzM3NixAd881IPZd3Tm8iZBPOUxG4ejuKmuT97cnDBWj5eIiIiI1EB1vD0YcWksSx/tyVu3dqB9gwCeMM9UNhQRMViZmoReXl507NiRJUuWuLbZ7XaWLFlS5PHjv3vppZd45plnSEhIoFOnTqUex9vbG6vVWuQlItWfyWTi8mYhzL66kEhTOiXPVe0A20HnYysiIiIiUiNZzCauaR3B5/1RNhQRqQLK1CQEGDNmDNOnT2fWrFls2bKFe++9l+zsbEaOHAnAsGHDGDdunGv8iy++yFNPPcWMGTOIiYkhJSWFlJQUsrKyyu8sRKR6yUotfQywfdcOyrgAu4iIiIhUM6asNLfG7duXWMGViIjUbmWek3DIkCEcOnSI8ePHk5KSQrt27UhISCAsLAyAffv2YTaf7j2+/fbb5Ofnc9NNNxXZz4QJE5g4ceKFVS8i1ZN/mFvDxv94hPQNy/hH54u4vn0DAv08K7gwEREREal0bmbDx75NJW/Tr/yj80Vc2yYSX6+zF0cREZHzV6bVjY2iFexEahh7IUxp5ZyImrP/CnJgItMzhEtzXye7wPm+t4eZ/q0juKXLRXRqGITJVMzzKG6urCciUtPV9OxU089PpNZxIxtmeITQ9fhr5BU6M2CAtweD2kcxtHM0LSMDi9+ncqGICOB+dlKTUESMsXmhc6U6oGgYPNn8GzybzNh+LPjjIJ+s3sfWlGOuEU1C/Rl6STQ3dmhAUB2v0/tLeBxsSad3ZY2Evi9qJTwRqXVqenaq6ecnUiu5kQ3Tonvz6ZoDzP19P/vSc1wj2jYI5JbOFzGgbSR1vD2UC0VE/kZNQhGp+ooNcFHQ94UiAc7hcLB+fwafrN7HV38mc7zAubKdl8VM31bh/Ct8M81/vg/TWVeeT4dKBUIRqU1qenaq6ecnUmu5mQ3tdgcrdh3hk9X7+G5zCgWFzgxYx8vCuNid3Lr3SZz3H55JuVBEai81CUWkeijjoyDHcgv4cn0Sn6zex6YkG2bs/OL9IOGm9BJWYjI5rxyP3qBHTESk1qjp2ammn59IrVbGbHg4K4/P1h5gzu/72Xv4mDMXUtJKycqFIlI7uZudyrxwiYhIuTJbILaH28MDfDy5rWtDbuvakA0HMvntpwVE7ko/xyccYDvoDJtlOI6IiIiIGKCM2TDY35t/XtGYuy9vxOYVi4n8XrlQROR8FX/jjYhINdC6QSB3tavj1lj7sZQKrkZEREREjGIymWhpPe7W2N17dlENHqgTEal0upNQRKo3/zC3ht2/MImo/ZsZ2DaKVlHW4ldHFhEREZHqy81c+MT3h0hZs5SB7aIY2DaSJqH+FVyYiEj1oCahiFRvDbs755axJcNZC5c4t6RQn4SsRtiXJzJ9eSKxwXUY0CaCAW0jaRoWcPY+yzgXjoiIiIhUAaXmQhMZHiFstLck60gObyzZwRtLdnBxhJWBbSO5tk0E0fX8zt6vsqGI1BJauEREqr/NC2HesJO/nPlXmvNuwYKbZrKErnz1VxJLtqSSW2B3jWgRHsCAtpEMbBvpDIXFrqoXCX1f1Ep4IlJt1PTsVNPPT0QuQCm5kMGzyW58Dd9tTuGrP5NZtv0QJ+ynx3W4qC4D2kbSv00EoQE+yoYiUiNodWMRqV2KDXBR0PeFIgEuO+8EP2xJZeH6JJbtOERB4em/Au8J3cTjtudxXmc+0+lQqTAoItVBTc9ONf38ROQCuZkLAY5m55OwKYWF65P4LfEIp/7r2GyCByK2MDr9WZQNRaS6U5NQRGqfMj4KkpGTT8LGFL76K4lVuw6xzOtBwknHXOx0hSbnVePRG/R4iYhUeTU9O9X08xORcnAejwin2XL5+q9kvvoriT/3pfOLt7KhiNQMahKKiJTB0c1LCJp3Q6njHMO/whR7eSVUJCJy/mp6dqrp5ycixkv76wdCP7+x9IHDv4bYHhVfkIjIBXA3O2nhEhERIKjwqFvjnpnzE57tQugdF0676LpYir+0fJomuhYRERGpdkJNGW6Ne+HTpfh0CKN3XDgXRwRgMikbikj1ZTa6ABGRKsE/zK1hm4/58c7Pu7nx7RV0fu4HHv30T77ZkExW3oliBi+EKa1g1rXw2SjnzymtnNtFRKqJqVOnEhMTg4+PD126dGH16tUljp0+fTo9evQgKCiIoKAg4uPjzxo/YsQITCZTkVffvn0r+jRERMrGzWy4PsOHKT/s4Jo3lnPpCz/y5IIN/LQtjdyCwrMHKxuKSBWnx41FRMB5VXdKK7AlU3QlvFNM2AMi+Sb+OxI2H2LptjSO5Z5uDHpaTHRtVJ+rW4Ry9cVhRKf8cHJlvb/vSxNdi0jFK6/sNHfuXIYNG8a0adPo0qULU6ZM4dNPP2Xbtm2EhoaeNf7WW2/l0ksvpXv37vj4+PDiiy/yxRdfsGnTJqKiogBnkzA1NZX333/f9Tlvb2+CgoIq/fxERErkZjb8/IpvSNh0iF92HiK3wO5619fTwmVNg7m6RShXtQgl9MB3yoYiYhjNSSgiUlabF54Mb1A0wJ0d3goK7fy+J50lW9JYsiWVPUdyXKPN2Fnl+xDBjiMU/8CJJroWkYpVXtmpS5cuXHLJJbz55psA2O12oqOjeeCBBxg7dmypny8sLCQoKIg333yTYcOcf7+OGDGCjIwMFixYcN51KRuKSKUoQzbMLShkxa7DJ7NhGim2XNdoM3ZW+42mvv2wsqGIGMLd7KTHjUVETokb6Ax71oii262RZ13d9bSY6d44mKeujWPpv3uy5JEr+M81F9Mlth5dLdsIKbFBCOAA20HnfDQiIlVUfn4+a9euJT4+3rXNbDYTHx/PypUr3dpHTk4OBQUF1KtXr8j2pUuXEhoaSvPmzbn33ns5cuRIudYuIlIuypANfTwtXNUijOeub83KcVex6MHLGNOrGW2j69LZvJXgEhuEoGwoIlWFFi4RETlT3EBo0b/ME0o3DvGncYg/d13eiJw1e+Hr0g+1Ydt2GkV2o463G38Va5JrEalkhw8fprCwkLCwovNyhYWFsXXrVrf28fjjjxMZGVmk0di3b19uuOEGYmNj2bVrF0888QT9+vVj5cqVWCzF/72Wl5dHXl6e63ebzXYeZyQich7OIxuaTCZaRgbSMjKQB69uSubqvbC49ENt3bmDmAbd8fF0I+MpG4pIBVCTUETk78wWiO1x3h/3qx/l1rjnlqWzdvl3dGwYxOXNQujRJIS4SOvZKyZvXggJj4Mt6fQ2ayT0fVFz14hIlfXCCy8wZ84cli5dio+Pj2v70KFDXf/cunVr2rRpQ+PGjVm6dClXX311sfuaPHkykyZNqvCaRUSKdYHZMDAk2q1xE386wvpl39G1UX16NA2hR9Ngmob6n71isrKhiFQQNQlFRMpbw+7OoFbCRNcOTGR6hpDs246Co/n8tjud33an8xLbsPp40Dm2Pt0b16db4/o0T/8J86fDz96PLdk5R44muRaRChIcHIzFYiE1NbXI9tTUVMLDw8/52ZdffpkXXniBH374gTZt2pxzbKNGjQgODmbnzp0lNgnHjRvHmDFjXL/bbDaio937j24REcO5kQ0zPELY49GG3KwTLN12iKXbDgEQ7O9F10bOXNitUX1iDy3BNE/ZUEQqhpqEIiLlzWxxXsmdNwznxNZFJ7o2AXWvf4Wf43qx53A2y3cc4ufth1m1+wi23BP8sCWVH7akYsbOCp+HCcNRzBw2Due+E8Y6H4HR4yUiUs68vLzo2LEjS5YsYdCgQYBz4ZIlS5Zw//33l/i5l156ieeee45vv/2WTp06lXqcAwcOcOTIESIiIkoc4+3tjbe3d5nPQUSkSnAjGwbd8AorL+7NttRjLN9+mGU7DvH7nnQOZ+Xz9V/JfP1XMmbsrPR5mFBlQxGpIGoSiohUhFMTXRf7KMgLriu8McF1iAmuw+3dYjhRaGdTko2Vu4+wctcR2LOccM41mf8Zk1yX5REYzWEjIm4aM2YMw4cPp1OnTnTu3JkpU6aQnZ3NyJEjARg2bBhRUVFMnjwZgBdffJHx48fz8ccfExMTQ0pKCgD+/v74+/uTlZXFpEmTuPHGGwkPD2fXrl089thjNGnShD59+hh2niIiFc6NbGgCWoRbaRFu5a7LG5F3opA/92eyctcRVuw6jOf+XwlTNhSRCqQmoYhIRSnjRNceFjNto+vSNrou91zRmBN/7oEvSj/MO4tWYG8VySUxQbRuEIi3xzlCneawEZEyGDJkCIcOHWL8+PGkpKTQrl07EhISXIuZ7Nu3D7PZ7Br/9ttvk5+fz0033VRkPxMmTGDixIlYLBb++usvZs2aRUZGBpGRkfTu3ZtnnnlGdwqKSM1Xxmzo7WGhc2w9OsfW46H4puSv3w8LSj/MjG9XQqsGXBJTj4sjAvCwmEserGwoImcwORyOsydFqGJsNhuBgYFkZmZitVqNLkdEpHIkLodZ15Y6bGj+k/xmjwPAy8NMuwZ16RQTxCUx9ejQMIhAX0/nwM0LTz7m8ve/9k8+sKI5bERqjJqenWr6+YmIFOs8sqGfl4UOFwXRKSaIzjH1aHdRXfy8Tt4rpGwoUmu4m510J6GISFXlxiTXBXXC6XXVIOrutbFmr3PemtV70lm9Jx3YhckEzUIDaN8ggKd2PYqf5rARERERqZ7cyIb5fuFcccVAfPdksmbvUY7lnuCXnYf5ZedhACxmE3ERVto3CGDstkfxVTYUkTOoSSgiUlW5Mcm1V/+XGBXXlFGAw+Fgz5Ecfk9M5/c96azZe5TEw9lsSz1G0KFV1PFKLfYwTucxh43mrxERERGpPG5kQ+9rX+LeuObcC9jtDranHeP3PUdZsyedNXuOcjDjOBsOZlIneSV+yoYi8jdqEoqIVGVuLoACYDKZiA2uQ2xwHQZfEg3AoWN5/LHvKHl/7ISdpR9uye9/UccRR8tIKwE+niUP1Pw1IiIiIpWvDNnQbDa5FkK5vWtDAA5mHOePfUcp+GMHJJZ+uJ/XbsDf3Iq4CCu+Xpr3WqSm05yEIiLVwYVemT2POWxig+vQKiqQVpHWkz8DCfTz1Pw1ItVATc9ONf38RERKVcnZ0GyCJqH+rkzYukEgcRFW6nh7KBuKVAOak1BEpCYxW9x/1KM4bsxhk+UdSt1GlxOVlM3BjOMkHs4m8XA2X/15+opwwyAvFhSMoW55z1+jx1NERERE3FcZ2dArFL+LLiM4KZvDWXlsT81ie2oWn687CIDJBI3r+/BprrKhSE2hJqGISG3gxhw2Ade9zLS4LgCkZ+ez8WAmG5MynT8P2tiXnkNE5nqCvA6d40DO+WsKE3/F0vhy92rT4ykiIiIilcudbDjoZWbEdQMg1ZbLxoOZbDiZCzclZZKcmUtw+jq3sqF9z6+YGykbilR1etxYRKQ2KTZ0RZ01h01xMnMKSP31A5r9+nCph3n4xANsC+lDi4gALg630iIigBbhVkICvM+uR4+niJS7mp6davr5iYhUmgvIhoeO5XFoxYfErRxT6mEesT/IzrC+XBweQIvwAFpEWGkRHkBdP6+z61E2FCl3etxYRETOFjfQ+bjHeTy+EejnSWCTpvBr6YdJtgeyOdnG5mQbcNC1PdjfixbhVpqHB9AsxJfrf/43nuX5eIoeTRERERFx3wVkw5AAb0KaNYOVpR/m4Akrf+7P4M/9GUW2RwT60Dw8gObhATQN8WPgT48pG4oYSE1CEZHa5kLmsCll/how4bBG8n/D7mFLajZbU46xNcXG1uRjJB7J5nBWPr/sPMwvOw/T1byZIV4p5ziY8/EU9q5wr149miIiIiJSdpWQDZ+79W62puawNcXGlmRnPjxw9DjJmbkkZ+aydNshupo3c5NX8jkOpmwoUtHUJBQREfeVMn8NgKnvC0QHBxAdHEDvluGud4/nF7I99WTTMOUYwYkbIL30Q772xXLSYgJpEhpAo+A6xAbXoUGQLx4W8+lBJT2aYkt2btejKSIiIiLlz81s2DgskMZhgfRvE+F615ZbwPaUY2xJOcaO1GOEJP4FGaUf8r9f/kJaTBBNw/yJPZkNIwN9MZvPuP9Q2VDkvKhJKCIiZRM30Bmsir0yW/L8Nb5eFtpG16VtdF3nhsR0mFX64VYd9uS3tP1FtnmYTVxUz88ZDOv78PDGR/HTqnoiIiIile88s6HVx5NOMfXoFFPPuSHxqFvZ8Nc0D35L2Vtkm5eHmZj6fsTUr0OjYB8e/OtRfJUNRcpMTUIRESm7C5i/xqWUx1McmDhRJ4JbrhvCJYeOszMti8TD2ew5kk1ugZ3dh7PZfTibrubN1PFKPceBnI+mHN70E/VaXl30KnNJ9HiKiIiIiPsqKRsW1Ann5msH086VDbPYl55D/gk721Oz2J6aRVfzZvzcyIZHty6l7sVXYTIpG4qcoiahiIicnwuZv+bU58/xeIoJ8Oz/ItfFXVTkY3a7gxRbLomHs0k8nI3fth2QWPrhnv7kJxJMBTQI8qVBPT8uqufLRfX8iA7yI7qeHxfV98Pq41n+j6foqrOIiIjUBpWQDb36v8SNcQ2LfOxEoZ2kjFwSj2STeCgL/x3bYU/ph5vw4Y98Z8knOsjPmQlPvpz/7Et0kB91vD2UDaVWUZNQRESMcx6Pp5jNJiLr+hJZ15dLmwRDWEe3moRHTEHkF56+A7E4QT5mvjM/THB5PZ6iq84iIiIi7juPbOhhMXNRfecF3yuahUBkJ7eahIeoS26BnR1pWexIyyp2TGgdC4sdD1Nf2VBqCZPD4ShuCaIqxWazERgYSGZmJlar1ehyRESkvF3IFVV7IUxpdc5V9bBGcuKBP0nJKmBfeg7703PYn36cfek5rt+PZOfT1byZOV7PlnrI/wt/hayIrkTW9SUqyNmwjKrrS4i/9+nHmUu66nwqYp7PhNm68ixuqunZqaafn4hIrVcJ2TD//j9JPpbvyoP70nM4cEY+zDxe4HY2fDXyVbIiuhFZ14eoM/Jh/Tpepx9nVjYUA7mbnXQnoYiIGO9CHk9xY1U9+r6Ah6cnDYI8aRDkB43P3k123gkyVh+BJaUfcv++RBbuiThru6fFRHigD1FWL6YdHkNgeU6YXd5XnhUqRUREpKqqhGzo5eVJw/qeNKxfp9jdZB4vwLb6CPxU+iH37NnNwt3hZ2338jA7m4aBnryZWoWzoXKhnKQmoYiIVH/nuaremep4e1CnQaxbh7v20vZEWRqTlHH85CuXFFsuBYUO9qcfJypjLXW9Dp1jD84Js1+e/j628K6EWX0Is/oQbvUhzOpNWKAPAd4epV95Pt+5cPSoi4iIiNRk5ZANA309CbzIvWx4Tbe2hFsacdCVDY+TdiyP/BN2Eg9nE5a+2a1s+Np7M8kM60p44MlM6MqHPs75EU8pz2yoXChnUJNQRERqhkpYVe/U4ym9+11P77/t90ShnbRjeSRlHIeNB2FN6YfbtzeRhYln35EI4OdlcTYP/T14+/AY6pZw5dmBCVNZ58LR5NsiIiJS01ViNuzb/0b6/m2/+SfspNpyOZhxHPPGA7Cu9MMlJu5m4a6z70gECPD2ICzQmQ3fTCunbFjeuRCUDas5NQlFRKTmqOBV9QDn1edigo6HxexaUAXHxW41Ca+9tB3RHo1JteWRasslJTOXVFsuttwT5OQXuq48B53jyrPp5JXnR15+m6S6nQgJ8CbY3/vkTy/X78H+3gT5mvFOeJziQ24VmHxboVJERETKk4HZ0MvD7FoxGVOcW03Ca7q1JdKjMak2ZyZMseWSmplLdn4hx/JOcCwti+DD7mXDx16ZRlJQp7My4amf9X0thHzzuHP8WarAI9CgbGgANQlFRETOVA6Pp7h/R+INZ92RCJCTf4JUWx4pmbl4bUlyq+FYkJHMyvQj5xzjnHw76RwjnKFy2+pv8Wx8OfXreBPg43F6MZYzVeVHoBUoRUREpLxUYjYs7o5EgKy8E66LyV5bDsLa0g+ZezSJX44cLvF9d3Ph2uWLMMX2oH4dL4LqeBWdEudMyoY1gpqEIiIif3ehj6dcwFVnAD8vD2KDPYgNrgPmFm41Ce+6phtX1WnH4aw8Dh3L49DJn4ez8jl0LI+jOfmEkuFW+VMX/spCu7Nmi9lEkJ8n9ep4EeTnRX1/L+r5Wnhs6yMEnOMxFxLGYjLiEWhdwRYREZHyZnA29Pf2oEmoP01C/cFysVtNwlH9unKFX1tXNjyc5cyHh4/lcygrj9DcDLdKn/XtKhbaT9flaTER5OdFvTrOV1AdL+r7Wvj3lkfwr4rT4ygblonJ4XAU18auUtxdqllERKRKKTaURLl/1RmcQWRKq1KvPDN6wzkDit3uIGf7UvznDCr1kA96P8OPuc3JyjtR7PvOK8/Plrqff1omsbtOewJ9Panr50mgr5frn52/exLobab7Vz3xzEkuJlS6f35AyYHy1J5r0RXsmp6davr5iYhIDVWFsmHh7mVYZg8o9XDjrJNZlt+Cozn55OQXFjvG3Wx4r8ckdtfpQODJHFjX94xM6OdF3ZPZsOtXV+KZrWxYntzNTrqTUEREpKKUx4TZF3jl2bUbswn/Zpe79ajLG6PvA7OFvBOFHM0uID073/nKyedodj4he3bD9tJL9z5+iB3ZWecc09W8mSu9ks8x4vRK0MlBlxDg44HVxwOrrycBPh4E+Jz86WWm1aLHsJRwBfu85lusqlewRUREpHqqQtnQEnOpW7lw8uh/uvaVW1B4OheefB3Jzid87y7YUXrpnjmH2JZ17Jxjupo3c7kb2fDV/80kOajT6Szo44HVxxOr78l86GUibrGyYVmpSSgiIlKRLnTCbCifuXBO1VKGUOntYSE80EJ4oE/R/US0c6tJ+PANPRhatxOZxwvIOF5ARk4BmccLyDye79yWU0DbjDzILn1f51oJGk5dwS49UL723kyS6naijrcHdbwtzp9eHtTx9sDf24Kflwd1PE20WfQYHuURKiti1UARERGpvqpKNjyPZqOPp+X0Qn1nimrvVpPw4et7MNiVDfPJyCnAdjITZpzMh+0yciGn9H3t2bObhbuLXwka3M+GU2bMIqlux5NZ0AM/L2cmrOP6Zw/qeEKrWpIN1SQUERGpDsrjyvOp/VTS5NuxHXoRW1p9iQUw6/9KPeSAS9tzcZ0WHMst4FjuiTN+nsCWW0Dz7BwoKL30xMTdLLSXHCjB/VD53NvvsTegA3W8PfD1suDnacHPy4Kvlwd+Xhb8POHaH/+NT3ldwRYRERE5pTyyYXldiHY3G3Z0JxuegFkvl3rIay9tR3O/5n/LhafzYfPsbCh+5pwidu/exUJ72DnHlCUb7rd2PJkH/5YLvSz4elT9bKgmoYiISHVRHleewfDJt4twM1T26nc9vc61v0QTzHq11MP1v7QdLeo0JzvvBNl5hc6f+SfIyiskJ+8EWXknaJGTA/mll556cC/f2aNKfL+reTM3e6WcYw/OQMneFeXz5yoiIiK1S3ndlVhFHoEG3M6GvfvdQO9Ss+FrpR7umm5tae5/KhuezIT5zkx4Ki+2yHHvYnTqwb0k7K/e2VBNQhERkdroQkOlQY9Al8jNQNmn3w2l78vNhuNNV3bkEmsrjuefICe/kOP5heScfB0vOEHroxvgcKm7cQZyEREREaNUlUegT9VSidmwb/8byy0b3nhFRy6xtjwjDzobjqdyYpujG+BIqbsxNBueV5Nw6tSp/N///R8pKSm0bduW//73v3Tu3LnE8Z9++ilPPfUUe/bsoWnTprz44otcc8015120iIiIVAFV6RFoA65gXx5/3bn3l5gDs0o/HP7nfsTFaOWd+xwOBxMmTGD69OlkZGRw6aWX8vbbb9O0adPKOB0RERGpKLU8G17Rq/pnQ3NZPzB37lzGjBnDhAkTWLduHW3btqVPnz6kpaUVO37FihXccsstjBo1ij/++INBgwYxaNAgNm7ceMHFi4iIiMFOXXlufZPz5/nOnxI3EEZvhOFfw43vOX+O3lC2iZtPBUrr3xY4sUaWbRLoU6ES4KwZY87jCnYxs8649mWNco6roioi97300ku88cYbTJs2jVWrVlGnTh369OlDbm5uZZ2WiIiIVBRlw5JVg2xocjgcxbVBS9SlSxcuueQS3nzzTQDsdjvR0dE88MADjB079qzxQ4YMITs7m6+//tq1rWvXrrRr145p06a5dUybzUZgYCCZmZlYrdaylCsiIiK1jb3wwq9gg3P1ubOuYEeV7ZEZ1wp2UOwV7Apawa68slN55z6Hw0FkZCSPPPIIjz76KACZmZmEhYUxc+ZMhg4dWqnnJyIiIrWAsqHb2alMjxvn5+ezdu1axo0b59pmNpuJj49n5cqVxX5m5cqVjBkzpsi2Pn36sGDBghKPk5eXR15enut3m81WljJFRESkNqsqC7yc2kd5zM9jgIrIfYmJiaSkpBAfH+96PzAwkC5durBy5coSm4TKhiIiInLelA3dVqYm4eHDhyksLCQsrOjz0WFhYWzdurXYz6SkpBQ7PiWl5BVdJk+ezKRJk8pSmoiIiEj5qyqrBhqgInLfqZ/KhiIiIlIt1fBsWOY5CSvDuHHjyMzMdL32799vdEkiIiIi56+85ueppZQNRUREpEapotmwTHcSBgcHY7FYSE0tuhxzamoq4eHhxX4mPDy8TOMBvL298fb2LktpIiIiIlKOKiL3nfqZmppKREREkTHt2rUrsRZlQxEREZGKV6Y7Cb28vOjYsSNLlixxbbPb7SxZsoRu3boV+5lu3boVGQ/w/ffflzheRERERIxXEbkvNjaW8PDwImNsNhurVq1SNhQRERExWJnuJAQYM2YMw4cPp1OnTnTu3JkpU6aQnZ3NyJEjARg2bBhRUVFMnjwZgIceeogrrriCV155hf79+zNnzhzWrFnDu+++W75nIiIiIiLlqrxzn8lkYvTo0Tz77LM0bdqU2NhYnnrqKSIjIxk0aJBRpykiIiIinEeTcMiQIRw6dIjx48eTkpJCu3btSEhIcE1AvW/fPszm0zcodu/enY8//pgnn3ySJ554gqZNm7JgwQJatWpVfmchIiIiIuWuInLfY489RnZ2NnfffTcZGRlcdtllJCQk4OPjU+nnJyIiIiKnmRwOh8PoIkpjs9kIDAwkMzMTq9VqdDkiIiIiVVpNz041/fxEREREypO72alKrm4sIiIiIiIiIiIilUdNQhERERERERERkVquzHMSGuHUE9E2m83gSkRERESqvlOZqRrMKnNelA1FRERE3OduNqwWTcJjx44BEB0dbXAlIiIiItXHsWPHCAwMNLqMcqdsKCIiIlJ2pWXDarFwid1uJykpiYCAAEwmU4Uey2azER0dzf79+2v9RNj6Lk7Td3GavovT9F0Upe/jNH0Xp+m7OK0yvwuHw8GxY8eIjIwssvpwTVFZ2VD/+y1K38dp+i5O03dxmr6LovR9nKbv4jR9F6dVxWxYLe4kNJvNNGjQoFKPabVaa/3/YE/Rd3GavovT9F2cpu+iKH0fp+m7OE3fxWmV9V3UxDsIT6nsbKj//Ral7+M0fRen6bs4Td9FUfo+TtN3cZq+i9OqUjaseZeWRUREREREREREpEzUJBQRERERERH5//buNTaKuovj+CmFbYFAC6H0olguSpFCQdQ2RQklFEptCH0jhQgBA2oIJDaKyhutyAuKEomaRtQAxVtrlVuiyL0LEUtJoESKSAALiFIIxELLRbF7nhc8nWHYXtilZXd2vp+kgZ09O/zneJj8/LvuAoDDsUl4h4iICCkoKJCIiIhALyXg6IWJXpjohYleWNEPE70w0QsTvbAf/plZ0Q8TvTDRCxO9sKIfJnphohemYOyFLb64BAAAAAAAAEDH4Z2EAAAAAAAAgMOxSQgAAAAAAAA4HJuEAAAAAAAAgMOF/CZhUVGR9O/fXyIjIyUtLU3279/fav23334rQ4YMkcjISBk+fLhs3rzZ8ryqyltvvSXx8fHStWtXyczMlOPHj3fkJbQbX3rx2WefyZgxY6RXr17Sq1cvyczM9KqfPXu2hIWFWX4mTZrU0ZfRbnzpR3Fxsde1RkZGWmqcMhsZGRlevQgLC5OcnByjxq6zsWfPHpk8ebIkJCRIWFiYbNy4sc3XuN1uGTVqlERERMjDDz8sxcXFXjW+3oeCga+9WL9+vUyYMEFiYmKkZ8+ekp6eLlu3brXUvP32215zMWTIkA68ivbhay/cbnezf0dqa2stdU6Yi+buBWFhYZKcnGzU2HUuli5dKk8++aT06NFD+vbtK7m5uXLs2LE2XxfKOcMuyIYmsqGJXGhFNiQX3olsaCIbmsiGplDJhiG9SfjNN9/IK6+8IgUFBXLw4EEZMWKEZGVlyYULF5qt//nnn2X69OkyZ84cqaqqktzcXMnNzZXq6mqj5t1335UPP/xQVq5cKZWVldK9e3fJysqSGzdu3K/L8ouvvXC73TJ9+nQpLy+XiooK6devn0ycOFH+/PNPS92kSZPk3Llzxk9JScn9uJx75ms/RER69uxpudbTp09bnnfKbKxfv97Sh+rqagkPD5dnn33WUmfH2bh69aqMGDFCioqK7qq+pqZGcnJyZNy4cXLo0CHJz8+XuXPnWgKQP7MWDHztxZ49e2TChAmyefNmOXDggIwbN04mT54sVVVVlrrk5GTLXPz0008dsfx25Wsvmhw7dsxyrX379jWec8pcfPDBB5Ye/PHHH9K7d2+v+4Ud52L37t0yf/582bdvn2zfvl1u3rwpEydOlKtXr7b4mlDOGXZBNjSRDU3kQiuy4S3kQiuyoYlsaCIbmkImG2oIS01N1fnz5xuPGxsbNSEhQZcuXdps/dSpUzUnJ8dyLC0tTV966SVVVfV4PBoXF6fvvfee8XxdXZ1GRERoSUlJB1xB+/G1F3f677//tEePHrp27Vrj2KxZs3TKlCntvdT7wtd+rFmzRqOiolo8n5NnY8WKFdqjRw9taGgwjtl5NpqIiG7YsKHVmtdff12Tk5Mtx/Ly8jQrK8t4fK/9DQZ304vmDB06VBcvXmw8Ligo0BEjRrTfwgLgbnpRXl6uIqJ///13izVOnYsNGzZoWFiYnjp1yjgWCnOhqnrhwgUVEd29e3eLNaGcM+yCbGgiG5rIhVZkQ2/kQiuyoYlsaCIbWtk1G4bsOwn//fdfOXDggGRmZhrHOnXqJJmZmVJRUdHsayoqKiz1IiJZWVlGfU1NjdTW1lpqoqKiJC0trcVzBgN/enGna9euyc2bN6V3796W4263W/r27StJSUkyb948uXTpUruuvSP424+GhgZJTEyUfv36yZQpU+TIkSPGc06ejVWrVsm0adOke/fuluN2nA1ftXXPaI/+2pXH45H6+nqve8bx48clISFBBg4cKM8995ycOXMmQCvseCNHjpT4+HiZMGGC7N271zju5LlYtWqVZGZmSmJiouV4KMzF5cuXRUS8Zv52oZoz7IJsaCIbmsiFVmRD/5ELW0c2JBs2h2wYfDkjZDcJL168KI2NjRIbG2s5Hhsb6/X//jepra1ttb7pV1/OGQz86cWd3njjDUlISLAM56RJk+Tzzz+XnTt3yrJly2T37t2SnZ0tjY2N7br+9uZPP5KSkmT16tWyadMm+fLLL8Xj8cjo0aPl7NmzIuLc2di/f79UV1fL3LlzLcftOhu+aumeceXKFbl+/Xq7/N2zq+XLl0tDQ4NMnTrVOJaWlibFxcWyZcsW+fjjj6WmpkbGjBkj9fX1AVxp+4uPj5eVK1fKunXrZN26ddKvXz/JyMiQgwcPikj73JPt6K+//pIff/zR634RCnPh8XgkPz9fnnrqKRk2bFiLdaGaM+yCbGgiG5rIhVZkQ/+RC1tHNiQb3olsGJw5o3OHnBUhpbCwUEpLS8Xtdls+lHnatGnG74cPHy4pKSkyaNAgcbvdMn78+EAstcOkp6dLenq68Xj06NHy6KOPyieffCJLliwJ4MoCa9WqVTJ8+HBJTU21HHfSbMDb119/LYsXL5ZNmzZZPmslOzvb+H1KSoqkpaVJYmKilJWVyZw5cwKx1A6RlJQkSUlJxuPRo0fLyZMnZcWKFfLFF18EcGWBtXbtWomOjpbc3FzL8VCYi/nz50t1dbUtPi8HaA9Oz4bkwpaRDdEcsiHZsDlkw+AUsu8k7NOnj4SHh8v58+ctx8+fPy9xcXHNviYuLq7V+qZffTlnMPCnF02WL18uhYWFsm3bNklJSWm1duDAgdKnTx85ceLEPa+5I91LP5p06dJFHnvsMeNanTgbV69eldLS0ru6UdtlNnzV0j2jZ8+e0rVr13aZNbspLS2VuXPnSllZmddb5+8UHR0tgwcPDrm5aE5qaqpxnU6cC1WV1atXy8yZM8XlcrVaa7e5WLBggXz//fdSXl4uDz74YKu1oZoz7IJsaCIbmsiFVmRD/5ELm0c2bB7ZkGwoEpw5I2Q3CV0ulzz++OOyc+dO45jH45GdO3da/svf7dLT0y31IiLbt2836gcMGCBxcXGWmitXrkhlZWWL5wwG/vRC5Na36CxZskS2bNkiTzzxRJt/ztmzZ+XSpUsSHx/fLuvuKP7243aNjY1y+PBh41qdNhsit76q/Z9//pEZM2a0+efYZTZ81dY9oz1mzU5KSkrk+eefl5KSEsnJyWmzvqGhQU6ePBlyc9GcQ4cOGdfptLkQufVtbydOnLirf3G0y1yoqixYsEA2bNggu3btkgEDBrT5mlDNGXZBNjSRDU3kQiuyof/Ihd7Ihi0jG5INRYI0Z3TI16EEidLSUo2IiNDi4mL99ddf9cUXX9To6Gitra1VVdWZM2fqokWLjPq9e/dq586ddfny5Xr06FEtKCjQLl266OHDh42awsJCjY6O1k2bNukvv/yiU6ZM0QEDBuj169fv+/X5wtdeFBYWqsvl0u+++07PnTtn/NTX16uqan19vS5cuFArKiq0pqZGd+zYoaNGjdJHHnlEb9y4EZBr9IWv/Vi8eLFu3bpVT548qQcOHNBp06ZpZGSkHjlyxKhxymw0efrppzUvL8/ruJ1no76+XquqqrSqqkpFRN9//32tqqrS06dPq6rqokWLdObMmUb977//rt26ddPXXntNjx49qkVFRRoeHq5btmwxatrqb7DytRdfffWVdu7cWYuKiiz3jLq6OqPm1VdfVbfbrTU1Nbp3717NzMzUPn366IULF+779fnC116sWLFCN27cqMePH9fDhw/ryy+/rJ06ddIdO3YYNU6ZiyYzZszQtLS0Zs9p17mYN2+eRkVFqdvttsz8tWvXjBon5Qy7IBuayIYmcqEV2fAWcqEV2dBENjSRDU2hkg1DepNQVfWjjz7Shx56SF0ul6ampuq+ffuM58aOHauzZs2y1JeVlengwYPV5XJpcnKy/vDDD5bnPR6PvvnmmxobG6sRERE6fvx4PXbs2P24lHvmSy8SExNVRLx+CgoKVFX12rVrOnHiRI2JidEuXbpoYmKivvDCC0F/E7udL/3Iz883amNjY/WZZ57RgwcPWs7nlNlQVf3tt99URHTbtm1e57LzbJSXlzc7903XP2vWLB07dqzXa0aOHKkul0sHDhyoa9as8Tpva/0NVr72YuzYsa3Wq6rm5eVpfHy8ulwufeCBBzQvL09PnDhxfy/MD772YtmyZTpo0CCNjIzU3r17a0ZGhu7atcvrvE6YC1XVuro67dq1q3766afNntOuc9FcH0TEcg9wWs6wC7KhiWxoIhdakQ3JhXciG5rIhiayoSlUsmHY/y8GAAAAAAAAgEOF7GcSAgAAAAAAALg7bBICAAAAAAAADscmIQAAAAAAAOBwbBICAAAAAAAADscmIQAAAAAAAOBwbBICAAAAAAAADscmIQAAAAAAAOBwbBICAAAAAAAADscmIQAAAAAAAOBwbBICgJ8yMjIkPz8/0MsAAABAECAbArA7NgkBAAAAAAAAhwtTVQ30IgDAbmbPni1r1661HKupqZH+/fsHZkEAAAAIGLIhgFDAJiEA+OHy5cuSnZ0tw4YNk3feeUdERGJiYiQ8PDzAKwMAAMD9RjYEEAo6B3oBAGBHUVFR4nK5pFu3bhIXFxfo5QAAACCAyIYAQgGfSQgAAAAAAAA4HJuEAAAAAAAAgMOxSQgAfnK5XNLY2BjoZQAAACAIkA0B2B2bhADgp/79+0tlZaWcOnVKLl68KB6PJ9BLAgAAQICQDQHYHZuEAOCnhQsXSnh4uAwdOlRiYmLkzJkzgV4SAAAAAoRsCMDuwlRVA70IAAAAAAAAAIHDOwkBAAAAAAAAh2OTEAAAAAAAAHA4NgkBAAAAAAAAh2OTEAAAAAAAAHA4NgkBAAAAAAAAh2OTEAAAAAAAAHA4NgkBAAAAAAAAh2OTEAAAAAAAAHA4NgkBAAAAAAAAh2OTEAAAAAAAAHA4NgkBAAAAAAAAh2OTEAAAAAAAAHC4/wFvcCAxvCg5NgAAAABJRU5ErkJggg==", + "image/png": "iVBORw0KGgoAAAANSUhEUgAABQkAAAGGCAYAAADYVwfrAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8hTgPZAAAACXBIWXMAAA9hAAAPYQGoP6dpAACNvUlEQVR4nOzdeXhTVf7H8U+S7rQpW1coUBbByipKBUVFK1QRxBlFGZVFxZ+OG1ZHwVHAFXFFhRFldIBxFBAVUbCoKCKyySaryL51A0ob2tKFJr8/AimhWwpN06bv1/PkKbk5997vDYgfzr3nHIPNZrMJAAAAAAAAQL1l9HQBAAAAAAAAADyLTkIAAAAAAACgnqOTEAAAAAAAAKjn6CQEAAAAAAAA6jk6CQEAAAAAAIB6jk5CAAAAAAAAoJ6jkxAAAAAAAACo5+gkBAAAAAAAAOo5H08X4Aqr1aqUlBSFhITIYDB4uhwAAIBazWaz6fjx44qOjpbR6H33hMmGAAAArnM1G9aJTsKUlBTFxMR4ugwAAIA65cCBA2revLmny6h2ZEMAAICqqywb1olOwpCQEEn2izGbzR6uBgAAoHazWCyKiYlxZChvQzYEAABwnavZsE50Ep4eRmI2mwmCAAAALvLWobhkQwAAgKqrLBt63yQ1AAAAAAAAAKqETkIAAAAAAACgnqOTEAAAAAAAAKjn6sSchAAAwL2Ki4tVVFTk6TLgIl9fX5lMJk+XAQAAaimyXf1SXdmQTkIAAOoxm82mtLQ0ZWVleboUVFHDhg0VGRnptYuTAACAqiPb1V/VkQ3pJAQAoB47HSLDw8MVFBREh1MdYLPZlJeXp4yMDElSVFSUhysCAAC1Bdmu/qnObEgnIQAA9VRxcbEjRDZp0sTT5aAKAgMDJUkZGRkKDw9n6DEAACDb1WPVlQ2rvHDJ0qVLNWDAAEVHR8tgMGjevHmV7rNkyRJdfPHF8vf3V9u2bTV9+vRzKLUG5GRKUy6TJray/8zJ9HRFAAC4zel5aoKCgjxcCc7F6d83d803NGHCBF166aUKCQlReHi4Bg0apO3bt1e632effaYOHTooICBAnTp10sKFC50+t9lsGjt2rKKiohQYGKiEhATt2LHDLddwXgpPSPMflt68UHozTpr/qH0bAAC1FNmufquObFjlTsLc3Fx16dJFU6ZMcan9nj171L9/f/Xp00cbNmzQqFGjdO+992rRokVVLtatXmsnvR4rHd4mnThm//l6rH07AABejGEodZO7f99+/vlnPfjgg1q5cqW+//57FRUVqW/fvsrNzS13n+XLl2vIkCG65557tH79eg0aNEiDBg3S5s2bHW1effVVvfPOO5o6dapWrVqlBg0aqF+/fsrPz3fr9VTJp0OklyOldTMlS4pkOSStm27f9ukQT1cHAECFyHb1U3X8vhtsNpvtfAr48ssvNWjQoHLbPPXUU1qwYIFTOLz99tuVlZWl5ORkl85jsVgUGhqq7Oxsmc3mcy23fK+1k3Izyv+8Qbj0j1p4hxsAgPOQn5+vPXv2KDY2VgEBAZ4up1a66667dOGFF+rpp5+u0fPefvvtuvTSS/X444+X26ai3z93ZKfDhw8rPDxcP//8s6688soy29x2223Kzc3VN99849h22WWXqWvXrpo6dapsNpuio6P1+OOP64knnpAkZWdnKyIiQtOnT9ftt9/uUi1uzYafDpG2L6y4TfsbpCGfVu95AQA4T2Q79yosLFRcXJxmzpypXr161eh5L7jgAs2dO1eXXHJJue2qIxtW+UnCqlqxYoUSEhKctvXr108rVqxw96ldk5NZcQehZP+coccAANQrv//+uxYuXKhHHnmk3DaZmZl6+OGH1b59ewUGBqpFixZ65JFHlJ2dXart9OnTS025smTJEhkMhlIrED7zzDN66aWXyjyOp5yupXHjxuW2qSz37dmzR2lpaU5tQkNDFR8fXzuyYeGJyjsIJXsbhh4DAFAtXJ3iZO/evRo+fHjNF3jK1KlTFRsbW2EH4e+//64hQ4YoJiZGgYGBuvDCC/X222+X2Xb48OHau3ev07bx48era9euTtv8/Pz0xBNP6KmnnjrfS6iU2zsJ09LSFBER4bQtIiJCFotFJ06UHa4KCgpksVicXm7zn8TqbQcAALzCu+++q1tvvVXBwcHltklJSVFKSopef/11bd68WdOnT1dycrLuueceR5u33npLx48fd7w/fvy43nrrrQrP3bFjR7Vp00Yff/zx+V9INbBarRo1apQuv/xydezYsdx25eW+tLQ0x+ent5XXpiw1lg2/f8b1totGu6cGAADqmcqmOPnf//6nXbt2OdrbbDZNmTJFx44dq7EabTabJk+e7JTxyrJ27VqFh4fr448/1pYtW/TPf/5TY8aM0eTJkyXZbzBPmTJFZw7q3bVrl/73v/9VeNw77rhDy5Yt05YtW87/Yirg9k7CczFhwgSFhoY6XjExMe47WfY+19od2+2+GgAAQJVYrVZNmDBBsbGxCgwMVJcuXTR37lzZbDYlJCSoX79+jvCVmZmp5s2ba+zYsZJKnt5bsGCBOnfurICAAF122WVOU6MUFxdr7ty5GjBgQIV1dOzYUZ9//rkGDBigNm3a6JprrtFLL72kr7/+WidPnpQkNWrUSNddd52WLVumZcuW6brrrlOjRo20d+9e9enTx9HGYDA43R0fMGCAZs2aVZ1f2zl78MEHtXnzZo/VU2PZMLMKeW+7a9PmAACAiiUnJ2v48OG66KKL1KVLF02fPl379+/X2rVrJUmxsbEaNmyYpk6dqoMHDyoxMVGHDh2Sv7+/JCkrK0v33nuvwsLCZDabdc011+j333+XZJ8uJTIyUi+//LLjfMuXL5efn58WL14sqeTpvffff18xMTEKCgrS4MGDnUZ0rF27Vrt27VL//v0rvJa7775bb7/9tq666iq1bt1ad955p0aMGKEvvvhCkhQQEKBDhw4pMTFRBw8e1NSpUzV8+HDFxsZq+vTpeu655/T777/LYDDIYDA4RqE0atRIl19+uduzmI9bjy4pMjJS6enpTtvS09NlNpsdSzSfbcyYMUpKSnK8t1gs7guDPgHSSRcmyrYWSdZiyXhuy0gDAIDqM2HCBH388ceaOnWq2rVrp6VLl+rOO+9UWFiYZsyYoU6dOumdd97Ro48+qvvvv1/NmjVzdBKe9o9//ENvv/22IiMj9fTTT2vAgAH6888/5evrq40bNyo7O7vCeV/Kc3quFx8fe8waPny4rrnmGvXo0UOStHr1arVo0ULFxcX6/PPP9de//lXbt28vlY169Oihl156SQUFBY4Q7AkPPfSQvvnmGy1dulTNmzevsG15uS8yMtLx+eltUVFRTm3OHlpzphrLho1bS7t+dK1tbgbZEAAANzh7ipNevXrpp59+UkJCgn799Vd9/fXXuv766x3tb731VgUGBurbb79VaGio3n//fV177bX6888/FRYWpo8++kiDBg1S37591b59e91111166KGHdO211zqOsXPnTs2ZM0dff/21LBaL7rnnHv397393POH3yy+/6IILLlBISMg5Xc/pawkKCtLLL7+shQsXauDAgTp58qR+/PFH+fr6qlu3btq8ebOSk5P1ww8/SLJPy3Jajx499Msvv1T5/FXh9k7Cnj17auFC57ldvv/+e/Xs2bPcffz9/WsuDF/2sLTkBdfaLn1dutr9Y8ABAPAEm82mE0XFHjl3oK/J5RXZCgoK9PLLL+uHH35w5InWrVtr2bJlev/99/XJJ5/o/fff19ChQ5WWlqaFCxdq/fr1jk6708aNG6frrrtOkjRjxgw1b95cX375pQYPHqx9+/bJZDIpPDy8Stdx5MgRvfDCC7rvvvsc2z7++GNNnjzZced58ODBeuihh3TnnXc6AmN4eLgaNmzodKzo6GgVFhYqLS1NLVu2rFId1cFms+nhhx/Wl19+qSVLlig2NrbSfXr27KnFixdr1KhRjm1n5r7Y2FhFRkZq8eLFjk5Bi8WiVatW6YEHHij3uDWWDa97Ufrt3661tVnJhgCAWq2uZLszlTXFyapVq/SPf/xDvXr1kq+vryZNmqQVK1bo6aef1po1a7R69WplZGQ4ssLrr7+uefPmae7cubrvvvt0ww03aOTIkbrjjjt0ySWXqEGDBpowYYLTefPz8zVz5kw1a9ZMkn3amf79++uNN95QZGSk9u3bp+jo6Cpfz/LlyzV79mwtWLDAcZ6XX35Zq1at0tVXX61LLrlECQkJeu2119SjRw8FBwfLx8fHcWP1TNHR0dq3z8XRsOeoyp2EOTk52rlzp+P9nj17tGHDBjVu3FgtWrTQmDFjdOjQIc2cOVOSdP/992vy5Ml68skndffdd+vHH3/UnDlzHF+Qx13xiOudhKumSlc+wR1jAIBXOlFUrLixizxy7q3P91OQn2uxZOfOncrLy3N08J1WWFiobt26SbLfUf7yyy/1yiuv6L333lO7du1KHefMG5aNGzdW+/bttW3bNknSiRMn5O/v7xRuX375ZaehKlu3blWLFi0c7y0Wi/r376+4uDiNHz/esT0jI0Pff/+9Pv/8c0nSpEmTNG3atEqv8/RThXl5eZW2dYcHH3xQn3zyib766iuFhIQ45gwMDQ111DZ06FA1a9bMEbQfffRRXXXVVXrjjTfUv39/zZo1S2vWrNEHH3wgSTIYDBo1apRefPFFtWvXTrGxsXr22WcVHR2tQYMGeeQ6nfgFSu0SpR0uDiUmGwIAarG6ku3OdHqKk2XLljm27dixQ//5z39kMpk0fvx4/ec//9G//vUv5eXl6ffff1dOTo6aNGnidJwTJ044zWP4+uuvq2PHjvrss8+0du3aUjcfW7Ro4egglOw50Wq1avv27YqMjNSJEydKrRh8/fXXO57sa9myZan5Ajdv3qybbrpJ48aNU9++fSXZc11ERISSk5M1YsQI3X///Ro5cqRWrFjhGHVSnsDAQLfnwir/jq1Zs8Yxf44kx9CPYcOGafr06UpNTdX+/fsdn8fGxmrBggV67LHH9Pbbb6t58+b697//rX79+lVD+dXAx0+66C/Sli8qb3siU9q3XIrt7f66AABAmXJyciRJCxYscApzkhyBLy8vT2vXrpXJZNKOHTuqfI6mTZsqLy9PhYWF8vPzk2S/8Tl48GBHmzPvJh8/flyJiYkKCQnRl19+KV9fX8dnZw6TlaSQkJBS28qSmZkpSQoLC6ty/dXhvffekyRdffXVTtv/85//OOZO3L9/v4zGkimue/XqpU8++UTPPPOMnn76abVr107z5s1zWuzkySefVG5uru677z5lZWXpiiuuUHJycqng7TF3zJZeby/llL+QigPZEACAalPeFCd33nmnJDlWAjYYDHrwwQcl2XNhVFSUlixZUup4Z47S2LVrl1JSUmS1WrV371516tSpSrU1bdpUmzZtctr273//27Eg75nZT7LfTL722mt133336ZlnShZGa9y4saP209q0aaM2bdpUWkNmZqbbc2GVOwmvvvpqp1VYznZ6UsWz91m/fn1VT1Vz/vpv6Y8FUnFB5W1z0itvAwBAHRToa9LW5z1zEy/Q1/UnseLi4uTv76/9+/frqquuKrPN448/LqPRqG+//VY33HCD+vfvr2uuucapzcqVKx1PAh47dkx//vmnLrzwQklyDIXdunWr49eNGzd2DA8+k8ViUb9+/eTv76/58+eX29l15qIkp53ugCwuLj0UaPPmzWrevLmaNm1a5vHcraK8d1pZgfzWW2/VrbfeWu4+BoNBzz//vJ5//vnzKc+9krZKL0WRDQEAdVpdyXauTnHSqlWrUn1OF198sdLS0uTj46NWrVqVuV9hYaHuvPNO3XbbbWrfvr3uvfdebdq0yWlamf379yslJcVxE3jlypUyGo1q3769JKlbt2567733ZLPZHCNNzr5ZfdqWLVt0zTXXaNiwYXrppZfKve6y+s/8/PzKzIWSPRueHjXjLm6fk7BOMJqk3o9LS16uvG2QZ4I6AADuZjAYzmlYSE0LCQnRE088occee0xWq1VXXHGFsrOz9euvv8psNqtp06b66KOPtGLFCl188cX6xz/+oWHDhmnjxo1q1KiR4zjPP/+8mjRpooiICP3zn/9U06ZNHUNew8LCdPHFF2vZsmUVLqhhsVjUt29f5eXl6eOPP5bFYpHFYnEcw2SqOCC3bNlSBoNB33zzjW644QYFBgYqODhYkn2C7NNDU1DDqpINgyPcXw8AAOegrmQ7V6Y4KU9CQoJ69uypQYMG6dVXX9UFF1yglJQULViwQDfffLMuueQS/fOf/1R2drbeeecdBQcHa+HChbr77rv1zTffOI4TEBCgYcOG6fXXX5fFYtEjjzyiwYMHO+YG7NOnj3JycrRlyxanERJn27x5s6655hr169dPSUlJjmsxmUwuPQXYqlUrx7R+zZs3V0hIiGOkzC+//KIXXnBxurxzZKy8ST1x5RNSYOmnA0r56gFp63z31wMAAMr1wgsv6Nlnn9WECRN04YUXKjExUQsWLFCrVq10zz33aPz48br44oslSc8995wiIiJ0//33Ox3jlVde0aOPPqru3bsrLS1NX3/9tePJPkm69957HSvalWfdunVatWqVNm3apLZt2yoqKsrxOnDgQKXX0axZMz333HMaPXq0IiIi9NBDD0myT2o9b948jRw5sqpfDaqLK9nQYJTyjtZMPQAAeKn33ntP2dnZuvrqq52y1OzZsyvd12AwaOHChbryyis1YsQIXXDBBbr99tu1b98+RUREaMmSJZo0aZL++9//ymw2y2g06r///a9++eUXx9QqktS2bVv95S9/0Q033KC+ffuqc+fO+te//uX4vEmTJrr55psrzYZz587V4cOH9fHHHztdy6WXXurSd/HXv/5ViYmJ6tOnj8LCwvTpp59KklasWKHs7GzdcsstLh3nXBlsrowl8TCLxaLQ0FBlZ2fLbDa770Rb50tzhkqq6Cs5NYH54JlS3ED31QIAgJvl5+drz549io2NrT3zwdWAJUuWqE+fPjp27FipFYXPdOLECbVv316zZ892WuSkJrz33nv68ssv9d1335XbpqLfvxrLTh5S67IhuRAAUAvU12x3vsaPH6958+Zpw4YNFbbbuHGjrrvuOu3atcsx8qOm3HbbberSpYuefvrpcttURzbk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8WJNnDhRP//8s66//noVF5f//9WCggJZLBanF1ATYps20Jd/76VebZoot7BY985co2lLd9tXPiYbAgC8DMu4AmcJ8DXp7du7qk1YsN764U99uGyPGu1L1t/zp8l4PKWkoTlaSpwoxQ30XLEAANRSH374oTp16qQePXo4bb/99tsdv+7UqZM6d+6sNm3aaMmSJbr22mvLPNaECRP03HPPubVeoDwNg/w04+4eGvvVFn26er9eWrhN/jsW6K6sf8lANgQAeBGeJATKYDAY9GhCO03+Wzfd6LtGf894zjkESpIlVZozVNo63zNFAgDgRk2bNpXJZFJ6errT9vT0dEVGRla4b25urmbNmqV77rmn0vO0bt1aTZs21c6dO8ttM2bMGGVnZzteBw4ccO0igGriazLq5Zs7auyNcbretFp37n9GIhsCALwMnYRABW7sGKE3zZ9KBslQ6tNT03kmj2Z4CQDA6/j5+al79+5avHixY5vVatXixYvVs2fPCvf97LPPVFBQoDvvvLPS8xw8eFBHjx5VVFRUuW38/f1lNpudXkBNMxgMurtXC71lnmV/X6oF2RAAULfRSQhUZN9y+eWmVvAfik2yHJL2La/BogAAqBlJSUmaNm2aZsyYoW3btumBBx5Qbm6uRowYIUkaOnSoxowZU2q/Dz/8UIMGDSq1GElOTo7+8Y9/aOXKldq7d68WL16sm266SW3btlW/fv1q5JqA87JvuQJOpMlYuofwFLIhAKDuYk5CoCI56ZW3qUo7AADqkNtuu02HDx/W2LFjlZaWpq5duyo5OdmxmMn+/ftlNDrfStu+fbuWLVum7777rtTxTCaTNm7cqBkzZigrK0vR0dHq27evXnjhBfn7+9fINQHnhWwIAPBidBICFQmOqLxNVdoBAFDHPPTQQ3rooYfK/GzJkiWltrVv396+8msZAgMDtWjRouosD6hZZEMAgBdjuDFQkZa97CvVlTHrjCRZbVKammhVcfuarQsAAAA1z4VsmG5oqj8DOtVsXQAAVAM6CYGKGE1S4sRTb5zDoE0GGQzSuMK7dMdHazRj+d5yn5wAAACAF3AhG44tuFOD3luphZtSa74+AADOA52EQGXiBkqDZ0pm51UXDeZoFf5luvw7DdJJq03j5m9R0pzflVd40kOFAgAAwO0qyIY5N/1Hx2OvV15hsf7+v3WasHCbioqt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", "text/plain": [ "
" ] @@ -138,7 +127,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 12, "metadata": {}, "outputs": [ { @@ -147,7 +136,7 @@ "'final time'" ] }, - "execution_count": 4, + "execution_count": 12, "metadata": {}, "output_type": "execute_result" } @@ -167,12 +156,12 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 13, "metadata": {}, "outputs": [ { "data": { - "image/png": "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Ga8sGzZsIDQ0lI8//pitW7fyn//8hwkTJjBt2jTAfoF5+vTpnDnz3969e/nkk0/Oetzhw4fz66+/snXr1gs/mbOo9iZheHg4qaklg1RqaioBAQGOJZr/bsKECWRmZjpehw4dqr4C/cLOPaYy40RERKTaWa1WpkyZQmxsLN7e3nTq1IlFixZhs9mIj4+nX79+jvCVnp5O48aNmThxInD67r2lS5fSsWNHvLy8uPTSS0vMn2yxWFi0aBGDBg06ax3t27fn888/Z9CgQTRr1ow+ffrw/PPP8/XXX1NUVARAgwYNuOaaa/j111/59ddfueaaa2jQoAH79++nd+/ejjEGg6HE1fFBgwY5rSl3pvvvv59vvvmGn3/+mcaNG591bHm5Lzw83PH9qW3ljSlLjWVD5UIREZEal5CQwKhRo2jXrh2dOnVi9uzZHDx4kA0bNgAQGxvLyJEjmTFjBocPH6Z///4cOXIET09PADIyMrjrrrsICQkhICCAPn368OeffwL2OZXDw8NLTB+zevVqPDw8WL58OXD67r13332X6OhofHx8GDp0aIknOjZs2MDevXsZOHDgWc/lzjvv5I033uDKK6+kadOm3H777YwePZovvvgCAC8vL44cOUL//v05fPgwM2bMYNSoUcTGxjJ79myefvpp/vzzTwwGAwaDwfEUSoMGDbjsssuqPRu6VevRgR49erBs2bIS23744Qd69OhR7j6enp6Ov+xq16QnBESCOZmy558x2L9vUv7tpCIiIq7AZrNxstA5c615u5sqtSLblClT+Pjjj5kxYwYtWrRg5cqV3H777YSEhDBnzhw6dOjAm2++yUMPPcQ999xDVFSUo0l4yv/93//xxhtvEB4ezhNPPMGgQYPYtWsX7u7u/PXXX2RmZtKtW7dKn8upCaHd3Owxa9SoUfTp04fu3bsDsG7dOi666CIsFguff/45N910Ezt37ix1AbV79+48//zz5Ofn11wuOoPNZuOBBx7gyy+/ZMWKFcTGxp5znx49erB8+XIefvhhx7Yzc19sbCzh4eEsX77c8SiN2Wxm7dq13HvvveUet8ay4TlyoQ0DBuVCERGpI+pStjvT3+dB7tmzJz///DPx8fH89ttvfP311wwYMMAx/uabb8bb25tvv/2WwMBA3n33Xa6++mp27dpFSEgIs2bNYsiQIfTt25dWrVpxxx13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", + "image/png": "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dZA/+ugj7du3zzHeZrNpxowZOnnyZI3VaLPZNH36dKeMV5aNGzcqPDxcH374obZv365//OMfmjBhgqZPny7J/gXzjBkzdObKf/v27dNHH31U4eveeeedWr16tbZv337hJ1OBam8SRkZGKjU11elYamqqgoODHVs0n23ChAnKzMx03A4fPlx9BXq5OE/fWihZLdVXBwAAcJnVatWUKVMUGxsrf39/de7cWYsXL5bNZlOfPn3Uv39/R/hKT09Xs2bNNHHiREklV+8tW7ZMnTp1kp+fn6644gqn9ZMtFosWL16sgQMHVlhHhw4d9Omnn2rgwIFq3bq1rrvuOr300kv68ssvVVRUJElq2LCh+vbtq9WrV2v16tXq27evGjZsqAMHDqh3796OMQaDwenb8YEDB7qtKXemhx9+WF999ZV++OEHNWvWrMKx5eW+yMhIx+Onj5U3piw1lg0btXJ9bE4a2RAAgCqQmJioUaNG6ZJLLlHnzp01Z84cHTp0SBs3bpQkxcbGauTIkZo5c6aOHDmihIQEHT16VL6+vpKkjIwM3XfffWrSpImCg4N13XXX6ffff5dkX1M5MjLSafmYNWvWyMfHRytXrpRUcvXee++9p5iYGAUEBGjIkCFOMzo2btyoffv2acCAARWeyz333KO33npL11xzjVq1aqW77rpLo0eP1meffSZJ8vPz09GjR5WQkKAjR45o5syZGjVqlGJjYzVnzhw999xz+v3332UwGGQwGByzUBo2bKgrr7yy2rOhV7W+uqQePXpo+XLntV2+/fZb9ejRo9zn+Pr6On6zq90Vj0irXnBt7E+vS9dW/+WdAAC4g81m06lC9zQ9/L1NldqRbcqUKfrwww81c+ZMtW3bVj/99JPuuusuNWnSRHPnzlXHjh319ttv67HHHtMDDzygpk2bOpqEp/3973/XW2+9pcjISD399NMaOHCg/vjjD3l7e2vLli3KzMzUZZddVulzOb0gtJeXPWaNGjVK1113nbp37y5JWr9+vZo3by6LxaJPP/1Ut956q3bv3l3qC9Tu3bvrpZdeUn5+fs3lojPYbDY98sgj+vzzz7Vq1SrFxsae8zk9evTQypUrNXbsWMexM3NfbGysIiMjtXLlSsdUGrPZrHXr1unBBx8s93VrLBv2fVH69d+ujbVZyYYAgFqtLmW7M529DnLPnj31ww8/qE+fPvrf//6nL7/8UjfccINj/O233y5/f399/fXXCgkJ0Xvvvafrr79ef/zxh5o0aaLZs2dr8ODB6tevn9q1a6e7775bDz/8sK6//nrHa+zdu1eLFi3Sl19+KbPZrHvvvVd//etfHVf4/fzzz7rooosUFBR0Xudz+lwCAgL08ssva/ny5Ro0aJCKior0/fffy9vbW127dtW2bduUmJio7777TpJ97ebTunfvrp9//rnS718ZlW4SZmdna+/evY77+/fv1+bNm9WoUSM1b95cEyZM0NGjRzVv3jxJ0gMPPKDp06frySef1D333KPvv/9eixYt0rJly6ruLC7EVY+63iRcN1O6+gnJaKremgAAcINThRbFTVzhlvfe8Xx/Bfi4Fkvy8/P18ssv67vvvnM0n1q1aqXVq1frvffe0/z58/Xee+9pxIgRSklJ0fLly/Xbb785mnanTZo0SX379pUkzZ07V82aNdPnn3+uIUOG6ODBgzKZTAoPD6/UeRw/flwvvPCC7r//fsexDz/8UNOnT3d88zxkyBA9/PDDuuuuuxyBMTw8XKGhoU6vFR0drYKCAqWkpKhFixaVqqMqPPTQQ5o/f76++OILBQUFOdYMDAkJcTQzR4wYoaZNm2rKlCmSpMcee0zXXHON3njjDQ0YMEALFizQhg0b9P7770uSDAaDxo4dqxdffFFt27ZVbGysnn32WUVHR2vw4ME1fo6l+PhLbROkPS5OJSYbAgBqsbqS7c5U1jrI69at09///nf17NlT3t7emjZtmtauXaunn35aGzZs0Pr165WWlub4QvH111/XkiVLtHjxYt1///268cYbNWbMGN1555267LLL1KBBA0d2OS0vL0/z5s1T06ZNJdmXnRkwYIDeeOMNRUZG6uDBg4qOjq70+axZs0YLFy509MDy8vL08ssva926dbr22mt12WWXqU+fPnrttdfUvXt3BQYGysvLq8wZFtHR0Tp40MUl885Tpacbb9iwQV27dlXXrl0lSePGjVPXrl0d384nJyfr0KFDjvGxsbFatmyZvv32W3Xu3FlvvPGG/v3vf6t///5VdAoXyMtHuuQvro09lS4drNldbAAAgLO9e/cqNzdXffv2VWBgoOM2b948x3o1t99+u2655Ra98sorev3119W2bdtSr3PmrIZGjRqpXbt22rlzpyTp1KlT8vX1dfoG/OWX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", "text/plain": [ "
" ] @@ -242,7 +231,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 14, "metadata": {}, "outputs": [ { @@ -251,7 +240,7 @@ "'event: v=0.2'" ] }, - "execution_count": 6, + "execution_count": 14, "metadata": {}, "output_type": "execute_result" } @@ -271,7 +260,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 15, "metadata": {}, "outputs": [ { @@ -301,7 +290,7 @@ "\n", "dae_solver = pybamm.IDAKLUSolver()\n", "dae_solver.set_up(model)\n", - "dae_solver._set_consistent_initialization(model, 0, {})\n", + "dae_solver._set_consistent_initialization(model, 0, [{}])\n", "print(f\"y0_fixed={model.y0}\")" ] }, @@ -316,7 +305,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 16, "metadata": {}, "outputs": [ { @@ -325,7 +314,9 @@ "text": [ "[1] Joel A. E. Andersson, Joris Gillis, Greg Horn, James B. Rawlings, and Moritz Diehl. CasADi – A software framework for nonlinear optimization and optimal control. Mathematical Programming Computation, 11(1):1–36, 2019. doi:10.1007/s12532-018-0139-4.\n", "[2] Charles R. Harris, K. Jarrod Millman, Stéfan J. van der Walt, Ralf Gommers, Pauli Virtanen, David Cournapeau, Eric Wieser, Julian Taylor, Sebastian Berg, Nathaniel J. Smith, and others. Array programming with NumPy. Nature, 585(7825):357–362, 2020. doi:10.1038/s41586-020-2649-2.\n", - "[3] Valentin Sulzer, Scott G. Marquis, Robert Timms, Martin Robinson, and S. Jon Chapman. Python Battery Mathematical Modelling (PyBaMM). Journal of Open Research Software, 9(1):14, 2021. doi:10.5334/jors.309.\n", + "[3] Alan C. Hindmarsh. The PVODE and IDA algorithms. Technical Report, Lawrence Livermore National Lab., CA (US), 2000. doi:10.2172/802599.\n", + "[4] Alan C. Hindmarsh, Peter N. Brown, Keith E. Grant, Steven L. Lee, Radu Serban, Dan E. Shumaker, and Carol S. Woodward. SUNDIALS: Suite of nonlinear and differential/algebraic equation solvers. ACM Transactions on Mathematical Software (TOMS), 31(3):363–396, 2005. doi:10.1145/1089014.1089020.\n", + "[5] Valentin Sulzer, Scott G. Marquis, Robert Timms, Martin Robinson, and S. Jon Chapman. Python Battery Mathematical Modelling (PyBaMM). Journal of Open Research Software, 9(1):14, 2021. doi:10.5334/jors.309.\n", "\n" ] } @@ -337,7 +328,7 @@ ], "metadata": { "kernelspec": { - "display_name": "env", + "display_name": "venv", "language": "python", "name": "python3" }, @@ -351,12 +342,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.7" - }, - "vscode": { - "interpreter": { - "hash": "19e5ebaa8d5a3277b4deed2928f02ad0cad6c3ab0b2beced644d557f155bce64" - } + "version": "3.11.10" } }, "nbformat": 4, diff --git a/src/pybamm/solvers/idaklu_solver.py b/src/pybamm/solvers/idaklu_solver.py index e125be2b45..9cf045ecea 100644 --- a/src/pybamm/solvers/idaklu_solver.py +++ b/src/pybamm/solvers/idaklu_solver.py @@ -777,7 +777,7 @@ def _set_consistent_initialization(self, model, time, inputs_list): The model for which to calculate initial conditions. time : numeric type The time at which to calculate the initial conditions. - inputs_dict : dict + inputs_list : list of dict Any input parameters to pass to the model when solving. """ From 6818544b51157656c1d5b1d85b977f31533c7855 Mon Sep 17 00:00:00 2001 From: Pip Liggins Date: Mon, 27 Oct 2025 11:34:13 +0000 Subject: [PATCH 16/42] Remove unused y0S from _integrate_single --- src/pybamm/solvers/algebraic_solver.py | 10 +++++++--- src/pybamm/solvers/base_solver.py | 18 ++++-------------- src/pybamm/solvers/casadi_algebraic_solver.py | 10 +++++++--- src/pybamm/solvers/casadi_solver.py | 10 +++++++--- src/pybamm/solvers/dummy_solver.py | 6 +----- src/pybamm/solvers/scipy_solver.py | 10 ++++------ tests/unit/test_solvers/test_base_solver.py | 2 +- 7 files changed, 31 insertions(+), 35 deletions(-) diff --git a/src/pybamm/solvers/algebraic_solver.py b/src/pybamm/solvers/algebraic_solver.py index ee029d3fdb..c34d9ac4cb 100644 --- a/src/pybamm/solvers/algebraic_solver.py +++ b/src/pybamm/solvers/algebraic_solver.py @@ -78,7 +78,7 @@ def tol(self): def tol(self, value): self._tol = value - def _integrate_single(self, model, t_eval, inputs_dict, y0, y0S): + def _integrate_single(self, model, t_eval, inputs_dict, y0): """ Calculate the solution of the algebraic equations through root-finding @@ -92,8 +92,12 @@ def _integrate_single(self, model, t_eval, inputs_dict, y0, y0S): Any input parameters to pass to the model when solving y0 : array-like The initial conditions for the model - y0S : array-like - The initial sensitivities for the model + + Returns + ------- + :class:`pybamm.Solution` + A Solution object containing the times and values of the solution, + as well as various diagnostic messages. """ inputs_dict = inputs_dict or {} if model.convert_to_format == "casadi": diff --git a/src/pybamm/solvers/base_solver.py b/src/pybamm/solvers/base_solver.py index ecd7042081..b087ca3f38 100644 --- a/src/pybamm/solvers/base_solver.py +++ b/src/pybamm/solvers/base_solver.py @@ -773,12 +773,6 @@ def _integrate( inputs_list = inputs_list or [{}] - y0S_list = ( - model.y0S_list - if model.y0S_list is not None - else ([None] * len(inputs_list)) - ) - ninputs = len(inputs_list) if ninputs == 1: new_solution = self._integrate_single( @@ -786,7 +780,6 @@ def _integrate( t_eval, inputs_list[0], model.y0_list[0], - y0S_list[0], ) new_solutions = [new_solution] else: @@ -801,7 +794,6 @@ def _integrate( t_eval_list, inputs_list, y0_list, - y0S_list, strict=True, ), ) @@ -810,7 +802,7 @@ def _integrate( return new_solutions - def _integrate_single(self, model, t_eval, inputs_dict, y0, y0S): + def _integrate_single(self, model, t_eval, inputs_dict, y0): """ Solve a single model instance with initial conditions y0. @@ -824,8 +816,6 @@ def _integrate_single(self, model, t_eval, inputs_dict, y0, y0S): Any input parameters to pass to the model when solving y0 : array-like The initial conditions for the model - y0S : array-like - The initial sensitivities for the model """ raise NotImplementedError("BaseSolver does not implement _integrate_single.") @@ -968,9 +958,9 @@ def solve( # If the new initial conditions are different # and cannot be evaluated directly, set up again self.set_up(model, model_inputs_list, t_eval, ics_only=True) - self._model_set_up[model]["initial conditions"] = ( - model.concatenated_initial_conditions - ) + self._model_set_up[model][ + "initial conditions" + ] = model.concatenated_initial_conditions else: # Set the standard initial conditions self._set_initial_conditions(model, t_eval[0], model_inputs_list) diff --git a/src/pybamm/solvers/casadi_algebraic_solver.py b/src/pybamm/solvers/casadi_algebraic_solver.py index da3df29518..004535a13d 100644 --- a/src/pybamm/solvers/casadi_algebraic_solver.py +++ b/src/pybamm/solvers/casadi_algebraic_solver.py @@ -151,7 +151,7 @@ def set_up_root_solver(self, model, inputs_dict, t_eval): pybamm.logger.info(f"Finish building {self.name}") - def _integrate_single(self, model, t_eval, inputs_dict, y0, y0S): + def _integrate_single(self, model, t_eval, inputs_dict, y0): """ Calculate the solution of the algebraic equations through root-finding @@ -165,8 +165,12 @@ def _integrate_single(self, model, t_eval, inputs_dict, y0, y0S): Any input parameters to pass to the model when solving y0 : array-like The initial conditions for the model - y0S : array-like - The initial sensitivities for the model + + Returns + ------- + :class:`pybamm.Solution` + A Solution object containing the times and values of the solution, + as well as various diagnostic messages. """ # Record whether there are any symbolic inputs inputs_dict = inputs_dict or {} diff --git a/src/pybamm/solvers/casadi_solver.py b/src/pybamm/solvers/casadi_solver.py index 1862abaf16..71ae22989d 100644 --- a/src/pybamm/solvers/casadi_solver.py +++ b/src/pybamm/solvers/casadi_solver.py @@ -140,7 +140,7 @@ def __init__( pybamm.citations.register("Andersson2019") - def _integrate_single(self, model, t_eval, inputs_dict, y0, y0S): + def _integrate_single(self, model, t_eval, inputs_dict, y0): """ Solve a single DAE model defined by residuals with initial conditions y0. @@ -154,8 +154,12 @@ def _integrate_single(self, model, t_eval, inputs_dict, y0, y0S): Any input parameters to pass to the model when solving y0 : array-like The initial conditions for the model - y0S : array-like - The initial sensitivities for the model + + Returns + ------- + :class:`pybamm.Solution` + A Solution object containing the times and values of the solution, + as well as various diagnostic messages. """ # casadi solver does not support sensitivity analysis diff --git a/src/pybamm/solvers/dummy_solver.py b/src/pybamm/solvers/dummy_solver.py index ed9f0f3d3f..edd81bd67d 100644 --- a/src/pybamm/solvers/dummy_solver.py +++ b/src/pybamm/solvers/dummy_solver.py @@ -13,7 +13,7 @@ def __init__(self): super().__init__() self.name = "Dummy solver" - def _integrate_single(self, model, t_eval, inputs_dict, y0, y0S): + def _integrate_single(self, model, t_eval, inputs_dict, y0=None): """ Solve an empty model. @@ -23,10 +23,6 @@ def _integrate_single(self, model, t_eval, inputs_dict, y0, y0S): The model whose solution to calculate. t_eval : :class:`numpy.array`, size (k,) The times at which to compute the solution - y0 : array-like - The initial conditions for the model - y0S : array-like - The initial sensitivities for the model inputs_dict : dict, optional Any input parameters to pass to the model when solving diff --git a/src/pybamm/solvers/scipy_solver.py b/src/pybamm/solvers/scipy_solver.py index b92134507b..bd6e8dabdd 100644 --- a/src/pybamm/solvers/scipy_solver.py +++ b/src/pybamm/solvers/scipy_solver.py @@ -52,7 +52,7 @@ def __init__( self.name = f"Scipy solver ({method})" pybamm.citations.register("Virtanen2020") - def _integrate_single(self, model, t_eval, inputs_dict, y0, y0S): + def _integrate_single(self, model, t_eval, inputs_dict, y0): """ Solve a model defined by dydt with initial conditions y0. @@ -66,14 +66,12 @@ def _integrate_single(self, model, t_eval, inputs_dict, y0, y0S): Any input parameters to pass to the model when solving y0 : array-like The initial conditions for the model - y0S : array-like - The initial sensitivities for the model Returns ------- - object - An object containing the times and values of the solution, as well as - various diagnostic messages. + :class:`pybamm.Solution` + A Solution object containing the times and values of the solution, + as well as various diagnostic messages. """ # scipy solver does not support sensitivity analysis diff --git a/tests/unit/test_solvers/test_base_solver.py b/tests/unit/test_solvers/test_base_solver.py index 27939b9254..baa871d628 100644 --- a/tests/unit/test_solvers/test_base_solver.py +++ b/tests/unit/test_solvers/test_base_solver.py @@ -489,4 +489,4 @@ def test_integrate_single_error(self): NotImplementedError, match="BaseSolver does not implement _integrate_single.", ): - solver._integrate_single(model, np.array([0, 1]), {}, np.array([1]), None) + solver._integrate_single(model, np.array([0, 1]), {}, np.array([1])) From 8ab05ade6044f68a10dcc940b340ef3f92501cdc Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 27 Oct 2025 11:35:04 +0000 Subject: [PATCH 17/42] style: pre-commit fixes --- src/pybamm/solvers/base_solver.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pybamm/solvers/base_solver.py b/src/pybamm/solvers/base_solver.py index b087ca3f38..ece9310865 100644 --- a/src/pybamm/solvers/base_solver.py +++ b/src/pybamm/solvers/base_solver.py @@ -958,9 +958,9 @@ def solve( # If the new initial conditions are different # and cannot be evaluated directly, set up again self.set_up(model, model_inputs_list, t_eval, ics_only=True) - self._model_set_up[model][ - "initial conditions" - ] = model.concatenated_initial_conditions + self._model_set_up[model]["initial conditions"] = ( + model.concatenated_initial_conditions + ) else: # Set the standard initial conditions self._set_initial_conditions(model, t_eval[0], model_inputs_list) From 381d4885fbbd47e470f8acac0d046463dcb6e39c Mon Sep 17 00:00:00 2001 From: Pip Liggins Date: Tue, 28 Oct 2025 10:39:19 +0000 Subject: [PATCH 18/42] Add ids to parameterized idaklu-jax tests --- tests/unit/test_solvers/test_idaklu_jax.py | 188 +++++++++++++++------ 1 file changed, 141 insertions(+), 47 deletions(-) diff --git a/tests/unit/test_solvers/test_idaklu_jax.py b/tests/unit/test_solvers/test_idaklu_jax.py index c73b7bce80..8cd817f88a 100644 --- a/tests/unit/test_solvers/test_idaklu_jax.py +++ b/tests/unit/test_solvers/test_idaklu_jax.py @@ -200,7 +200,9 @@ def test_no_inputs(self): # Scalar evaluation @pytest.mark.parametrize( - "output_variables,idaklu_jax_solver,f,wrapper", make_test_cases() + "output_variables,idaklu_jax_solver,f,wrapper", + make_test_cases(), + ids=["single-no-jit", "single-jit", "multiple-no-jit", "multiple-jit"], ) def test_f_scalar(self, output_variables, idaklu_jax_solver, f, wrapper): out = wrapper(f)(t_eval[k], inputs) @@ -209,7 +211,9 @@ def test_f_scalar(self, output_variables, idaklu_jax_solver, f, wrapper): ) @pytest.mark.parametrize( - "output_variables,idaklu_jax_solver,f,wrapper", make_test_cases() + "output_variables,idaklu_jax_solver,f,wrapper", + make_test_cases(), + ids=["single-no-jit", "single-jit", "multiple-no-jit", "multiple-jit"], ) def test_f_vector(self, output_variables, idaklu_jax_solver, f, wrapper): out = wrapper(f)(t_eval, inputs) @@ -218,7 +222,9 @@ def test_f_vector(self, output_variables, idaklu_jax_solver, f, wrapper): ) @pytest.mark.parametrize( - "output_variables,idaklu_jax_solver,f,wrapper", make_test_cases() + "output_variables,idaklu_jax_solver,f,wrapper", + make_test_cases(), + ids=["single-no-jit", "single-jit", "multiple-no-jit", "multiple-jit"], ) def test_f_vmap(self, output_variables, idaklu_jax_solver, f, wrapper): out = wrapper(jax.vmap(f, in_axes=in_axes))(t_eval, inputs) @@ -227,7 +233,9 @@ def test_f_vmap(self, output_variables, idaklu_jax_solver, f, wrapper): ) @pytest.mark.parametrize( - "output_variables,idaklu_jax_solver,f,wrapper", make_test_cases() + "output_variables,idaklu_jax_solver,f,wrapper", + make_test_cases(), + ids=["single-no-jit", "single-jit", "multiple-no-jit", "multiple-jit"], ) def test_f_batch_over_inputs(self, output_variables, idaklu_jax_solver, f, wrapper): inputs_mock = np.array([1.0, 2.0, 3.0]) @@ -237,7 +245,9 @@ def test_f_batch_over_inputs(self, output_variables, idaklu_jax_solver, f, wrapp # Get all vars (should mirror test_f_* [above]) @pytest.mark.parametrize( - "output_variables,idaklu_jax_solver,f,wrapper", make_test_cases() + "output_variables,idaklu_jax_solver,f,wrapper", + make_test_cases(), + ids=["single-no-jit", "single-jit", "multiple-no-jit", "multiple-jit"], ) def test_getvars_call_signature( self, output_variables, idaklu_jax_solver, f, wrapper @@ -252,7 +262,9 @@ def test_getvars_call_signature( idaklu_jax_solver.get_vars(1, 2, 3) # too many arguments @pytest.mark.parametrize( - "output_variables,idaklu_jax_solver,f,wrapper", make_test_cases() + "output_variables,idaklu_jax_solver,f,wrapper", + make_test_cases(), + ids=["single-no-jit", "single-jit", "multiple-no-jit", "multiple-jit"], ) def test_getvars_scalar(self, output_variables, idaklu_jax_solver, f, wrapper): out = wrapper(idaklu_jax_solver.get_vars(output_variables))(t_eval[k], inputs) @@ -261,7 +273,9 @@ def test_getvars_scalar(self, output_variables, idaklu_jax_solver, f, wrapper): ) @pytest.mark.parametrize( - "output_variables,idaklu_jax_solver,f,wrapper", make_test_cases() + "output_variables,idaklu_jax_solver,f,wrapper", + make_test_cases(), + ids=["single-no-jit", "single-jit", "multiple-no-jit", "multiple-jit"], ) def test_getvars_vector(self, output_variables, idaklu_jax_solver, f, wrapper): out = wrapper(idaklu_jax_solver.get_vars(output_variables))(t_eval, inputs) @@ -270,7 +284,9 @@ def test_getvars_vector(self, output_variables, idaklu_jax_solver, f, wrapper): ) @pytest.mark.parametrize( - "output_variables,idaklu_jax_solver,f,wrapper", make_test_cases() + "output_variables,idaklu_jax_solver,f,wrapper", + make_test_cases(), + ids=["single-no-jit", "single-jit", "multiple-no-jit", "multiple-jit"], ) def test_getvars_vector_array( self, output_variables, idaklu_jax_solver, f, wrapper @@ -282,7 +298,9 @@ def test_getvars_vector_array( np.testing.assert_allclose(out, array) @pytest.mark.parametrize( - "output_variables,idaklu_jax_solver,f,wrapper", make_test_cases() + "output_variables,idaklu_jax_solver,f,wrapper", + make_test_cases(), + ids=["single-no-jit", "single-jit", "multiple-no-jit", "multiple-jit"], ) def test_getvars_vmap(self, output_variables, idaklu_jax_solver, f, wrapper): out = wrapper( @@ -298,7 +316,9 @@ def test_getvars_vmap(self, output_variables, idaklu_jax_solver, f, wrapper): # Isolate single output variable @pytest.mark.parametrize( - "output_variables,idaklu_jax_solver,f,wrapper", make_test_cases() + "output_variables,idaklu_jax_solver,f,wrapper", + make_test_cases(), + ids=["single-no-jit", "single-jit", "multiple-no-jit", "multiple-jit"], ) def test_getvar_call_signature( self, output_variables, idaklu_jax_solver, f, wrapper @@ -313,7 +333,9 @@ def test_getvar_call_signature( idaklu_jax_solver.get_var(1, 2, 3) # too many arguments @pytest.mark.parametrize( - "output_variables,idaklu_jax_solver,f,wrapper", make_test_cases() + "output_variables,idaklu_jax_solver,f,wrapper", + make_test_cases(), + ids=["single-no-jit", "single-jit", "multiple-no-jit", "multiple-jit"], ) def test_getvar_scalar_float_jaxpr( self, output_variables, idaklu_jax_solver, f, wrapper @@ -324,7 +346,9 @@ def test_getvar_scalar_float_jaxpr( np.testing.assert_allclose(out, sim[outvar](float(t_eval[k]))) @pytest.mark.parametrize( - "output_variables,idaklu_jax_solver,f,wrapper", make_test_cases() + "output_variables,idaklu_jax_solver,f,wrapper", + make_test_cases(), + ids=["single-no-jit", "single-jit", "multiple-no-jit", "multiple-jit"], ) def test_getvar_scalar_float_f( self, output_variables, idaklu_jax_solver, f, wrapper @@ -337,7 +361,9 @@ def test_getvar_scalar_float_f( np.testing.assert_allclose(out, sim[outvar](float(t_eval[k]))) @pytest.mark.parametrize( - "output_variables,idaklu_jax_solver,f,wrapper", make_test_cases() + "output_variables,idaklu_jax_solver,f,wrapper", + make_test_cases(), + ids=["single-no-jit", "single-jit", "multiple-no-jit", "multiple-jit"], ) def test_getvar_scalar_jaxpr(self, output_variables, idaklu_jax_solver, f, wrapper): # Per variable checks using the default JAX expression (self.jaxpr) @@ -346,7 +372,9 @@ def test_getvar_scalar_jaxpr(self, output_variables, idaklu_jax_solver, f, wrapp np.testing.assert_allclose(out, sim[outvar](t_eval[k])) @pytest.mark.parametrize( - "output_variables,idaklu_jax_solver,f,wrapper", make_test_cases() + "output_variables,idaklu_jax_solver,f,wrapper", + make_test_cases(), + ids=["single-no-jit", "single-jit", "multiple-no-jit", "multiple-jit"], ) def test_getvar_scalar_f(self, output_variables, idaklu_jax_solver, f, wrapper): # Per variable checks using a provided JAX expression (f) @@ -355,7 +383,9 @@ def test_getvar_scalar_f(self, output_variables, idaklu_jax_solver, f, wrapper): np.testing.assert_allclose(out, sim[outvar](t_eval[k])) @pytest.mark.parametrize( - "output_variables,idaklu_jax_solver,f,wrapper", make_test_cases() + "output_variables,idaklu_jax_solver,f,wrapper", + make_test_cases(), + ids=["single-no-jit", "single-jit", "multiple-no-jit", "multiple-jit"], ) def test_getvar_vector_jaxpr(self, output_variables, idaklu_jax_solver, f, wrapper): # Per variable checks using the default JAX expression (self.jaxpr) @@ -364,7 +394,9 @@ def test_getvar_vector_jaxpr(self, output_variables, idaklu_jax_solver, f, wrapp np.testing.assert_allclose(out, sim[outvar](t_eval)) @pytest.mark.parametrize( - "output_variables,idaklu_jax_solver,f,wrapper", make_test_cases() + "output_variables,idaklu_jax_solver,f,wrapper", + make_test_cases(), + ids=["single-no-jit", "single-jit", "multiple-no-jit", "multiple-jit"], ) def test_getvar_vector_f(self, output_variables, idaklu_jax_solver, f, wrapper): # Per variable checks using a provided JAX expression (f) @@ -373,7 +405,9 @@ def test_getvar_vector_f(self, output_variables, idaklu_jax_solver, f, wrapper): np.testing.assert_allclose(out, sim[outvar](t_eval)) @pytest.mark.parametrize( - "output_variables,idaklu_jax_solver,f,wrapper", make_test_cases() + "output_variables,idaklu_jax_solver,f,wrapper", + make_test_cases(), + ids=["single-no-jit", "single-jit", "multiple-no-jit", "multiple-jit"], ) def test_getvar_vector_array(self, output_variables, idaklu_jax_solver, f, wrapper): # Per variable checks using a provided np.ndarray @@ -385,7 +419,9 @@ def test_getvar_vector_array(self, output_variables, idaklu_jax_solver, f, wrapp np.testing.assert_allclose(out, sim[outvar](t_eval)) @pytest.mark.parametrize( - "output_variables,idaklu_jax_solver,f,wrapper", make_test_cases() + "output_variables,idaklu_jax_solver,f,wrapper", + make_test_cases(), + ids=["single-no-jit", "single-jit", "multiple-no-jit", "multiple-jit"], ) def test_getvar_vmap(self, output_variables, idaklu_jax_solver, f, wrapper): for outvar in output_variables: @@ -400,7 +436,9 @@ def test_getvar_vmap(self, output_variables, idaklu_jax_solver, f, wrapper): # Differentiation rules (jacfwd) @pytest.mark.parametrize( - "output_variables,idaklu_jax_solver,f,wrapper", make_test_cases() + "output_variables,idaklu_jax_solver,f,wrapper", + make_test_cases(), + ids=["single-no-jit", "single-jit", "multiple-no-jit", "multiple-jit"], ) def test_jacfwd_scalar(self, output_variables, idaklu_jax_solver, f, wrapper): out = wrapper(jax.jacfwd(f, argnums=1))(t_eval[k], inputs) @@ -416,7 +454,9 @@ def test_jacfwd_scalar(self, output_variables, idaklu_jax_solver, f, wrapper): np.testing.assert_allclose(flat_out, check.flatten()) @pytest.mark.parametrize( - "output_variables,idaklu_jax_solver,f,wrapper", make_test_cases() + "output_variables,idaklu_jax_solver,f,wrapper", + make_test_cases(), + ids=["single-no-jit", "single-jit", "multiple-no-jit", "multiple-jit"], ) def test_jacfwd_vector(self, output_variables, idaklu_jax_solver, f, wrapper): out = wrapper(jax.jacfwd(f, argnums=1))(t_eval, inputs) @@ -435,7 +475,9 @@ def test_jacfwd_vector(self, output_variables, idaklu_jax_solver, f, wrapper): ) @pytest.mark.parametrize( - "output_variables,idaklu_jax_solver,f,wrapper", make_test_cases() + "output_variables,idaklu_jax_solver,f,wrapper", + make_test_cases(), + ids=["single-no-jit", "single-jit", "multiple-no-jit", "multiple-jit"], ) def test_jacfwd_vmap(self, output_variables, idaklu_jax_solver, f, wrapper): out = wrapper( @@ -456,7 +498,9 @@ def test_jacfwd_vmap(self, output_variables, idaklu_jax_solver, f, wrapper): np.testing.assert_allclose(flat_out, check.flatten()) @pytest.mark.parametrize( - "output_variables,idaklu_jax_solver,f,wrapper", make_test_cases() + "output_variables,idaklu_jax_solver,f,wrapper", + make_test_cases(), + ids=["single-no-jit", "single-jit", "multiple-no-jit", "multiple-jit"], ) def test_jacfwd_vmap_wrt_time( self, output_variables, idaklu_jax_solver, f, wrapper @@ -470,7 +514,9 @@ def test_jacfwd_vmap_wrt_time( )(t_eval, inputs) @pytest.mark.parametrize( - "output_variables,idaklu_jax_solver,f,wrapper", make_test_cases() + "output_variables,idaklu_jax_solver,f,wrapper", + make_test_cases(), + ids=["single-no-jit", "single-jit", "multiple-no-jit", "multiple-jit"], ) def test_jacfwd_batch_over_inputs( self, output_variables, idaklu_jax_solver, f, wrapper @@ -487,7 +533,9 @@ def test_jacfwd_batch_over_inputs( # Differentiation rules (jacrev) @pytest.mark.parametrize( - "output_variables,idaklu_jax_solver,f,wrapper", make_test_cases() + "output_variables,idaklu_jax_solver,f,wrapper", + make_test_cases(), + ids=["single-no-jit", "single-jit", "multiple-no-jit", "multiple-jit"], ) def test_jacrev_scalar(self, output_variables, idaklu_jax_solver, f, wrapper): out = wrapper(jax.jacrev(f, argnums=1))(t_eval[k], inputs) @@ -503,7 +551,9 @@ def test_jacrev_scalar(self, output_variables, idaklu_jax_solver, f, wrapper): np.testing.assert_allclose(flat_out, check.flatten()) @pytest.mark.parametrize( - "output_variables,idaklu_jax_solver,f,wrapper", make_test_cases() + "output_variables,idaklu_jax_solver,f,wrapper", + make_test_cases(), + ids=["single-no-jit", "single-jit", "multiple-no-jit", "multiple-jit"], ) def test_jacrev_vector(self, output_variables, idaklu_jax_solver, f, wrapper): out = wrapper(jax.jacrev(f, argnums=1))(t_eval, inputs) @@ -519,7 +569,9 @@ def test_jacrev_vector(self, output_variables, idaklu_jax_solver, f, wrapper): np.testing.assert_allclose(flat_out, check.flatten()) @pytest.mark.parametrize( - "output_variables,idaklu_jax_solver,f,wrapper", make_test_cases() + "output_variables,idaklu_jax_solver,f,wrapper", + make_test_cases(), + ids=["single-no-jit", "single-jit", "multiple-no-jit", "multiple-jit"], ) def test_jacrev_vmap(self, output_variables, idaklu_jax_solver, f, wrapper): out = wrapper( @@ -540,7 +592,9 @@ def test_jacrev_vmap(self, output_variables, idaklu_jax_solver, f, wrapper): np.testing.assert_allclose(flat_out, check.flatten()) @pytest.mark.parametrize( - "output_variables,idaklu_jax_solver,f,wrapper", make_test_cases() + "output_variables,idaklu_jax_solver,f,wrapper", + make_test_cases(), + ids=["single-no-jit", "single-jit", "multiple-no-jit", "multiple-jit"], ) def test_jacrev_batch_over_inputs( self, output_variables, idaklu_jax_solver, f, wrapper @@ -557,7 +611,9 @@ def test_jacrev_batch_over_inputs( # Forward differentiation rules with get_vars (multiple) and get_var (singular) @pytest.mark.parametrize( - "output_variables,idaklu_jax_solver,f,wrapper", make_test_cases() + "output_variables,idaklu_jax_solver,f,wrapper", + make_test_cases(), + ids=["single-no-jit", "single-jit", "multiple-no-jit", "multiple-jit"], ) def test_jacfwd_scalar_getvars( self, output_variables, idaklu_jax_solver, f, wrapper @@ -582,7 +638,9 @@ def test_jacfwd_scalar_getvars( np.testing.assert_allclose(flat_out, flat_check) @pytest.mark.parametrize( - "output_variables,idaklu_jax_solver,f,wrapper", make_test_cases() + "output_variables,idaklu_jax_solver,f,wrapper", + make_test_cases(), + ids=["single-no-jit", "single-jit", "multiple-no-jit", "multiple-jit"], ) def test_jacfwd_scalar_getvar( self, output_variables, idaklu_jax_solver, f, wrapper @@ -603,7 +661,9 @@ def test_jacfwd_scalar_getvar( np.testing.assert_allclose(flat_out, flat_check) @pytest.mark.parametrize( - "output_variables,idaklu_jax_solver,f,wrapper", make_test_cases() + "output_variables,idaklu_jax_solver,f,wrapper", + make_test_cases(), + ids=["single-no-jit", "single-jit", "multiple-no-jit", "multiple-jit"], ) def test_jacfwd_vector_getvars( self, output_variables, idaklu_jax_solver, f, wrapper @@ -629,7 +689,9 @@ def test_jacfwd_vector_getvars( np.testing.assert_allclose(flat_out, flat_check) @pytest.mark.parametrize( - "output_variables,idaklu_jax_solver,f,wrapper", make_test_cases() + "output_variables,idaklu_jax_solver,f,wrapper", + make_test_cases(), + ids=["single-no-jit", "single-jit", "multiple-no-jit", "multiple-jit"], ) def test_jacfwd_vector_getvar( self, output_variables, idaklu_jax_solver, f, wrapper @@ -650,7 +712,9 @@ def test_jacfwd_vector_getvar( np.testing.assert_allclose(flat_out, flat_check) @pytest.mark.parametrize( - "output_variables,idaklu_jax_solver,f,wrapper", make_test_cases() + "output_variables,idaklu_jax_solver,f,wrapper", + make_test_cases(), + ids=["single-no-jit", "single-jit", "multiple-no-jit", "multiple-jit"], ) def test_jacfwd_vmap_getvars(self, output_variables, idaklu_jax_solver, f, wrapper): out = wrapper( @@ -671,7 +735,9 @@ def test_jacfwd_vmap_getvars(self, output_variables, idaklu_jax_solver, f, wrapp np.testing.assert_allclose(flat_out, check.flatten()) @pytest.mark.parametrize( - "output_variables,idaklu_jax_solver,f,wrapper", make_test_cases() + "output_variables,idaklu_jax_solver,f,wrapper", + make_test_cases(), + ids=["single-no-jit", "single-jit", "multiple-no-jit", "multiple-jit"], ) def test_jacfwd_vmap_getvar(self, output_variables, idaklu_jax_solver, f, wrapper): for outvar in output_variables: @@ -692,7 +758,9 @@ def test_jacfwd_vmap_getvar(self, output_variables, idaklu_jax_solver, f, wrappe # Reverse differentiation rules with get_vars (multiple) and get_var (singular) @pytest.mark.parametrize( - "output_variables,idaklu_jax_solver,f,wrapper", make_test_cases() + "output_variables,idaklu_jax_solver,f,wrapper", + make_test_cases(), + ids=["single-no-jit", "single-jit", "multiple-no-jit", "multiple-jit"], ) def test_jacrev_scalar_getvars( self, output_variables, idaklu_jax_solver, f, wrapper @@ -717,7 +785,9 @@ def test_jacrev_scalar_getvars( np.testing.assert_allclose(flat_out, flat_check) @pytest.mark.parametrize( - "output_variables,idaklu_jax_solver,f,wrapper", make_test_cases() + "output_variables,idaklu_jax_solver,f,wrapper", + make_test_cases(), + ids=["single-no-jit", "single-jit", "multiple-no-jit", "multiple-jit"], ) def test_jacrev_scalar_getvar( self, output_variables, idaklu_jax_solver, f, wrapper @@ -740,7 +810,9 @@ def test_jacrev_scalar_getvar( ) @pytest.mark.parametrize( - "output_variables,idaklu_jax_solver,f,wrapper", make_test_cases() + "output_variables,idaklu_jax_solver,f,wrapper", + make_test_cases(), + ids=["single-no-jit", "single-jit", "multiple-no-jit", "multiple-jit"], ) def test_jacrev_vector_getvars( self, output_variables, idaklu_jax_solver, f, wrapper @@ -766,7 +838,9 @@ def test_jacrev_vector_getvars( np.testing.assert_allclose(flat_out, flat_check) @pytest.mark.parametrize( - "output_variables,idaklu_jax_solver,f,wrapper", make_test_cases() + "output_variables,idaklu_jax_solver,f,wrapper", + make_test_cases(), + ids=["single-no-jit", "single-jit", "multiple-no-jit", "multiple-jit"], ) def test_jacrev_vector_getvar( self, output_variables, idaklu_jax_solver, f, wrapper @@ -787,7 +861,9 @@ def test_jacrev_vector_getvar( np.testing.assert_allclose(flat_out, flat_check) @pytest.mark.parametrize( - "output_variables,idaklu_jax_solver,f,wrapper", make_test_cases() + "output_variables,idaklu_jax_solver,f,wrapper", + make_test_cases(), + ids=["single-no-jit", "single-jit", "multiple-no-jit", "multiple-jit"], ) def test_jacrev_vmap_getvars(self, output_variables, idaklu_jax_solver, f, wrapper): out = wrapper( @@ -808,7 +884,9 @@ def test_jacrev_vmap_getvars(self, output_variables, idaklu_jax_solver, f, wrapp np.testing.assert_allclose(flat_out, check.flatten()) @pytest.mark.parametrize( - "output_variables,idaklu_jax_solver,f,wrapper", make_test_cases() + "output_variables,idaklu_jax_solver,f,wrapper", + make_test_cases(), + ids=["single-no-jit", "single-jit", "multiple-no-jit", "multiple-jit"], ) def test_jacrev_vmap_getvar(self, output_variables, idaklu_jax_solver, f, wrapper): for outvar in output_variables: @@ -829,7 +907,9 @@ def test_jacrev_vmap_getvar(self, output_variables, idaklu_jax_solver, f, wrappe # Gradient rule (takes single variable) @pytest.mark.parametrize( - "output_variables,idaklu_jax_solver,f,wrapper", make_test_cases() + "output_variables,idaklu_jax_solver,f,wrapper", + make_test_cases(), + ids=["single-no-jit", "single-jit", "multiple-no-jit", "multiple-jit"], ) def test_grad_scalar_getvar(self, output_variables, idaklu_jax_solver, f, wrapper): for outvar in output_variables: @@ -838,14 +918,18 @@ def test_grad_scalar_getvar(self, output_variables, idaklu_jax_solver, f, wrappe idaklu_jax_solver.get_var(outvar), argnums=1, ), - )(t_eval[k], inputs) # output should be a dictionary of inputs + )( + t_eval[k], inputs + ) # output should be a dictionary of inputs flat_out, _ = tree_flatten(out) flat_out = np.array([f for f in flat_out]).flatten() check = np.array([sim[outvar].sensitivities[invar][k] for invar in inputs]) np.testing.assert_allclose(flat_out, check.flatten()) @pytest.mark.parametrize( - "output_variables,idaklu_jax_solver,f,wrapper", make_test_cases() + "output_variables,idaklu_jax_solver,f,wrapper", + make_test_cases(), + ids=["single-no-jit", "single-jit", "multiple-no-jit", "multiple-jit"], ) def test_grad_vmap_getvar(self, output_variables, idaklu_jax_solver, f, wrapper): for outvar in output_variables: @@ -866,7 +950,9 @@ def test_grad_vmap_getvar(self, output_variables, idaklu_jax_solver, f, wrapper) # Value and gradient (takes single variable) @pytest.mark.parametrize( - "output_variables,idaklu_jax_solver,f,wrapper", make_test_cases() + "output_variables,idaklu_jax_solver,f,wrapper", + make_test_cases(), + ids=["single-no-jit", "single-jit", "multiple-no-jit", "multiple-jit"], ) def test_value_and_grad_scalar( self, output_variables, idaklu_jax_solver, f, wrapper @@ -888,7 +974,9 @@ def test_value_and_grad_scalar( np.testing.assert_allclose(flat_t, check.flatten()) @pytest.mark.parametrize( - "output_variables,idaklu_jax_solver,f,wrapper", make_test_cases() + "output_variables,idaklu_jax_solver,f,wrapper", + make_test_cases(), + ids=["single-no-jit", "single-jit", "multiple-no-jit", "multiple-jit"], ) def test_value_and_grad_vmap(self, output_variables, idaklu_jax_solver, f, wrapper): for outvar in output_variables: @@ -913,7 +1001,9 @@ def test_value_and_grad_vmap(self, output_variables, idaklu_jax_solver, f, wrapp # Helper functions - These return values (not jaxexprs) so cannot be JITed @pytest.mark.parametrize( - "output_variables,idaklu_jax_solver,f,wrapper", make_test_cases() + "output_variables,idaklu_jax_solver,f,wrapper", + make_test_cases(), + ids=["single-no-jit", "single-jit", "multiple-no-jit", "multiple-jit"], ) def test_jax_vars(self, output_variables, idaklu_jax_solver, f, wrapper): if wrapper == jax.jit: @@ -930,7 +1020,9 @@ def test_jax_vars(self, output_variables, idaklu_jax_solver, f, wrapper): ) @pytest.mark.parametrize( - "output_variables,idaklu_jax_solver,f,wrapper", make_test_cases() + "output_variables,idaklu_jax_solver,f,wrapper", + make_test_cases(), + ids=["single-no-jit", "single-jit", "multiple-no-jit", "multiple-jit"], ) def test_jax_grad(self, output_variables, idaklu_jax_solver, f, wrapper): if wrapper == jax.jit: @@ -949,7 +1041,9 @@ def test_jax_grad(self, output_variables, idaklu_jax_solver, f, wrapper): # Wrap jaxified expression in another function and take the gradient @pytest.mark.parametrize( - "output_variables,idaklu_jax_solver,f,wrapper", make_test_cases() + "output_variables,idaklu_jax_solver,f,wrapper", + make_test_cases(), + ids=["single-no-jit", "single-jit", "multiple-no-jit", "multiple-jit"], ) def test_grad_wrapper_sse(self, output_variables, idaklu_jax_solver, f, wrapper): # Use surrogate for experimental data From f81ca5c17ee7e3988a8fd25fde1ffeb9259d0efe Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 28 Oct 2025 10:39:42 +0000 Subject: [PATCH 19/42] style: pre-commit fixes --- tests/unit/test_solvers/test_idaklu_jax.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/unit/test_solvers/test_idaklu_jax.py b/tests/unit/test_solvers/test_idaklu_jax.py index 8cd817f88a..ab468e9aed 100644 --- a/tests/unit/test_solvers/test_idaklu_jax.py +++ b/tests/unit/test_solvers/test_idaklu_jax.py @@ -918,9 +918,7 @@ def test_grad_scalar_getvar(self, output_variables, idaklu_jax_solver, f, wrappe idaklu_jax_solver.get_var(outvar), argnums=1, ), - )( - t_eval[k], inputs - ) # output should be a dictionary of inputs + )(t_eval[k], inputs) # output should be a dictionary of inputs flat_out, _ = tree_flatten(out) flat_out = np.array([f for f in flat_out]).flatten() check = np.array([sim[outvar].sensitivities[invar][k] for invar in inputs]) From d05d9d8bd23dbbbd3153a57ff5df28b68158a666 Mon Sep 17 00:00:00 2001 From: Pip Liggins Date: Tue, 4 Nov 2025 13:01:48 +0000 Subject: [PATCH 20/42] Use BaseModel for integration test with obvious initial cons/input params. Rename some unit tests --- tests/integration/test_solvers/test_idaklu.py | 41 +++++++------------ tests/unit/test_solvers/test_idaklu_solver.py | 6 +-- 2 files changed, 18 insertions(+), 29 deletions(-) diff --git a/tests/integration/test_solvers/test_idaklu.py b/tests/integration/test_solvers/test_idaklu.py index 13b7b0cb48..1231605a5e 100644 --- a/tests/integration/test_solvers/test_idaklu.py +++ b/tests/integration/test_solvers/test_idaklu.py @@ -1,5 +1,4 @@ import numpy as np -import pytest import pybamm @@ -211,29 +210,23 @@ def test_with_experiments(self): sols[1].cycles[-1]["Current [A]"].data, ) - @pytest.mark.parametrize( - "model_cls", - [ - pybamm.lithium_ion.SPM, - pybamm.lithium_ion.DFN, - ], - ids=["SPM", "DFN"], - ) - def test_multiple_initial_conditions_against_independent_solves(self, model_cls): - model = model_cls() - geom = model.default_geometry - param = model.default_parameter_values - param.update({"Current function [A]": "[input]"}) - param.process_model(model) - param.process_geometry(geom) - mesh = pybamm.Mesh(geom, model.default_submesh_types, model.default_var_pts) - disc = pybamm.Discretisation(mesh, model.default_spatial_methods) + def test_multiple_initial_conditions_against_independent_solves(self): + model = pybamm.BaseModel() + u = pybamm.Variable("u") + v = pybamm.Variable("v") + u0 = pybamm.InputParameter("u0") + v0 = pybamm.InputParameter("v0") + model.rhs = {u: -u, v: -2 * v} + model.initial_conditions = {u: u0, v: v0} + model.variables = {"u": u, "v": v} + + disc = pybamm.Discretisation() disc.process_model(model) t_eval = np.array([0, 1]) solver = pybamm.IDAKLUSolver() - inputs = [{"Current function [A]": value} for value in [0.1, 2.0]] + inputs = [{"u0": 3, "v0": 4}, {"u0": 5, "v0": 6}] multi_sols = solver.solve( model, @@ -241,23 +234,19 @@ def test_multiple_initial_conditions_against_independent_solves(self, model_cls) inputs=inputs, ) assert isinstance(multi_sols, list) and len(multi_sols) == 2 + np.testing.assert_equal([sol["u"](0) for sol in multi_sols], [3, 5]) indep_sols = [] for ic in inputs: sol_indep = solver.solve(model, t_eval, inputs=ic) indep_sols.append(sol_indep) - if model_cls is pybamm.lithium_ion.SPM: - rtol, atol = 1e-8, 1e-10 - else: - rtol, atol = 1e-6, 1e-8 - for idx in (0, 1): sol_vec = multi_sols[idx] sol_ind = indep_sols[idx] - np.testing.assert_allclose(sol_vec.t, sol_ind.t, rtol=1e-12, atol=0) - np.testing.assert_allclose(sol_vec.y, sol_ind.y, rtol=rtol, atol=atol) + np.testing.assert_allclose(sol_vec.t, sol_ind.t) + np.testing.assert_allclose(sol_vec.y, sol_ind.y) def test_outvars_with_experiments_multi_simulation(self): model = pybamm.lithium_ion.SPM() diff --git a/tests/unit/test_solvers/test_idaklu_solver.py b/tests/unit/test_solvers/test_idaklu_solver.py index 08469d148c..afbdd1a106 100644 --- a/tests/unit/test_solvers/test_idaklu_solver.py +++ b/tests/unit/test_solvers/test_idaklu_solver.py @@ -1176,7 +1176,7 @@ def test_model_solver_with_non_identity_mass(self): 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_multiple_initial_conditions_dict(self): + def test_multiple_initial_conditions_single_variable(self): model = pybamm.BaseModel() model.convert_to_format = None u = pybamm.Variable("u") @@ -1213,7 +1213,7 @@ def test_multiple_initial_conditions_dict(self): atol=1e-5, ) - def test_single_initial_condition_dict(self): + def test_single_initial_condition_single_variable(self): model = pybamm.BaseModel() model.convert_to_format = "casadi" u = pybamm.Variable("u") @@ -1240,7 +1240,7 @@ def test_single_initial_condition_dict(self): solution["u"](t_eval), 5 * np.exp(-t_eval), rtol=1e-3, atol=1e-5 ) - def test_multiple_variables(self): + def test_multiple_initial_conditions_multiple_variables(self): model = pybamm.BaseModel() u = pybamm.Variable("u") v = pybamm.Variable("v") From 7bb4d997f9823e66af981ab8cfea8b9a7a16f15b Mon Sep 17 00:00:00 2001 From: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> Date: Thu, 13 Nov 2025 09:12:24 +0530 Subject: [PATCH 21/42] Don't be too strict with func_args longer than symbol.children --- src/pybamm/parameters/parameter_values.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/pybamm/parameters/parameter_values.py b/src/pybamm/parameters/parameter_values.py index 2bec326a93..845be2b0bf 100644 --- a/src/pybamm/parameters/parameter_values.py +++ b/src/pybamm/parameters/parameter_values.py @@ -866,12 +866,21 @@ def _process_function_parameter(self, symbol): else: new_children.append(self.process_symbol(child)) - # Get the expression and inputs for the function + # Get the expression and inputs for the function. + # func_args may include arguments that were not explicitly wired up + # in this FunctionParameter (e.g., kwargs with default values). After + # serialisation/deserialisation, we only recover the children that were + # actually connected. + # + # Using strict=True here therefore raises a ValueError when there are + # more args than children. We allow func_args to be longer than + # symbol.children and only build the mapping for the args for which we + # actually have children. expression = function_parameter.child inputs = { arg: child for arg, child in zip( - function_parameter.func_args, symbol.children, strict=True + function_parameter.func_args, symbol.children, strict=False ) } From 56bd16fbb40614dfc78afb8d794fd886a8062c27 Mon Sep 17 00:00:00 2001 From: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> Date: Thu, 13 Nov 2025 09:15:45 +0530 Subject: [PATCH 22/42] Add a test --- .../test_parameters/test_parameter_values.py | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/unit/test_parameters/test_parameter_values.py b/tests/unit/test_parameters/test_parameter_values.py index 769705cf51..50b4c7065d 100644 --- a/tests/unit/test_parameters/test_parameter_values.py +++ b/tests/unit/test_parameters/test_parameter_values.py @@ -1298,6 +1298,31 @@ def test_to_json_with_filename(self): finally: os.remove(temp_path) + def test_roundtrip_with_keyword_args(self): + def func_no_kwargs(x): + return 2 * x + + def func_with_kwargs(x, y=1): + return 2 * x + + x = pybamm.Scalar(2) + func_param = pybamm.FunctionParameter("func", {"x": x}) + + parameter_values = pybamm.ParameterValues({"func": func_no_kwargs}) + assert parameter_values.evaluate(func_param) == 4.0 + + serialized = parameter_values.to_json() + parameter_values_loaded = pybamm.ParameterValues.from_json(serialized) + assert parameter_values_loaded.evaluate(func_param) == 4.0 + + parameter_values = pybamm.ParameterValues({"func": func_with_kwargs}) + assert parameter_values.evaluate(func_param) == 4.0 + + serialized = parameter_values.to_json() + parameter_values_loaded = pybamm.ParameterValues.from_json(serialized) + + assert parameter_values_loaded.evaluate(func_param) == 4.0 + def test_convert_symbols_in_dict_with_interpolator(self): """Test convert_symbols_in_dict with interpolator (covers lines 1154-1170).""" import numpy as np From 425af117ee378ac1c77209100e5db74788145064 Mon Sep 17 00:00:00 2001 From: Swasti Mishra <140950062+swastim01@users.noreply.github.com> Date: Sun, 16 Nov 2025 02:59:38 +0530 Subject: [PATCH 23/42] Add support for uniform grid sizing across subdomains (#720) (#5253) Co-authored-by: Valentin Sulzer --- CHANGELOG.md | 1 + src/pybamm/meshes/meshes.py | 33 +++++++++++++++++++++++++++ tests/unit/test_meshes/test_meshes.py | 31 +++++++++++++++++++++++++ 3 files changed, 65 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d51fc7641..f5ff296807 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ ## Features +- Added uniform grid sizing across subdomains in the x-dimension, ensuring consistent grid spacing when geometries have varying lengths. ([#5253](https://github.com/pybamm-team/PyBaMM/pull/5253)) - Added the `electrode_phases` kwarg to `plot_voltage_components()` which allows choosing between plotting primary or secondary phase overpotentials. ([#5229](https://github.com/pybamm-team/PyBaMM/pull/5229)) - Added the `num_steps_no_progress` and `t_no_progress` options in the `IDAKLUSolver` to early terminate the simulation if little progress is detected. ([#5201](https://github.com/pybamm-team/PyBaMM/pull/5201)) - EvaluateAt symbol: add support for children evaluated at edges ([#5190](https://github.com/pybamm-team/PyBaMM/pull/5190)) diff --git a/src/pybamm/meshes/meshes.py b/src/pybamm/meshes/meshes.py index f717b0a347..ddc295d57b 100644 --- a/src/pybamm/meshes/meshes.py +++ b/src/pybamm/meshes/meshes.py @@ -9,6 +9,39 @@ import pybamm +def compute_var_pts_from_thicknesses(electrode_thicknesses, grid_size): + """ + Compute a ``var_pts`` dictionary using electrode thicknesses and a target cell size (dx). + + Added as per maintainer feedback in issue # to make mesh generation + explicit — ``grid_size`` now represents the mesh cell size in metres. + + Parameters + ---------- + electrode_thicknesses : dict + Domain thicknesses in metres. + grid_size : float + Desired uniform mesh cell size (m). + + Returns + ------- + dict + Mapping of each domain to its computed grid points. + """ + if not isinstance(electrode_thicknesses, dict): + raise TypeError("electrode_thicknesses must be a dictionary") + + if not isinstance(grid_size, (int | float)) or grid_size <= 0: + raise ValueError("grid_size must be a positive number") + + var_pts = {} + for domain, thickness in electrode_thicknesses.items(): + npts = max(round(thickness / grid_size), 2) + var_pts[domain] = {f"x_{domain[0]}": npts} + + return var_pts + + class Mesh(dict): """ Mesh contains a list of submeshes on each subdomain. diff --git a/tests/unit/test_meshes/test_meshes.py b/tests/unit/test_meshes/test_meshes.py index 8c26a0900f..15ef8317ba 100644 --- a/tests/unit/test_meshes/test_meshes.py +++ b/tests/unit/test_meshes/test_meshes.py @@ -584,6 +584,37 @@ def test_to_json(self): assert mesh_json == expected_json + def test_compute_var_pts_from_thicknesses_cell_size(self): + from pybamm.meshes.meshes import compute_var_pts_from_thicknesses + + electrode_thicknesses = { + "negative electrode": 100e-6, + "separator": 25e-6, + "positive electrode": 100e-6, + } + + cell_size = 5e-6 # 5 micrometres per cell + var_pts = compute_var_pts_from_thicknesses(electrode_thicknesses, cell_size) + + assert isinstance(var_pts, dict) + assert all(isinstance(v, dict) for v in var_pts.values()) + assert var_pts["negative electrode"]["x_n"] == 20 + assert var_pts["separator"]["x_s"] == 5 + assert var_pts["positive electrode"]["x_p"] == 20 + + def test_compute_var_pts_from_thicknesses_invalid_thickness_type(self): + from pybamm.meshes.meshes import compute_var_pts_from_thicknesses + + with pytest.raises(TypeError): + compute_var_pts_from_thicknesses(["not", "a", "dict"], 1e-6) + + def test_compute_var_pts_from_thicknesses_invalid_grid_size(self): + from pybamm.meshes.meshes import compute_var_pts_from_thicknesses + + electrode_thicknesses = {"negative electrode": 100e-6} + with pytest.raises(ValueError): + compute_var_pts_from_thicknesses(electrode_thicknesses, -1e-6) + class TestMeshGenerator: def test_init_name(self): From 6ba2cde8dcee5db4732752682fed6b646e613659 Mon Sep 17 00:00:00 2001 From: Chase Naples Date: Sun, 16 Nov 2025 11:24:14 -0500 Subject: [PATCH 24/42] Fix typo in Butler-Volmer equation docstring (#5279) --- src/pybamm/models/submodels/interface/kinetics/butler_volmer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pybamm/models/submodels/interface/kinetics/butler_volmer.py b/src/pybamm/models/submodels/interface/kinetics/butler_volmer.py index c3e9ac0fc0..84cee586a3 100644 --- a/src/pybamm/models/submodels/interface/kinetics/butler_volmer.py +++ b/src/pybamm/models/submodels/interface/kinetics/butler_volmer.py @@ -12,7 +12,7 @@ class SymmetricButlerVolmer(BaseKinetics): Submodel which implements the symmetric forward Butler-Volmer equation: .. math:: - j = 2 * j_0(c) * \\sinh(ne * F * \\eta_r(c) / RT) + j = 2 * j_0(c) * \\sinh(ne * F * \\eta_r(c) / 2RT) Parameters ---------- From e8931565f4355951872d2bae0db50d7d73dbf6e1 Mon Sep 17 00:00:00 2001 From: Robert Timms <43040151+rtimms@users.noreply.github.com> Date: Tue, 18 Nov 2025 07:30:57 +0000 Subject: [PATCH 25/42] fix bug with bulk ocp lithiation (#5280) --- .../interface/open_circuit_potential/base_hysteresis_ocp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pybamm/models/submodels/interface/open_circuit_potential/base_hysteresis_ocp.py b/src/pybamm/models/submodels/interface/open_circuit_potential/base_hysteresis_ocp.py index 934ed5e6cf..cdfc47ac5e 100644 --- a/src/pybamm/models/submodels/interface/open_circuit_potential/base_hysteresis_ocp.py +++ b/src/pybamm/models/submodels/interface/open_circuit_potential/base_hysteresis_ocp.py @@ -84,7 +84,7 @@ def _get_coupled_variables(self, variables): U_eq = self.phase_param.U(sto_surf, T) U_eq_x_av = self.phase_param.U(sto_surf, T) U_lith = self.phase_param.U(sto_surf, T, "lithiation") - U_lith_bulk = self.phase_param.U(sto_bulk, T_bulk) + U_lith_bulk = self.phase_param.U(sto_bulk, T_bulk, "lithiation") U_delith = self.phase_param.U(sto_surf, T, "delithiation") U_delith_bulk = self.phase_param.U(sto_bulk, T_bulk, "delithiation") From 6c8cbfd3d83814925ebf9e1f1aef65e4a4d08ae7 Mon Sep 17 00:00:00 2001 From: Gregor Decristoforo Date: Wed, 19 Nov 2025 15:47:54 +0100 Subject: [PATCH 26/42] doc: fix typo in concentration description in notebook (#5284) * Fix typo in concentration description in notebook * Add CHANGELOG.md entry for typo fix * Remove unneccesary changelog entry Co-authored-by: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> --------- Co-authored-by: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> --- .../4-comparing-full-and-reduced-order-models.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/examples/notebooks/creating_models/4-comparing-full-and-reduced-order-models.ipynb b/docs/source/examples/notebooks/creating_models/4-comparing-full-and-reduced-order-models.ipynb index 071fd54f95..19780371c0 100644 --- a/docs/source/examples/notebooks/creating_models/4-comparing-full-and-reduced-order-models.ipynb +++ b/docs/source/examples/notebooks/creating_models/4-comparing-full-and-reduced-order-models.ipynb @@ -24,7 +24,7 @@ "$$\n", "\\left.c\\right\\vert_{t=0} = c_0,\n", "$$\n", - "where $c$$ is the concentration, $r$ the radial coordinate, $t$ time, $R$ the particle radius, $D$ the diffusion coefficient, $j$ the interfacial current density, $F$ Faraday's constant, and $c_0$ the initial concentration. \n", + "where $c$ is the concentration, $r$ the radial coordinate, $t$ time, $R$ the particle radius, $D$ the diffusion coefficient, $j$ the interfacial current density, $F$ Faraday's constant, and $c_0$ the initial concentration. \n", "\n", "As in the previous example we use the following parameters:\n", "\n", From 385ae7b3ccf37b6bf32fc37dc03dde80e0abeeda Mon Sep 17 00:00:00 2001 From: Pip Liggins Date: Thu, 20 Nov 2025 17:23:47 +0000 Subject: [PATCH 27/42] fix: instruct uv to install into system for CI (#5288) --- .github/workflows/benchmark_on_push.yml | 2 +- .github/workflows/periodic_benchmarks.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/benchmark_on_push.yml b/.github/workflows/benchmark_on_push.yml index 18a9e7931d..f70556c782 100644 --- a/.github/workflows/benchmark_on_push.yml +++ b/.github/workflows/benchmark_on_push.yml @@ -40,7 +40,7 @@ jobs: enable-cache: true - name: Install python dependencies - run: uv pip install asv[virtualenv] + run: uv pip install --system asv - name: Fetch base branch run: | diff --git a/.github/workflows/periodic_benchmarks.yml b/.github/workflows/periodic_benchmarks.yml index 86cd1991ab..0a7a7d9c47 100644 --- a/.github/workflows/periodic_benchmarks.yml +++ b/.github/workflows/periodic_benchmarks.yml @@ -48,7 +48,7 @@ jobs: enable-cache: true - name: Install python dependencies - run: uv pip install asv[virtualenv] + run: uv pip install --system asv - name: Run benchmarks run: | From 1319b4a985b848a17d9714888c323149d42a4b9a Mon Sep 17 00:00:00 2001 From: Marc Berliner <34451391+MarcBerliner@users.noreply.github.com> Date: Thu, 20 Nov 2025 14:32:21 -0500 Subject: [PATCH 28/42] Fix `InputParameter` serialisation (#5289) * fix `InputParameter` serialisation * Update CHANGELOG.md --- CHANGELOG.md | 2 ++ src/pybamm/expression_tree/operations/serialise.py | 2 ++ tests/unit/test_serialisation/test_serialisation.py | 8 ++++++++ 3 files changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ba2390255..6fb52142ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ## Bug fixes +- Fix a bug with serialising `InputParameter`s. ([#5289](https://github.com/pybamm-team/PyBaMM/pull/5289)) + # [v25.10.1](https://github.com/pybamm-team/PyBaMM/tree/v25.10.1) - 2025-11-14 ## Features diff --git a/src/pybamm/expression_tree/operations/serialise.py b/src/pybamm/expression_tree/operations/serialise.py index 6b733e0fad..3cc8f3e98f 100644 --- a/src/pybamm/expression_tree/operations/serialise.py +++ b/src/pybamm/expression_tree/operations/serialise.py @@ -1625,6 +1625,8 @@ def convert_symbol_from_json(json_data): elif json_data["type"] == "Parameter": # Convert stored parameters back to PyBaMM Parameter objects return pybamm.Parameter(json_data["name"]) + elif json_data["type"] == "InputParameter": + return pybamm.InputParameter(json_data["name"]) elif json_data["type"] == "Scalar": # Convert stored numerical values back to PyBaMM Scalar objects return pybamm.Scalar(json_data["value"]) diff --git a/tests/unit/test_serialisation/test_serialisation.py b/tests/unit/test_serialisation/test_serialisation.py index d6ddac10e4..4dafcc5641 100644 --- a/tests/unit/test_serialisation/test_serialisation.py +++ b/tests/unit/test_serialisation/test_serialisation.py @@ -617,6 +617,14 @@ def test_serialise_time(self): t2 = convert_symbol_from_json(j) assert isinstance(t2, pybamm.Time) + def test_serialise_input_parameter(self): + """Test InputParameter serialization and deserialization.""" + ip = pybamm.InputParameter("test_param") + j = convert_symbol_to_json(ip) + ip_restored = convert_symbol_from_json(j) + assert isinstance(ip_restored, pybamm.InputParameter) + assert ip_restored.name == "test_param" + def test_convert_symbol_to_json_with_number_and_list(self): for val in (0, 3.14, -7, True): out = convert_symbol_to_json(val) From 80e18706796be782db7bf8cc0a9400b4a6546ff6 Mon Sep 17 00:00:00 2001 From: Brady Planden <55357039+BradyPlanden@users.noreply.github.com> Date: Fri, 21 Nov 2025 09:08:04 +0000 Subject: [PATCH 29/42] Bugfix: inputs for `initial_conditions_from` scale evaluation (#5285) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- src/pybamm/models/base_model.py | 11 +++++++++-- .../full_battery_models/lithium_ion/electrode_soh.py | 6 +++--- src/pybamm/solvers/base_solver.py | 4 +++- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/pybamm/models/base_model.py b/src/pybamm/models/base_model.py index 9fd7483385..67096912cf 100644 --- a/src/pybamm/models/base_model.py +++ b/src/pybamm/models/base_model.py @@ -906,7 +906,12 @@ def _build_model(self): self.build_model_equations() def set_initial_conditions_from( - self, solution, inplace=True, return_type="model", mesh=None + self, + solution, + inputs=None, + inplace=True, + return_type="model", + mesh=None, ): """ Update initial conditions with the final states from a Solution object or from @@ -918,6 +923,8 @@ def set_initial_conditions_from( ---------- solution : :class:`pybamm.Solution`, or dict The solution to use to initialize the model + inputs : dict + The dictionary of model input parameters. inplace : bool, optional Whether to modify the model inplace or create a new model (default True) return_type : str, optional @@ -1081,7 +1088,7 @@ def get_variable_state(var): scale, reference = pybamm.Scalar(1), pybamm.Scalar(0) initial_conditions[var] = ( pybamm.Vector(final_state_eval) - reference - ) / scale.evaluate() + ) / scale.evaluate(inputs=inputs) # Also update the concatenated initial conditions if the model is already # discretised diff --git a/src/pybamm/models/full_battery_models/lithium_ion/electrode_soh.py b/src/pybamm/models/full_battery_models/lithium_ion/electrode_soh.py index a9322991b2..b9abb4261f 100644 --- a/src/pybamm/models/full_battery_models/lithium_ion/electrode_soh.py +++ b/src/pybamm/models/full_battery_models/lithium_ion/electrode_soh.py @@ -561,14 +561,14 @@ def _set_up_solve(self, inputs, direction): def _solve_full(self, inputs, ics, direction): sim = self._get_electrode_soh_sims_full(direction) sim.build() - sim.built_model.set_initial_conditions_from(ics) + sim.built_model.set_initial_conditions_from(ics, inputs=inputs) sol = sim.solve([0], inputs=inputs) return sol def _solve_split(self, inputs, ics, direction): x100_sim, x0_sim = self._get_electrode_soh_sims_split(direction) x100_sim.build() - x100_sim.built_model.set_initial_conditions_from(ics) + x100_sim.built_model.set_initial_conditions_from(ics, inputs=inputs) x100_sol = x100_sim.solve([0], inputs=inputs) if self.options["open-circuit potential"] == "MSMR": inputs["Un(x_100)"] = x100_sol["Un(x_100)"].data[0] @@ -577,7 +577,7 @@ def _solve_split(self, inputs, ics, direction): inputs["x_100"] = x100_sol["x_100"].data[0] inputs["y_100"] = x100_sol["y_100"].data[0] x0_sim.build() - x0_sim.built_model.set_initial_conditions_from(ics) + x0_sim.built_model.set_initial_conditions_from(ics, inputs=inputs) x0_sol = x0_sim.solve([0], inputs=inputs) return x0_sol diff --git a/src/pybamm/solvers/base_solver.py b/src/pybamm/solvers/base_solver.py index 92c7381f43..a1db0c2953 100644 --- a/src/pybamm/solvers/base_solver.py +++ b/src/pybamm/solvers/base_solver.py @@ -1381,7 +1381,9 @@ def step( else: _, concatenated_initial_conditions = model.set_initial_conditions_from( - old_solution, return_type="ics" + old_solution, + inputs=model_inputs, + return_type="ics", ) model.y0 = concatenated_initial_conditions.evaluate(0, inputs=model_inputs) if using_sensitivities: From f77c00268a4d407400f903e708efa86cceb241a6 Mon Sep 17 00:00:00 2001 From: Pip Liggins Date: Fri, 21 Nov 2025 11:37:19 +0000 Subject: [PATCH 30/42] Fix broken test after merge --- src/pybamm/solvers/base_solver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pybamm/solvers/base_solver.py b/src/pybamm/solvers/base_solver.py index 72eb8ba73d..1901ed381d 100644 --- a/src/pybamm/solvers/base_solver.py +++ b/src/pybamm/solvers/base_solver.py @@ -1161,7 +1161,7 @@ def _check_events_with_initialization(t_eval, model, y0, inputs_dict): event_eval = event(t_eval[0], y0, inputs) elif model.convert_to_format in ["python", "jax"]: event_eval = event(t=t_eval[0], y=y0, inputs=inputs_dict) - events_eval[idx] = event_eval + events_eval[idx] = np.array(event_eval) if events_eval.min() <= 0: # find the events that were triggered by initial conditions From 300c5536c81db87149ed842e4f8c44ecf6d714e8 Mon Sep 17 00:00:00 2001 From: Pip Liggins Date: Mon, 24 Nov 2025 17:25:50 +0000 Subject: [PATCH 31/42] Move calculation of t_eval with discontinuities into single loop --- .../expression_tree/binary_operators.py | 47 ++++++ src/pybamm/solvers/base_solver.py | 140 +++++++----------- tests/unit/test_solvers/test_scipy_solver.py | 33 ----- 3 files changed, 101 insertions(+), 119 deletions(-) diff --git a/src/pybamm/expression_tree/binary_operators.py b/src/pybamm/expression_tree/binary_operators.py index f3d3bcccb1..8202b9e615 100644 --- a/src/pybamm/expression_tree/binary_operators.py +++ b/src/pybamm/expression_tree/binary_operators.py @@ -213,6 +213,10 @@ def to_json(self): return json_dict + def _t_discon(self, expr, y0, inputs, num_events): + """Returns the discontinuity time-points for a function.""" + raise NotImplementedError(f"`_t_discon` not implemented for {self.name}") + class Power(BinaryOperator): """ @@ -673,6 +677,18 @@ def _evaluate_for_shape(self): # an array of NaNs return self._binary_evaluate(left, right) * np.nan + def _t_discon(self, expr, y0, inputs, num_events): + """Returns the discontinuity time-points for the heaviside function.""" + value = expr.evaluate(0, y0, inputs=inputs) + t_discon = [value] + t_discon.append(self._t_discon_next(value)) + return t_discon + + def _t_discon_next(self, value: float): + raise NotImplementedError( + "_t_discon_next method should be implemented in subclasses of _Heaviside" + ) + class EqualHeaviside(_Heaviside): """A heaviside function with equality (return 1 when left = right)""" @@ -695,6 +711,16 @@ def _binary_evaluate(self, left, right): with np.errstate(invalid="ignore"): return left <= right + def _t_discon_next(self, value: float): + if self.left == pybamm.t: + # t <= x + # Stop at t = x and right after t = x + return np.nextafter(value, np.inf) + else: + # t >= x + # Stop at t = x and right before t = x + return np.nextafter(value, -np.inf) + class NotEqualHeaviside(_Heaviside): """A heaviside function without equality (return 0 when left = right)""" @@ -716,6 +742,16 @@ def _binary_evaluate(self, left, right): with np.errstate(invalid="ignore"): return left < right + def _t_discon_next(self, value: float): + if self.left == pybamm.t: + # t < x + # Stop at t = x and right before t = x + return np.nextafter(value, -np.inf) + else: + # t > x + # Stop at t = x and right after t = x + return np.nextafter(value, np.inf) + class Modulo(BinaryOperator): """Calculates the remainder of an integer division.""" @@ -758,6 +794,17 @@ def _binary_evaluate(self, left, right): """See :meth:`pybamm.BinaryOperator._binary_evaluate()`.""" return left % right + def _t_discon(self, expr, y0, inputs, num_events): + value = expr.evaluate(0, y0, inputs=inputs) + t_discon = [] + + for i in np.arange(num_events): + t = value * (i + 1) + # Stop right before t and at t + t_discon.append(np.nextafter(t, -np.inf)) + t_discon.append(t) + return t_discon + class Minimum(BinaryOperator): """Returns the smaller of two objects.""" diff --git a/src/pybamm/solvers/base_solver.py b/src/pybamm/solvers/base_solver.py index 1901ed381d..0f7cf6c24b 100644 --- a/src/pybamm/solvers/base_solver.py +++ b/src/pybamm/solvers/base_solver.py @@ -232,7 +232,7 @@ def set_up( casadi_switch_events, terminate_events, interpolant_extrapolation_events, - t_discon_constant, + t_discon_constant_symbols, discontinuity_events, ) = self._set_up_events(model, t_eval, inputs, vars_for_processing) @@ -244,7 +244,7 @@ def set_up( model.terminate_events_eval = terminate_events model.interpolant_extrapolation_events_eval = interpolant_extrapolation_events model.discontinuity_events_eval = discontinuity_events - model.t_discon_constant = t_discon_constant + model.t_discon_constant_symbols = t_discon_constant_symbols model.jac_rhs_eval = jac_rhs model.jac_rhs_action_eval = jac_rhs_action @@ -491,7 +491,7 @@ def _set_up_model_sensitivities_inplace(model): model.mass_matrix_inv.entries[: model.len_rhs, : model.len_rhs] ) - def _set_up_events(self, model, t_eval, inputs, vars_for_processing): + def _set_up_events(self, model, t_eval, inputs: list[dict], vars_for_processing): # Check for heaviside and modulo functions in rhs and algebraic and add # discontinuity events if these exist. # Note: only checks for the case of t < X, t <= X, X < t, or X <= t, @@ -503,65 +503,7 @@ def supports_t_eval_discontinuities(expr): return self.supports_t_eval_discontinuities and expr.is_constant() # Find all the constant time-based discontinuities - t_discon = [] - - def append_t_discon(t): - t_discon.append(t) - - def heaviside_event(symbol, expr): - model.events.append( - pybamm.Event( - str(symbol), - expr, - pybamm.EventType.DISCONTINUITY, - ) - ) - - # TODO: still needs to be fixed - def heaviside_t_discon(symbol, expr): - value = expr.evaluate(0, model.y0_list[0].full(), inputs=inputs) - append_t_discon(value) - - if isinstance(symbol, pybamm.EqualHeaviside): - if symbol.left == pybamm.t: - # t <= x - # Stop at t = x and right after t = x - append_t_discon(np.nextafter(value, np.inf)) - else: - # t >= x - # Stop at t = x and right before t = x - append_t_discon(np.nextafter(value, -np.inf)) - elif isinstance(symbol, pybamm.NotEqualHeaviside): - if symbol.left == pybamm.t: - # t < x - # Stop at t = x and right before t = x - append_t_discon(np.nextafter(value, -np.inf)) - else: - # t > x - # Stop at t = x and right after t = x - append_t_discon(np.nextafter(value, np.inf)) - else: - raise ValueError( - f"Unknown heaviside function: {symbol}" - ) # pragma: no cover - - def modulo_event(symbol, expr, num_events): - for i in np.arange(num_events): - model.events.append( - pybamm.Event( - str(symbol), - expr * pybamm.Scalar(i + 1), - pybamm.EventType.DISCONTINUITY, - ) - ) - - def modulo_t_discon(symbol, expr, num_events): - value = expr.evaluate(0, model.y0_list[0].full(), inputs=inputs) - for i in np.arange(num_events): - t = value * (i + 1) - # Stop right before t and at t - append_t_discon(np.nextafter(t, -np.inf)) - append_t_discon(t) + t_discon_symbols = [] for symbol in itertools.chain( model.concatenated_rhs.pre_order(), @@ -578,18 +520,32 @@ def modulo_t_discon(symbol, expr, num_events): continue # pragma: no cover if supports_t_eval_discontinuities(expr): - heaviside_t_discon(symbol, expr) + # save the symbol and expression ready for evaluation later + t_discon_symbols.append((symbol, expr, None)) else: - heaviside_event(symbol, expr) + model.events.append( + pybamm.Event( + str(symbol), + expr, + pybamm.EventType.DISCONTINUITY, + ) + ) elif isinstance(symbol, pybamm.Modulo) and symbol.left == pybamm.t: expr = symbol.right num_events = 200 if (t_eval is None) else (tf // expr.value) if supports_t_eval_discontinuities(expr): - modulo_t_discon(symbol, expr, num_events) + t_discon_symbols.append((symbol, expr, num_events)) else: - modulo_event(symbol, expr, num_events) + for i in np.arange(num_events): + model.events.append( + pybamm.Event( + str(symbol), + expr * pybamm.Scalar(i + 1), + pybamm.EventType.DISCONTINUITY, + ) + ) else: continue @@ -653,7 +609,7 @@ def modulo_t_discon(symbol, expr, num_events): casadi_switch_events, terminate_events, interpolant_extrapolation_events, - t_discon, + t_discon_symbols, discontinuity_events, ) @@ -976,11 +932,26 @@ def solve( self._check_events_with_initialization(t_eval, model, y0, inpts) # Process discontinuities - ( - start_indices, - end_indices, - t_eval, - ) = self._get_discontinuity_start_end_indices(model, inputs, t_eval) + t_eval_info = [ + self._get_discontinuity_start_end_indices( + model, model.y0_list[i], inputs, t_eval + ) + for i, inputs in enumerate(model_inputs_list) + ] + + first_row = t_eval_info[0] + for row in t_eval_info[1:]: + if not all( + np.array_equal(row_ele, first_row_ele) + for row_ele, first_row_ele in zip(row, first_row, strict=True) + ): + # Can't handle different `t_eval`s for each input set + raise pybamm.SolverError( + "Discontinuity events occur at different times between input parameter sets. " + "Please ensure that all input sets produce the same discontinuities." + ) + + start_indices, end_indices, t_eval = first_row # Integrate separately over each time segment and accumulate into the solution # object, restarting the solver at each discontinuity (and recalculating a @@ -1098,33 +1069,30 @@ def filter_discontinuities(t_discon: list, t_eval: list) -> np.ndarray: idx_end = np.searchsorted(t_discon_unique, t_eval[-1], side="left") return t_discon_unique[idx_start:idx_end] - def _get_discontinuity_start_end_indices(self, model, inputs, t_eval): - if self.supports_t_eval_discontinuities: - t_discon_constant = self.filter_discontinuities( - model.t_discon_constant, t_eval - ) + def _get_discontinuity_start_end_indices(self, model, y0, inputs, t_eval): + if self.supports_t_eval_discontinuities and model.t_discon_constant_symbols: + pybamm.logger.verbose("Discontinuity events found for constant symbols") + _t_discon_constant = [] + for symbol, expr, num_events in model.t_discon_constant_symbols: + _t_discon_constant.extend( + symbol._t_discon(expr, y0, inputs, num_events) + ) + + t_discon_constant = self.filter_discontinuities(_t_discon_constant, t_eval) t_eval = np.union1d(t_eval, t_discon_constant) if not model.discontinuity_events_eval: - pybamm.logger.verbose("No discontinuity events found") + pybamm.logger.verbose("No additional discontinuity events found") return [0], [len(t_eval)], t_eval # Calculate all possible discontinuities _t_discon_full = [ - # Assuming that discontinuities do not depend on - # input parameters when len(input_list) > 1, only - # `inputs` is passed to `evaluate`. - # See https://github.com/pybamm-team/PyBaMM/pull/1261 event.expression.evaluate(inputs=inputs) for event in model.discontinuity_events_eval ] t_discon = self.filter_discontinuities(_t_discon_full, t_eval) pybamm.logger.verbose(f"Discontinuity events found at t = {t_discon}") - if isinstance(inputs, list): - raise pybamm.SolverError( - "Cannot solve for a list of input parameters sets with discontinuities" - ) # insert time points around discontinuities in t_eval # keep track of subsections to integrate by storing start and end indices diff --git a/tests/unit/test_solvers/test_scipy_solver.py b/tests/unit/test_solvers/test_scipy_solver.py index 79b7cb900f..2d1bce153d 100644 --- a/tests/unit/test_solvers/test_scipy_solver.py +++ b/tests/unit/test_solvers/test_scipy_solver.py @@ -301,39 +301,6 @@ def test_model_solver_multiple_inputs_happy_path(self, subtests): solution.y[0], np.exp(-0.01 * (i + 1) * solution.t) ) - def test_model_solver_multiple_inputs_discontinuity_error(self): - # Create model - model = pybamm.BaseModel() - model.convert_to_format = "casadi" - domain = ["negative electrode", "separator", "positive electrode"] - var = pybamm.Variable("var", domain=domain) - model.rhs = {var: -pybamm.InputParameter("rate") * var} - model.initial_conditions = {var: 1} - # create discretisation - mesh = get_mesh_for_testing() - spatial_methods = {"macroscale": pybamm.FiniteVolume()} - disc = pybamm.Discretisation(mesh, spatial_methods) - disc.process_model(model) - - solver = pybamm.ScipySolver(rtol=1e-8, atol=1e-8, method="RK45") - t_eval = np.linspace(0, 10, 100) - ninputs = 8 - inputs_list = [{"rate": 0.01 * (i + 1)} for i in range(ninputs)] - - model.events = [ - pybamm.Event( - "discontinuity", - pybamm.Scalar(t_eval[-1] / 2), - event_type=pybamm.EventType.DISCONTINUITY, - ) - ] - with pytest.raises( - pybamm.SolverError, - match="Cannot solve for a list of input parameters" - " sets with discontinuities", - ): - solver.solve(model, t_eval, inputs=inputs_list, nproc=2) - def test_model_solver_multiple_inputs_initial_conditions(self): # Create model model = pybamm.BaseModel() From 73ab559adc229264294fcc085daee503e969bdde Mon Sep 17 00:00:00 2001 From: Brady Planden <55357039+BradyPlanden@users.noreply.github.com> Date: Mon, 24 Nov 2025 20:31:26 +0000 Subject: [PATCH 32/42] Add `silence_sundials_errors` solver option (#5290) * feat: add`silence_sundial_warnings` solver option * refactor: `silence_sundials_warnings` -> `silence_sundials_errors` --- CHANGELOG.md | 2 ++ src/pybamm/solvers/idaklu_solver.py | 3 +++ 2 files changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fb52142ad..3463d08e5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Features +- Adds `silence_sundials_errors` IDAKLU solver option with `default=False` to match historical output. ([#5290](https://github.com/pybamm-team/PyBaMM/pull/5290)) + ## Bug fixes - Fix a bug with serialising `InputParameter`s. ([#5289](https://github.com/pybamm-team/PyBaMM/pull/5289)) diff --git a/src/pybamm/solvers/idaklu_solver.py b/src/pybamm/solvers/idaklu_solver.py index 4a0d9edb07..e418b4d3e5 100644 --- a/src/pybamm/solvers/idaklu_solver.py +++ b/src/pybamm/solvers/idaklu_solver.py @@ -76,6 +76,8 @@ class IDAKLUSolver(pybamm.BaseSolver): "increment_factor": 1.0, # Enable or disable linear solution scaling "linear_solution_scaling": True, + # Silence Sundials errors during solve + "silence_sundials_errors": False, ## Main solver # Maximum order of the linear multistep method "max_order_bdf": 5, @@ -176,6 +178,7 @@ def __init__( "epsilon_linear_tolerance": 0.05, "increment_factor": 1.0, "linear_solution_scaling": True, + "silence_sundials_errors": False, "max_order_bdf": 5, "max_num_steps": 100000, "dt_init": 0.0, From a1f5a90db63586eb6ead5996fc53da592ca3eccb Mon Sep 17 00:00:00 2001 From: Pip Liggins Date: Wed, 26 Nov 2025 13:07:35 +0000 Subject: [PATCH 33/42] Experiments can't use lists of inputs, remove references to inputs list in `step` --- src/pybamm/solvers/base_solver.py | 133 ++++++++++++------------------ 1 file changed, 54 insertions(+), 79 deletions(-) diff --git a/src/pybamm/solvers/base_solver.py b/src/pybamm/solvers/base_solver.py index 0f7cf6c24b..e863f236eb 100644 --- a/src/pybamm/solvers/base_solver.py +++ b/src/pybamm/solvers/base_solver.py @@ -613,7 +613,9 @@ def supports_t_eval_discontinuities(expr): discontinuity_events, ) - def _set_consistent_initialization(self, model, time, inputs_list): + def _set_consistent_initialization( + self, model: pybamm.BaseModel, time: float, inputs_list: list[dict] + ): """ Set initialized states for the model. This is skipped if the solver is an algebraic solver (since this would make the algebraic solver redundant), and if @@ -637,7 +639,9 @@ def _set_consistent_initialization(self, model, time, inputs_list): # Calculate consistent states for the algebraic equations model.y0_list = self.calculate_consistent_state(model, time, inputs_list) - def calculate_consistent_state(self, model, time=0, inputs=None): + def calculate_consistent_state( + self, model: pybamm.BaseModel, time: float = 0, inputs: list[dict] | None = None + ): """ Calculate consistent state for the algebraic equations through root-finding. model.y0_list is used as the initial guess for rootfinding @@ -659,8 +663,6 @@ def calculate_consistent_state(self, model, time=0, inputs=None): model.y0_list. """ pybamm.logger.debug("Start calculating consistent states") - if isinstance(inputs, dict): - inputs = [inputs] inputs = inputs or [{}] if self.root_method is None: @@ -680,7 +682,7 @@ def calculate_consistent_state(self, model, time=0, inputs=None): return y0s def _solve_process_calculate_sensitivities_arg( - inputs, model, calculate_sensitivities + inputs: dict, model: pybamm.BaseModel, calculate_sensitivities: list[str] | bool ): # get a list-only version of calculate_sensitivities if isinstance(calculate_sensitivities, bool): @@ -1242,7 +1244,7 @@ def step( (Note: t_eval is the time measured from the start of the step, so should start at 0 and end at dt). By default, the solution is returned at t0 and t0 + dt. npts : deprecated - inputs : dict, or list of dict, optional + inputs : dict, optional Any input parameters to pass to the model when solving save : bool, optional Save solution with all previous timesteps. Defaults to True. @@ -1250,7 +1252,7 @@ def step( Whether the solver calculates sensitivities of all input parameters. Defaults to False. If only a subset of sensitivities are required, can also pass a list of input parameter names. **Limitations**: sensitivities are not calculated up to numerical tolerances - so are not guarenteed to be within the tolerances set by the solver, please raise an issue if you + so are not guaranteed to be within the tolerances set by the solver, please raise an issue if you require this functionality. Also, when using this feature with `pybamm.Experiment`, the sensitivities do not take into account the movement of step-transitions wrt input parameters, so do not use this feature if the timings of your experimental protocol change rapidly with respect to your input parameters. @@ -1266,27 +1268,19 @@ def step( """ # Set up inputs - if isinstance(inputs, dict): - inputs_list = [inputs] - else: - inputs_list = inputs or [{}] + inputs = inputs or {} if old_solution is None: - old_solutions = [pybamm.EmptySolution()] * len(inputs_list) - elif not isinstance(old_solution, list): - old_solutions = [old_solution] + old_solution = pybamm.EmptySolution() if not ( - isinstance(old_solutions[0], pybamm.EmptySolution) - or old_solutions[0].termination == "final time" - or "[experiment]" in old_solutions[0].termination + isinstance(old_solution, pybamm.EmptySolution) + or old_solution.termination == "final time" + or "[experiment]" in old_solution.termination ): # Return same solution as an event has already been triggered # With hack to allow stepping past experiment current / voltage cut-off - if len(old_solutions) == 1: - return old_solutions[0] - else: - return old_solutions + return old_solution # Make sure model isn't empty self._check_empty_model(model) @@ -1320,7 +1314,7 @@ def step( t_interp = self.process_t_interp(t_interp) - t_start = old_solutions[0].t[-1] + t_start = old_solution.t[-1] t_eval = t_start + t_eval t_interp = t_start + t_interp t_end = t_start + dt @@ -1341,14 +1335,12 @@ def step( timer = pybamm.Timer() # Set up inputs - model_inputs_list: list[dict] = [ - self._set_up_model_inputs(model, inputs) for inputs in inputs_list - ] + model_inputs = self._set_up_model_inputs(model, inputs) # process calculate_sensitivities argument _, sensitivities_have_changed = ( BaseSolver._solve_process_calculate_sensitivities_arg( - model_inputs_list[0], model, calculate_sensitivities + model_inputs, model, calculate_sensitivities ) ) @@ -1361,95 +1353,81 @@ def step( f'"{existing_model.name}". Please create a separate ' "solver for this model" ) - self.set_up(model, model_inputs_list) + self.set_up(model, model_inputs) self._model_set_up.update( {model: {"initial conditions": model.concatenated_initial_conditions}} ) if ( - isinstance(old_solutions[0], pybamm.EmptySolution) - and old_solutions[0].termination is None + isinstance(old_solution, pybamm.EmptySolution) + and old_solution.termination is None ): pybamm.logger.verbose(f"Start stepping {model.name} with {self.name}") using_sensitivities = len(model.calculate_sensitivities) > 0 - if isinstance(old_solutions[0], pybamm.EmptySolution): + if isinstance(old_solution, pybamm.EmptySolution): if not first_step_this_model: # reset y0 to original initial conditions - self.set_up(model, model_inputs_list, ics_only=True) - elif old_solutions[0].all_models[-1] == model: - model.y0_list = [s.last_state.all_ys[0] for s in old_solutions] + self.set_up(model, model_inputs, ics_only=True) + elif old_solution.all_models[-1] == model: + last_state = old_solution.last_state + model.y0_list = [last_state.all_ys[0]] if using_sensitivities: - model.y0S_list = [] - for soln in old_solutions: - full_sens = soln.last_state._all_sensitivities["all"][0] - model.y0S_list.append( - tuple(full_sens[:, i] for i in range(full_sens.shape[1])) - ) + full_sens = last_state._all_sensitivities["all"][0] + model.y0S_list = [ + tuple(full_sens[:, i] for i in range(full_sens.shape[1])) + ] else: - model.y0_list = [] - for soln, inputs in zip(old_solutions, model_inputs_list, strict=True): - _, concatenated_initial_conditions = model.set_initial_conditions_from( - soln, inputs=inputs, return_type="ics" - ) - model.y0_list.append( - concatenated_initial_conditions.evaluate(0, inputs=inputs) - ) + _, concatenated_initial_conditions = model.set_initial_conditions_from( + old_solution, inputs=model_inputs, return_type="ics" + ) + model.y0_list = [ + concatenated_initial_conditions.evaluate(0, inputs=model_inputs) + ] if using_sensitivities: model.y0S_list = [ - self._set_sens_initial_conditions_from(soln, model) - for soln in old_solutions + self._set_sens_initial_conditions_from(old_solution, model) ] set_up_time = timer.time() # (Re-)calculate consistent initialization - self._set_consistent_initialization(model, t_start_shifted, model_inputs_list) + self._set_consistent_initialization(model, t_start_shifted, [model_inputs]) # Check consistent initialization doesn't violate events - for y0, inpts in zip(model.y0_list, model_inputs_list, strict=True): - self._check_events_with_initialization(t_eval, model, y0, inpts) + self._check_events_with_initialization(t_eval, model, model.y0, model_inputs) # Step pybamm.logger.verbose(f"Stepping for {t_start_shifted:.0f} < t < {t_end:.0f}") timer.reset() - solutions = self._integrate(model, t_eval, model_inputs_list, t_interp) - for i, s in enumerate(solutions): - solutions[i].solve_time = timer.time() - - # Check if extrapolation occurred - self.check_extrapolation(s, model.events) + solution = self._integrate(model, t_eval, [model_inputs], t_interp)[0] + solution.solve_time = timer.time() - # Identify the event that caused termination and update the solution to - # include the event time and state - solutions[i], termination = self.get_termination_reason(s, model.events) + # Check if extrapolation occurred + self.check_extrapolation(solution, model.events) + # Identify the event that caused termination and update the solution to + # include the event time and state + solution, termination = self.get_termination_reason(solution, model.events) - # Assign setup time - solutions[i].set_up_time = set_up_time + # Assign setup time + solution.set_up_time = set_up_time # Report times pybamm.logger.verbose(f"Finish stepping {model.name} ({termination})") pybamm.logger.verbose( - f"Set-up time: {solutions[0].set_up_time}, Step time: {solutions[0].solve_time} (of which integration time: {solutions[0].integration_time}), " - f"Total time: {solutions[0].total_time}" + f"Set-up time: {solution.set_up_time}, Step time: {solution.solve_time} (of which integration time: {solution.integration_time}), " + f"Total time: {solution.total_time}" ) # Return solution if save is False: - ret = solutions + return solution else: - ret = [ - old_s + s for (old_s, s) in zip(old_solutions, solutions, strict=True) - ] - - if len(ret) == 1: - return ret[0] - else: - return ret + return old_solution + solution @staticmethod def get_termination_reason(solution, events): @@ -1620,12 +1598,9 @@ def get_platform_context(self, system_type: str): return "spawn" @staticmethod - def _set_up_model_inputs(model, inputs): + def _set_up_model_inputs(model: pybamm.BaseModel, inputs: dict): """Set up input parameters""" - if inputs is None: - inputs = {} - else: - inputs = ParameterValues.check_parameter_values(inputs) + inputs = ParameterValues.check_parameter_values(inputs) # Go through all input parameters that can be found in the model # Only keep the ones that are actually used in the model From ef3006f355d2fb494dd2f07e79eee42571f52e66 Mon Sep 17 00:00:00 2001 From: Pip Liggins Date: Wed, 26 Nov 2025 13:17:14 +0000 Subject: [PATCH 34/42] coverage changes --- src/pybamm/expression_tree/binary_operators.py | 2 +- src/pybamm/solvers/idaklu_solver.py | 2 -- tests/unit/test_expression_tree/test_binary_operators.py | 7 +++++++ tests/unit/test_models/test_base_model.py | 4 ++++ 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/pybamm/expression_tree/binary_operators.py b/src/pybamm/expression_tree/binary_operators.py index 8202b9e615..237917e300 100644 --- a/src/pybamm/expression_tree/binary_operators.py +++ b/src/pybamm/expression_tree/binary_operators.py @@ -687,7 +687,7 @@ def _t_discon(self, expr, y0, inputs, num_events): def _t_discon_next(self, value: float): raise NotImplementedError( "_t_discon_next method should be implemented in subclasses of _Heaviside" - ) + ) # pragma: no cover class EqualHeaviside(_Heaviside): diff --git a/src/pybamm/solvers/idaklu_solver.py b/src/pybamm/solvers/idaklu_solver.py index 77a1c26609..3075f9a18c 100644 --- a/src/pybamm/solvers/idaklu_solver.py +++ b/src/pybamm/solvers/idaklu_solver.py @@ -790,8 +790,6 @@ def _set_consistent_initialization(self, model, time, inputs_list): casadi_format = model.convert_to_format == "casadi" def handle_y0(y0): - if y0 is None: - return y0 if isinstance(y0, casadi.DM): y0 = y0.full() return y0.flatten() diff --git a/tests/unit/test_expression_tree/test_binary_operators.py b/tests/unit/test_expression_tree/test_binary_operators.py index 04939309ea..ea7a488d2c 100644 --- a/tests/unit/test_expression_tree/test_binary_operators.py +++ b/tests/unit/test_expression_tree/test_binary_operators.py @@ -876,3 +876,10 @@ def test_to_json(self, mocker): not_equal_json["children"] = [pybamm.Scalar(2), pybamm.Scalar(4)] assert pybamm.NotEqualHeaviside._from_json(not_equal_json) == ne_h + + def test_t_discon_error(self): + a = pybamm.Symbol("a") + b = pybamm.Symbol("b") + bin = pybamm.BinaryOperator("binary test", a, b) + with pytest.raises(NotImplementedError): + bin._t_discon(None, None, None, None) diff --git a/tests/unit/test_models/test_base_model.py b/tests/unit/test_models/test_base_model.py index 5c73f86838..7bb64216fa 100644 --- a/tests/unit/test_models/test_base_model.py +++ b/tests/unit/test_models/test_base_model.py @@ -1638,3 +1638,7 @@ def test_save_load_model(self): new_model = pybamm.load_model("test_base_model.json") os.remove("test_base_model.json") + + def test_y0_property(self): + model = pybamm.BaseModel() + assert model.y0 is None From 3204e1e504ea34cae60fa974a5713d51b9df433d Mon Sep 17 00:00:00 2001 From: Pip Liggins Date: Wed, 26 Nov 2025 13:49:05 +0000 Subject: [PATCH 35/42] Mark jax gpu/unknown platform as no coverage --- src/pybamm/solvers/base_solver.py | 4 ---- src/pybamm/solvers/idaklu_solver.py | 4 ---- src/pybamm/solvers/jax_solver.py | 12 ++---------- 3 files changed, 2 insertions(+), 18 deletions(-) diff --git a/src/pybamm/solvers/base_solver.py b/src/pybamm/solvers/base_solver.py index e863f236eb..f8c4898312 100644 --- a/src/pybamm/solvers/base_solver.py +++ b/src/pybamm/solvers/base_solver.py @@ -95,10 +95,6 @@ def supports_interp(self): def supports_t_eval_discontinuities(self): return self._supports_t_eval_discontinuities - @property - def supports_parallel_solve(self): - return False - @property def on_extrapolation(self): return self._on_extrapolation diff --git a/src/pybamm/solvers/idaklu_solver.py b/src/pybamm/solvers/idaklu_solver.py index 3075f9a18c..e4e910d297 100644 --- a/src/pybamm/solvers/idaklu_solver.py +++ b/src/pybamm/solvers/idaklu_solver.py @@ -571,10 +571,6 @@ def __setstate__(self, d): options=self._options, ) - @property - def supports_parallel_solve(self): - return True - @property def options(self): return self._options diff --git a/src/pybamm/solvers/jax_solver.py b/src/pybamm/solvers/jax_solver.py index a68ad5799f..3f182111c0 100644 --- a/src/pybamm/solvers/jax_solver.py +++ b/src/pybamm/solvers/jax_solver.py @@ -196,14 +196,6 @@ def solve_model_bdf(inputs, y0): else: return jax.jit(solve_model_bdf) - @property - def supports_parallel_solve(self): - return True - - @property - def requires_explicit_sensitivities(self): - return False - def _integrate( self, model, @@ -257,7 +249,7 @@ async def solve_model_async(inputs_v, y0): platform.startswith("gpu") or platform.startswith("tpu") or platform.startswith("metal") - ): + ): # pragma: no cover # gpu execution runs faster when parallelised with vmap # (see also comment below regarding single-program multiple-data # execution (SPMD) using pmap on multiple XLAs) @@ -267,7 +259,7 @@ async def solve_model_async(inputs_v, y0): key: jnp.array([dic[key] for dic in inputs]) for key in inputs[0] } y.extend(jax.vmap(self._cached_solves[model])(inputs_v, model.y0_list)) - else: + else: # pragma: no cover # Unknown platform, use serial execution as fallback print( f'Unknown platform requested: "{platform}", ' From bb62015dcf956523704ef502725e8dc38af3cd3b Mon Sep 17 00:00:00 2001 From: Pip Liggins Date: Wed, 26 Nov 2025 15:31:14 +0000 Subject: [PATCH 36/42] Fix idaklu-jax closure bug? --- src/pybamm/solvers/idaklu_jax.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pybamm/solvers/idaklu_jax.py b/src/pybamm/solvers/idaklu_jax.py index 40abe1c42a..5589705f53 100644 --- a/src/pybamm/solvers/idaklu_jax.py +++ b/src/pybamm/solvers/idaklu_jax.py @@ -176,7 +176,7 @@ def slice_out(out): # Otherwise, return a function that slices the output def f_isolated(*args, **kwargs): - return slice_out(self.jaxify_f(*args, **kwargs)) + return slice_out(f(*args, **kwargs)) return f_isolated @@ -263,7 +263,7 @@ def slice_out(out): # Otherwise, return a function that slices the output def f_isolated(*args, **kwargs): - return slice_out(self.jaxify_f(*args, **kwargs)) + return slice_out(f(*args, **kwargs)) return f_isolated From 789e717f231fe12835f71f17c822c9261cf24a43 Mon Sep 17 00:00:00 2001 From: Pip Liggins Date: Wed, 26 Nov 2025 15:55:52 +0000 Subject: [PATCH 37/42] try again --- src/pybamm/solvers/idaklu_jax.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/pybamm/solvers/idaklu_jax.py b/src/pybamm/solvers/idaklu_jax.py index 5589705f53..65b7ae947b 100644 --- a/src/pybamm/solvers/idaklu_jax.py +++ b/src/pybamm/solvers/idaklu_jax.py @@ -160,9 +160,11 @@ def get_var( else: raise ValueError("Invalid call signature") + # Capture the index value eagerly to avoid self reference issues + index = self.jax_output_variables.index(varname) + # Utility function to slice the output def slice_out(out): - index = self.jax_output_variables.index(varname) if out.ndim == 0: return out # pragma: no cover elif out.ndim == 1: @@ -245,11 +247,13 @@ def get_vars( else: raise ValueError("Invalid call signature") + # Capture the index array eagerly to avoid self reference issues + index = np.array( + [self.jax_output_variables.index(varname) for varname in varnames] + ) + # Utility function to slice the output def slice_out(out): - index = np.array( - [self.jax_output_variables.index(varname) for varname in varnames] - ) if out.ndim == 0: return out # pragma: no cover elif out.ndim == 1: From 7e6dbbef54fdbb72c63b744d68eaee13c5d73b2d Mon Sep 17 00:00:00 2001 From: Pip Liggins Date: Wed, 26 Nov 2025 16:47:26 +0000 Subject: [PATCH 38/42] Add test for different discontinuity event times error --- tests/unit/test_solvers/test_base_solver.py | 25 +++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/unit/test_solvers/test_base_solver.py b/tests/unit/test_solvers/test_base_solver.py index 276cd8da90..5d8ac3ae0d 100644 --- a/tests/unit/test_solvers/test_base_solver.py +++ b/tests/unit/test_solvers/test_base_solver.py @@ -490,3 +490,28 @@ def test_integrate_single_error(self): match="BaseSolver does not implement _integrate_single.", ): solver._integrate_single(model, np.array([0, 1]), {}, np.array([1])) + + def test_discontinuity_events_different_times_error(self): + # Test that an error is raised when discontinuity events occur at different + # times for different input parameter sets + model = pybamm.BaseModel() + v = pybamm.Variable("v") + t_event = pybamm.InputParameter("t_event") + model.rhs = {v: pybamm.t > t_event} + model.initial_conditions = {v: 0} + model.variables = {"v": v} + + disc = pybamm.Discretisation() + disc.process_model(model) + + solver = pybamm.BaseSolver() + t_eval = np.linspace(0, 10) + + # Different input sets with discontinuities at different times + inputs_list = [{"t_event": 3.0}, {"t_event": 5.0}] + + with pytest.raises( + pybamm.SolverError, + match="Discontinuity events occur at different times between input parameter sets", + ): + solver.solve(model, t_eval, inputs=inputs_list) From 19c8b503578b9386fcacc5fdccd8a32c2992483d Mon Sep 17 00:00:00 2001 From: Pip Liggins Date: Wed, 26 Nov 2025 16:57:26 +0000 Subject: [PATCH 39/42] Test for model.y0 error --- tests/unit/test_solvers/test_scipy_solver.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/unit/test_solvers/test_scipy_solver.py b/tests/unit/test_solvers/test_scipy_solver.py index 2d1bce153d..a5219ccfaf 100644 --- a/tests/unit/test_solvers/test_scipy_solver.py +++ b/tests/unit/test_solvers/test_scipy_solver.py @@ -322,6 +322,10 @@ def test_model_solver_multiple_inputs_initial_conditions(self): solutions = solver.solve(model, t_eval, inputs=inputs_list, nproc=2) + with pytest.raises(ValueError, match="Model contains multiple initial states"): + # try to access y0 property where there's more than 1 + _ = model.y0 + # Extract y(0) actually used per run ic_used = [float(sol["var"].entries[0][0]) for sol in solutions] assert ic_used == [0.02, 0.04, 0.06, 0.08, 0.1, 0.12, 0.14, 0.16] From df19f3c6d277d3aa299ab89f6c6c8297c7431b31 Mon Sep 17 00:00:00 2001 From: Pip Liggins Date: Thu, 27 Nov 2025 11:44:28 +0000 Subject: [PATCH 40/42] Revert jax changes --- src/pybamm/solvers/idaklu_jax.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/pybamm/solvers/idaklu_jax.py b/src/pybamm/solvers/idaklu_jax.py index 65b7ae947b..40abe1c42a 100644 --- a/src/pybamm/solvers/idaklu_jax.py +++ b/src/pybamm/solvers/idaklu_jax.py @@ -160,11 +160,9 @@ def get_var( else: raise ValueError("Invalid call signature") - # Capture the index value eagerly to avoid self reference issues - index = self.jax_output_variables.index(varname) - # Utility function to slice the output def slice_out(out): + index = self.jax_output_variables.index(varname) if out.ndim == 0: return out # pragma: no cover elif out.ndim == 1: @@ -178,7 +176,7 @@ def slice_out(out): # Otherwise, return a function that slices the output def f_isolated(*args, **kwargs): - return slice_out(f(*args, **kwargs)) + return slice_out(self.jaxify_f(*args, **kwargs)) return f_isolated @@ -247,13 +245,11 @@ def get_vars( else: raise ValueError("Invalid call signature") - # Capture the index array eagerly to avoid self reference issues - index = np.array( - [self.jax_output_variables.index(varname) for varname in varnames] - ) - # Utility function to slice the output def slice_out(out): + index = np.array( + [self.jax_output_variables.index(varname) for varname in varnames] + ) if out.ndim == 0: return out # pragma: no cover elif out.ndim == 1: @@ -267,7 +263,7 @@ def slice_out(out): # Otherwise, return a function that slices the output def f_isolated(*args, **kwargs): - return slice_out(f(*args, **kwargs)) + return slice_out(self.jaxify_f(*args, **kwargs)) return f_isolated From 593d2aa026f35efe9afd34f522ee36ab5e66a0bc Mon Sep 17 00:00:00 2001 From: Brady Planden <55357039+BradyPlanden@users.noreply.github.com> Date: Thu, 27 Nov 2025 14:17:53 +0000 Subject: [PATCH 41/42] Update C-Rate current for changing nominal capacity (#5286) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- src/pybamm/simulation.py | 44 +++++++ .../test_simulation_with_experiment.py | 123 ++++++++++++++++++ 2 files changed, 167 insertions(+) diff --git a/src/pybamm/simulation.py b/src/pybamm/simulation.py index 8f3fcaaad1..8faff7a2e8 100644 --- a/src/pybamm/simulation.py +++ b/src/pybamm/simulation.py @@ -128,6 +128,7 @@ def __init__( self._model_with_set_params = None self._built_model = None self._built_initial_soc = None + self._built_nominal_capacity = None self.steps_to_built_models = None self.steps_to_built_solvers = None self._mesh = None @@ -163,6 +164,42 @@ def set_up_and_parameterise_experiment(self, solve_kwargs=None): warnings.warn(msg, DeprecationWarning, stacklevel=2) self._set_up_and_parameterise_experiment(solve_kwargs=solve_kwargs) + def _update_experiment_models_for_capacity(self, solve_kwargs=None): + """ + Check if the nominal capacity has changed and update the experiment models + if needed. This re-processes the models without rebuilding the mesh and + discretisation. + """ + current_capacity = self._parameter_values.get( + "Nominal cell capacity [A.h]", None + ) + + if self._built_nominal_capacity == current_capacity: + return + + # Capacity has changed, need to re-process the models + pybamm.logger.info( + f"Nominal capacity changed from {self._built_nominal_capacity} to " + f"{current_capacity}. Re-processing experiment models." + ) + + # Re-parameterise the experiment with the new capacity + self._set_up_and_parameterise_experiment(solve_kwargs) + + # Re-discretise the models + self.steps_to_built_models = {} + self.steps_to_built_solvers = {} + for ( + step, + model_with_set_params, + ) in self.experiment_unique_steps_to_model.items(): + built_model = self._disc.process_model(model_with_set_params, inplace=True) + solver = self._solver.copy() + self.steps_to_built_solvers[step] = solver + self.steps_to_built_models[step] = built_model + + self._built_nominal_capacity = current_capacity + def _set_up_and_parameterise_experiment(self, solve_kwargs=None): """ Create and parameterise the models for each step in the experiment. @@ -266,6 +303,7 @@ def set_initial_state(self, initial_soc, direction=None, inputs=None): # reset self._model_with_set_params = None self._built_model = None + self._built_nominal_capacity = None self.steps_to_built_models = None self.steps_to_built_solvers = None @@ -338,6 +376,8 @@ def build_for_experiment( self.set_initial_state(initial_soc, direction=direction, inputs=inputs) if self.steps_to_built_models: + # Check if we need to update the models due to capacity change + self._update_experiment_models_for_capacity(solve_kwargs) return else: self._set_up_and_parameterise_experiment(solve_kwargs) @@ -366,6 +406,10 @@ def build_for_experiment( self.steps_to_built_solvers[step] = solver self.steps_to_built_models[step] = built_model + self._built_nominal_capacity = self._parameter_values.get( + "Nominal cell capacity [A.h]", None + ) + def solve( self, t_eval=None, diff --git a/tests/unit/test_experiments/test_simulation_with_experiment.py b/tests/unit/test_experiments/test_simulation_with_experiment.py index 2b62b83db3..3c5099dde7 100644 --- a/tests/unit/test_experiments/test_simulation_with_experiment.py +++ b/tests/unit/test_experiments/test_simulation_with_experiment.py @@ -1,3 +1,4 @@ +import logging import os from datetime import datetime @@ -1022,3 +1023,125 @@ def neg_stoich_cutoff(variables): neg_stoich = sol["Negative electrode stoichiometry"].data assert neg_stoich[-1] == pytest.approx(0.5, abs=0.0001) + + def test_simulation_changing_capacity_crate_steps(self): + """Test that C-rate steps are correctly updated when capacity changes""" + model = pybamm.lithium_ion.SPM() + experiment = pybamm.Experiment( + [ + ( + "Discharge at C/5 for 20 minutes", + "Discharge at C/2 for 20 minutes", + "Discharge at 1C for 20 minutes", + ) + ] + ) + param = pybamm.ParameterValues("Chen2020") + sim = pybamm.Simulation(model, experiment=experiment, parameter_values=param) + + # First solve + sol1 = sim.solve(calc_esoh=False) + original_capacity = param["Nominal cell capacity [A.h]"] + + # Check that C-rates correspond to expected currents + I_C5_1 = np.abs(sol1.cycles[0].steps[0]["Current [A]"].data).mean() + I_C2_1 = np.abs(sol1.cycles[0].steps[1]["Current [A]"].data).mean() + I_1C_1 = np.abs(sol1.cycles[0].steps[2]["Current [A]"].data).mean() + + np.testing.assert_allclose(I_C5_1, original_capacity / 5, rtol=1e-2) + np.testing.assert_allclose(I_C2_1, original_capacity / 2, rtol=1e-2) + np.testing.assert_allclose(I_1C_1, original_capacity, rtol=1e-2) + + # Update capacity + new_capacity = 0.9 * original_capacity + sim._parameter_values.update({"Nominal cell capacity [A.h]": new_capacity}) + + # Second solve with updated capacity + sol2 = sim.solve(calc_esoh=False) + + # Check that C-rates now correspond to updated currents + I_C5_2 = np.abs(sol2.cycles[0].steps[0]["Current [A]"].data).mean() + I_C2_2 = np.abs(sol2.cycles[0].steps[1]["Current [A]"].data).mean() + I_1C_2 = np.abs(sol2.cycles[0].steps[2]["Current [A]"].data).mean() + + np.testing.assert_allclose(I_C5_2, new_capacity / 5, rtol=1e-2) + np.testing.assert_allclose(I_C2_2, new_capacity / 2, rtol=1e-2) + np.testing.assert_allclose(I_1C_2, new_capacity, rtol=1e-2) + + # Verify all currents scaled proportionally + np.testing.assert_allclose(I_C5_2 / I_C5_1, 0.9, rtol=1e-2) + np.testing.assert_allclose(I_C2_2 / I_C2_1, 0.9, rtol=1e-2) + np.testing.assert_allclose(I_1C_2 / I_1C_1, 0.9, rtol=1e-2) + + def test_simulation_multiple_cycles_with_capacity_change(self): + """Test capacity changes across multiple experiment cycles""" + model = pybamm.lithium_ion.SPM() + experiment = pybamm.Experiment( + [("Discharge at 1C for 5 minutes", "Charge at 1C for 5 minutes")] * 2 + ) + param = pybamm.ParameterValues("Chen2020") + sim = pybamm.Simulation(model, experiment=experiment, parameter_values=param) + + # First solve + sol1 = sim.solve(calc_esoh=False) + original_capacity = param["Nominal cell capacity [A.h]"] + + # Get discharge currents for both cycles + I_discharge_cycle1 = np.abs(sol1.cycles[0].steps[0]["Current [A]"].data).mean() + I_discharge_cycle2 = np.abs(sol1.cycles[1].steps[0]["Current [A]"].data).mean() + + # Both cycles should use the same capacity initially + np.testing.assert_allclose(I_discharge_cycle1, original_capacity, rtol=1e-2) + np.testing.assert_allclose(I_discharge_cycle2, original_capacity, rtol=1e-2) + + # Update capacity between cycles + new_capacity = 0.85 * original_capacity + sim._parameter_values.update({"Nominal cell capacity [A.h]": new_capacity}) + + # Solve again + sol2 = sim.solve(calc_esoh=False) + + # All cycles in the new solution should use updated capacity + I_discharge_cycle1_new = np.abs( + sol2.cycles[0].steps[0]["Current [A]"].data + ).mean() + I_discharge_cycle2_new = np.abs( + sol2.cycles[1].steps[0]["Current [A]"].data + ).mean() + + np.testing.assert_allclose(I_discharge_cycle1_new, new_capacity, rtol=1e-2) + np.testing.assert_allclose(I_discharge_cycle2_new, new_capacity, rtol=1e-2) + + def test_simulation_logging_with_capacity_change(self, caplog): + """Test that capacity changes are logged appropriately""" + model = pybamm.lithium_ion.SPM() + experiment = pybamm.Experiment([("Discharge at 1C for 10 minutes",)]) + param = pybamm.ParameterValues("Chen2020") + sim = pybamm.Simulation(model, experiment=experiment, parameter_values=param) + + # First solve + sim.solve(calc_esoh=False) + original_capacity = param["Nominal cell capacity [A.h]"] + + # Update capacity + new_capacity = 0.75 * original_capacity + sim._parameter_values.update({"Nominal cell capacity [A.h]": new_capacity}) + + # Set logging level to capture INFO messages + original_log_level = pybamm.logger.level + pybamm.set_logging_level("INFO") + + try: + # Second solve should log capacity change + with caplog.at_level(logging.INFO, logger="pybamm.logger"): + sim.solve(calc_esoh=False) + + # Check that a log message about capacity change was recorded + log_messages = [record.message for record in caplog.records] + capacity_change_logged = any( + "Nominal capacity changed" in msg for msg in log_messages + ) + assert capacity_change_logged + finally: + # Restore original logging level + pybamm.logger.setLevel(original_log_level) From 97b03ff998cd99c3164d091012817e177f21dfd6 Mon Sep 17 00:00:00 2001 From: Pip Liggins Date: Mon, 1 Dec 2025 12:16:29 +0000 Subject: [PATCH 42/42] fix jax test? --- tests/unit/test_solvers/test_idaklu_jax.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_solvers/test_idaklu_jax.py b/tests/unit/test_solvers/test_idaklu_jax.py index ab468e9aed..cdf8feead1 100644 --- a/tests/unit/test_solvers/test_idaklu_jax.py +++ b/tests/unit/test_solvers/test_idaklu_jax.py @@ -95,7 +95,7 @@ def no_jit(f): calculate_sensitivities=True, t_interp=t_eval, ) - f4 = jax_multi.get_jaxpr() + f4 = jax_multi2.get_jaxpr() return [ # single output