def test_timer_format(self): t = pybamm.Timer() self.assertEqual(t.format(1e-9), "1.000 ns") self.assertEqual(t.format(0.000000123456789), "123.457 ns") self.assertEqual(t.format(1e-6), "1.000 us") self.assertEqual(t.format(0.000123456789), "123.457 us") self.assertEqual(t.format(0.999e-3), "999.000 us") self.assertEqual(t.format(1e-3), "1.000 ms") self.assertEqual(t.format(0.123456789), "123.457 ms") self.assertEqual(t.format(2), "2.000 s") self.assertEqual(t.format(2.5), "2.500 s") self.assertEqual(t.format(12.5), "12.500 s") self.assertEqual(t.format(59.41), "59.410 s") self.assertEqual(t.format(59.4126347547), "59.413 s") self.assertEqual(t.format(60.2), "1 minute, 0 seconds") self.assertEqual(t.format(61), "1 minute, 1 second") self.assertEqual(t.format(121), "2 minutes, 1 second") self.assertEqual( t.format(604800), "1 week, 0 days, 0 hours, 0 minutes, 0 seconds" ) self.assertEqual( t.format(2 * 604800 + 3 * 3600 + 60 + 4), "2 weeks, 0 days, 3 hours, 1 minute, 4 seconds", ) # Test without argument self.assertIsInstance(t.format(), str)
def compute_solution(self, model, t_eval): """Calculate the solution of the model at specified times. In this class, we overwrite the behaviour of :class:`pybamm.DaeSolver`, since CasADi requires slightly different syntax. Parameters ---------- model : :class:`pybamm.BaseModel` The model whose solution to calculate. Must have attributes rhs and initial_conditions t_eval : numeric type The times at which to compute the solution """ timer = pybamm.Timer() solve_start_time = timer.time() pybamm.logger.info("Calling DAE solver") solution = self.integrate_casadi( self.casadi_problem, self.y0, t_eval, mass_matrix=model.mass_matrix.entries ) solve_time = timer.time() - solve_start_time # Events not implemented, termination is always 'final time' termination = "final time" return solution, solve_time, termination
def compute_solution(self, model, t_eval): """Calculate the solution of the model at specified times. Parameters ---------- model : :class:`pybamm.BaseModel` The model whose solution to calculate. Must have attributes rhs and initial_conditions t_eval : numeric type The times at which to compute the solution """ timer = pybamm.Timer() solve_start_time = timer.time() pybamm.logger.info("Calling DAE solver") solution = self.integrate( self.residuals, self.y0, t_eval, events=self.event_funs, mass_matrix=model.mass_matrix.entries, jacobian=self.jacobian, ) solve_time = timer.time() - solve_start_time # Identify the event that caused termination termination = self.get_termination_reason(solution, self.events) return solution, solve_time, termination
def test_timer_format(self): import sys t = pybamm.Timer() self.assertEqual(t.format(1e-3), "0.001 seconds") self.assertEqual(t.format(0.000123456789), "0.000123456789 seconds") self.assertEqual(t.format(0.123456789), "0.12 seconds") if sys.hexversion < 0x3000000: self.assertEqual(t.format(2), "2.0 seconds") else: self.assertEqual(t.format(2), "2 seconds") self.assertEqual(t.format(2.5), "2.5 seconds") self.assertEqual(t.format(12.5), "12.5 seconds") self.assertEqual(t.format(59.41), "59.41 seconds") self.assertEqual(t.format(59.4126347547), "59.41 seconds") self.assertEqual(t.format(60.2), "1 minute, 0 seconds") self.assertEqual(t.format(61), "1 minute, 1 second") self.assertEqual(t.format(121), "2 minutes, 1 second") self.assertEqual(t.format(604800), "1 week, 0 days, 0 hours, 0 minutes, 0 seconds") self.assertEqual( t.format(2 * 604800 + 3 * 3600 + 60 + 4), "2 weeks, 0 days, 3 hours, 1 minute, 4 seconds", ) # Test without argument self.assertIsInstance(t.format(), str)
def solve(self, model): """Calculate the solution of the model. Parameters ---------- model : :class:`pybamm.BaseModel` The model whose solution to calculate. Must only contain algebraic equations. """ pybamm.logger.info("Start solving {} with {}".format( model.name, self.name)) # Set up timer = pybamm.Timer() start_time = timer.time() concatenated_algebraic, jac = self.set_up(model) set_up_time = timer.time() - start_time # Create function to evaluate algebraic def algebraic(y): return concatenated_algebraic.evaluate(0, y, known_evals={})[0][:, 0] # Create function to evaluate jacobian if jac is not None: def jacobian(y): # Note: we only use this solver for time independent algebraic # systems, so jac is arbitrarily evaluated at t=0. Also, needs # to be converted from sparse to dense, so in very large # algebraic models it may be best to switch use_jacobian to False # by default. return jac.evaluate(0, y, known_evals={})[0].toarray() else: jacobian = None # Use "initial conditions" set in model as initial guess y0_guess = model.concatenated_initial_conditions.evaluate(t=0) # Solve solve_start_time = timer.time() pybamm.logger.info("Calling root finding algorithm") solution = self.root(algebraic, y0_guess, jacobian=jacobian) solution.model = model # Assign times solution.solve_time = timer.time() - solve_start_time solution.set_up_time = set_up_time pybamm.logger.info("Finish solving {}".format(model.name)) pybamm.logger.info( "Set-up time: {}, Solve time: {}, Total time: {}".format( timer.format(solution.set_up_time), timer.format(solution.solve_time), timer.format(solution.total_time), )) return solution
def test_append(self): model = pybamm.lithium_ion.SPMe() # create geometry geometry = model.default_geometry # load parameter values and process model and geometry param = model.default_parameter_values param.process_model(model) param.process_geometry(geometry) # set mesh mesh = pybamm.Mesh(geometry, model.default_submesh_types, model.default_var_pts) # discretise model disc = pybamm.Discretisation(mesh, model.default_spatial_methods) disc.process_model(model) # solve model t_eval = np.linspace(0, 3600, 100) solver = model.default_solver solution = solver.solve(model, t_eval) # step model old_t = 0 step_solver = model.default_solver step_solution = None # dt should be dimensional solution_times_dimensional = solution.t * model.timescale_eval for t in solution_times_dimensional[1:]: dt = t - old_t step_solution = step_solver.step(step_solution, model, dt=dt, npts=10) if t == solution_times_dimensional[1]: # Create voltage variable step_solution.update("Terminal voltage") old_t = t # Step solution should have been updated as we go along so be quicker to # calculate timer = pybamm.Timer() step_solution.update("Terminal voltage") step_sol_time = timer.time() timer.reset() solution.update("Terminal voltage") sol_time = timer.time() self.assertLess(step_sol_time, sol_time) # Check both give the same answer np.testing.assert_array_almost_equal( solution["Terminal voltage"](solution.t[:-1] * model.timescale_eval), step_solution["Terminal voltage"](solution.t[:-1] * model.timescale_eval), decimal=4, )
def test_notebook(path, executable="python"): """ Tests a single notebook, exists if it doesn't finish. """ import nbconvert import pybamm b = pybamm.Timer() print("Test " + path + " ... ", end="") sys.stdout.flush() # Load notebook, convert to python e = nbconvert.exporters.PythonExporter() code, __ = e.from_filename(path) # Remove coding statement, if present code = "\n".join([x for x in code.splitlines() if x[:9] != "# coding"]) # Tell matplotlib not to produce any figures env = dict(os.environ) env["MPLBACKEND"] = "Template" # If notebook makes use of magic commands then # the script must be ran using ipython # https://github.com/jupyter/nbconvert/issues/503#issuecomment-269527834 executable = "ipython" if ("run_cell_magic(" in code) else executable # Run in subprocess cmd = [executable] + ["-c", code] try: p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env) stdout, stderr = p.communicate() # TODO: Use p.communicate(timeout=3600) if Python3 only if p.returncode != 0: # Show failing code, output and errors before returning print("ERROR") print("-- script " + "-" * (79 - 10)) for i, line in enumerate(code.splitlines()): j = str(1 + i) print(j + " " * (5 - len(j)) + line) print("-- stdout " + "-" * (79 - 10)) print(str(stdout, "utf-8")) print("-- stderr " + "-" * (79 - 10)) print(str(stderr, "utf-8")) print("-" * 79) return False except KeyboardInterrupt: p.terminate() print("ABORTED") sys.exit(1) # Sucessfully run print("ok (" + b.format() + ")") return True
def test_timing(self): t = pybamm.Timer() a = t.time().value self.assertGreaterEqual(a, 0) for _ in range(100): self.assertGreater(t.time().value, a) a = t.time().value t.reset() b = t.time().value self.assertGreaterEqual(b, 0) self.assertLess(b, a)
def pybamm2(): # Writing lead-acid model example full = pybamm.lead_acid.Full() loqs = pybamm.lead_acid.LOQS() composite = pybamm.lead_acid.Composite() # load models models = [loqs, composite, full] # process parameters param = models[0].default_parameter_values param["Current function [A]"] = "[input]" for model in models: param.process_model(model) for model in models: # load and process default geometry geometry = model.default_geometry param.process_geometry(geometry) # discretise using default settings mesh = pybamm.Mesh(geometry, model.default_submesh_types, model.default_var_pts) disc = pybamm.Discretisation(mesh, model.default_spatial_methods) disc.process_model(model) timer = pybamm.Timer() solutions = {} t_eval = np.linspace(0, 3600 * 17, 100) # time in seconds solver = pybamm.CasadiSolver() for model in models: timer.reset() solution = solver.solve(model, t_eval, inputs={"Current function [A]": 1}) print("Solved the {} in {}".format(model.name, timer.time())) solutions[model] = solution for model in models: time = solutions[model]["Time [h]"].entries voltage = solutions[model]["Terminal voltage [V]"].entries plt.plot(time, voltage, lw=2, label=model.name) plt.xlabel("Time [h]", fontsize=15) plt.ylabel("Terminal voltage [V]", fontsize=15) plt.legend(fontsize=15) # plt.show() plt.savefig('foo1.png')
def solve(self, model, t_eval): """ Execute the solver setup and calculate the solution of the model at specified times. Parameters ---------- model : :class:`pybamm.BaseModel` The model whose solution to calculate. Must have attributes rhs and initial_conditions t_eval : numeric type The times at which to compute the solution Raises ------ :class:`pybamm.ModelError` If an empty model is passed (`model.rhs = {}` and `model.algebraic={}`) """ pybamm.logger.info("Start solving {}".format(model.name)) # Make sure model isn't empty if len(model.rhs) == 0 and len(model.algebraic) == 0: raise pybamm.ModelError("Cannot solve empty model") # Set up timer = pybamm.Timer() start_time = timer.time() if model.convert_to_format == "casadi" or isinstance(self, pybamm.CasadiSolver): self.set_up_casadi(model) else: self.set_up(model) set_up_time = timer.time() - start_time # Solve solution, solve_time, termination = self.compute_solution(model, t_eval) # Assign times solution.solve_time = solve_time solution.total_time = timer.time() - start_time solution.set_up_time = set_up_time pybamm.logger.info("Finish solving {} ({})".format(model.name, termination)) pybamm.logger.info( "Set-up time: {}, Solve time: {}, Total time: {}".format( timer.format(solution.set_up_time), timer.format(solution.solve_time), timer.format(solution.total_time), ) ) return solution
def test_script(path, executable="python"): """ Tests a single notebook, exists if it doesn't finish. """ import pybamm b = pybamm.Timer() print("Test " + path + " ... ", end="") sys.stdout.flush() # Tell matplotlib not to produce any figures env = dict(os.environ) env["MPLBACKEND"] = "Template" # Run in subprocess cmd = [executable] + [path] try: p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env) stdout, stderr = p.communicate() # TODO: Use p.communicate(timeout=3600) if Python3 only if p.returncode != 0: # Show failing code, output and errors before returning print("ERROR") print("-- stdout " + "-" * (79 - 10)) print(str(stdout, "utf-8")) print("-- stderr " + "-" * (79 - 10)) print(str(stderr, "utf-8")) print("-" * 79) return False except KeyboardInterrupt: p.terminate() print("ABORTED") sys.exit(1) # Sucessfully run print("ok (" + b.format() + ")") return True
def _integrate(self, model, t_eval, inputs_dict=None): """ Solve a model defined by dydt with initial conditions y0. Parameters ---------- model : :class:`pybamm.BaseModel` The model whose solution to calculate. t_eval : :class:`numpy.array`, size (k,) The times at which to compute the solution inputs_dict : dict, optional Any input parameters to pass to the model when solving Returns ------- object An object containing the times and values of the solution, as well as various diagnostic messages. """ timer = pybamm.Timer() if model not in self._cached_solves: self._cached_solves[model] = self.create_solve(model, t_eval) y = self._cached_solves[model](inputs_dict).block_until_ready() integration_time = timer.time() # convert to a normal numpy array y = onp.array(y) termination = "final time" t_event = None y_event = onp.array(None) sol = pybamm.Solution(t_eval, y, model, inputs_dict, t_event, y_event, termination) sol.integration_time = integration_time return sol
def solve( self, t_eval=None, solver=None, external_variables=None, inputs=None, check_model=True, ): """ A method to solve the model. This method will automatically build and set the model parameters if not already done so. Parameters ---------- t_eval : numeric type, optional The times at which to compute the solution. If None and the parameter "Current function [A]" is not read from data the model will be solved for a full discharge (1 hour / C_rate). If None and the parameter "Current function [A]" is read from data the model will be solved at the times provided in the data. solver : :class:`pybamm.BaseSolver` The solver to use to solve the model. external_variables : dict A dictionary of external variables and their corresponding values at the current time. The variables must correspond to the variables that would normally be found by solving the submodels that have been made external. inputs : dict, optional Any input parameters to pass to the model when solving check_model : bool, optional If True, model checks are performed after discretisation (see :meth:`pybamm.Discretisation.process_model`). Default is True. """ # Setup self.build(check_model=check_model) if solver is None: solver = self.solver if self.operating_mode == "without experiment": # For drive cycles (current provided as data) we perform additional tests # on t_eval (if provided) to ensure the returned solution captures the # input. If the current is provided as data then the "Current function [A]" # is the tuple (filename, data). # First, read the current function (if provided, otherwise return None) current = self._parameter_values.get("Current function [A]") if isinstance(current, tuple): filename = self._parameter_values["Current function [A]"][0] time_data = self._parameter_values["Current function [A]"][1][:, 0] # If no t_eval is provided, we use the times provided in the data. if t_eval is None: pybamm.logger.info( "Setting t_eval as specified by the data '{}'".format(filename) ) t_eval = time_data # If t_eval is provided we first check if it contains all of the times # in the data to within 10-12. If it doesn't, we then check # that the largest gap in t_eval is smaller than the smallest gap in the # time data (to ensure the resolution of t_eval is fine enough). # We only raise a warning here as users may genuinely only want # the solution returned at some specified points. elif ( set(np.round(time_data, 12)).issubset(set(np.round(t_eval, 12))) ) is False: warnings.warn( """ t_eval does not contain all of the time points in the data '{}'. Note: passing t_eval = None automatically sets t_eval to be the points in the data. """.format( filename ), pybamm.SolverWarning, ) dt_data_min = np.min(np.diff(time_data)) dt_eval_max = np.max(np.diff(t_eval)) if dt_eval_max > dt_data_min + sys.float_info.epsilon: warnings.warn( """ The largest timestep in t_eval ({}) is larger than the smallest timestep in the data ({}). The returned solution may not have the correct resolution to accurately capture the input. Try refining t_eval. Alternatively, passing t_eval = None automatically sets t_eval to be the points in the data. """.format( dt_eval_max, dt_data_min ), pybamm.SolverWarning, ) # If not using a drive cycle and t_eval is not provided, set t_eval # to correspond to a single discharge elif t_eval is None: if current is None: t_end = 1 else: # Get C-rate, return None if it doesn't exist capacity = self.parameter_values["Cell capacity [A.h]"] if isinstance(current, pybamm.InputParameter): C_rate = inputs["Current function [A]"] / capacity t_end = 3600 / C_rate else: try: C_rate = current / capacity t_end = 3600 / C_rate except TypeError: t_end = 3600 t_eval = np.linspace(0, t_end, 100) self.t_eval = t_eval self._solution = solver.solve( self.built_model, t_eval, external_variables=external_variables, inputs=inputs, ) elif self.operating_mode == "with experiment": if t_eval is not None: pybamm.logger.warning( "Ignoring t_eval as solution times are specified by the experiment" ) # Re-initialize solution, e.g. for solving multiple times with different # inputs without having to build the simulation again self._solution = None # Step through all experimental conditions inputs = inputs or {} pybamm.logger.info("Start running experiment") timer = pybamm.Timer() for idx, (exp_inputs, dt) in enumerate( zip(self._experiment_inputs, self._experiment_times) ): pybamm.logger.info(self.experiment.operating_conditions_strings[idx]) inputs.update(exp_inputs) # Make sure we take at least 2 timesteps npts = max(int(round(dt / exp_inputs["period"])) + 1, 2) self.step( dt, solver=solver, npts=npts, external_variables=external_variables, inputs=inputs, ) # Only allow events specified by experiment if not ( self._solution.termination == "final time" or "[experiment]" in self._solution.termination ): pybamm.logger.warning( "\n\n\tExperiment is infeasible: '{}' ".format( self._solution.termination ) + "was triggered during '{}'. ".format( self.experiment.operating_conditions_strings[idx] ) + "Try reducing current, shortening the time interval, " "or reducing the period.\n\n" ) break pybamm.logger.info( "Finish experiment simulation, took {}".format( timer.format(timer.time()) ) ) return self.solution
def _integrate(self, model, t_eval, inputs_dict=None): """ Calculate the solution of the algebraic equations through root-finding Parameters ---------- model : :class:`pybamm.BaseModel` The model whose solution to calculate. t_eval : :class:`numpy.array`, size (k,) The times at which to compute the solution inputs_dict : dict, optional Any input parameters to pass to the model when solving """ inputs_dict = inputs_dict or {} if model.convert_to_format == "casadi": inputs = casadi.vertcat(*[x for x in inputs_dict.values()]) else: inputs = inputs_dict y0 = model.y0 if isinstance(y0, casadi.DM): y0 = y0.full().flatten() # The casadi algebraic solver can read rhs equations, but leaves them unchanged # i.e. the part of the solution vector that corresponds to the differential # equations will be equal to the initial condition provided. This allows this # solver to be used for initialising the DAE solvers # Split y0 into differential and algebraic if model.rhs == {}: len_rhs = 0 else: len_rhs = model.rhs_eval(t_eval[0], y0, inputs).shape[0] y0_diff, y0_alg = np.split(y0, [len_rhs]) algebraic = model.algebraic_eval y_alg = np.empty((len(y0_alg), len(t_eval))) timer = pybamm.Timer() integration_time = 0 for idx, t in enumerate(t_eval): def root_fun(y_alg): "Evaluates algebraic using y" y = np.concatenate([y0_diff, y_alg]) out = algebraic(t, y, inputs) pybamm.logger.debug( "Evaluating algebraic equations at t={}, L2-norm is {}". format(t * model.timescale_eval, np.linalg.norm(out))) return out jac = model.jac_algebraic_eval if jac: if issparse(jac(t_eval[0], y0, inputs)): def jac_fn(y_alg): """ Evaluates jacobian using y0_diff (fixed) and y_alg (varying) """ y = np.concatenate([y0_diff, y_alg]) return jac(0, y, inputs)[:, len_rhs:].toarray() else: def jac_fn(y_alg): """ Evaluates jacobian using y0_diff (fixed) and y_alg (varying) """ y = np.concatenate([y0_diff, y_alg]) return jac(0, y, inputs)[:, len_rhs:] else: jac_fn = None # Evaluate algebraic with new t and previous y0, if it's already close # enough then keep it if np.all(abs(algebraic(t, y0, inputs)) < self.tol): pybamm.logger.debug("Keeping same solution at t={}".format(t)) y_alg[:, idx] = y0_alg # Otherwise calculate new y0 else: itr = 0 maxiter = 2 success = False while not success: # Methods which use least-squares are specified as either "lsq", # which uses the default method, or with "lsq__methodname" if self.method.startswith("lsq"): if self.method == "lsq": method = "trf" else: method = self.method[5:] if jac_fn is None: jac_fn = "2-point" timer.reset() sol = optimize.least_squares( root_fun, y0_alg, method=method, ftol=self.tol, jac=jac_fn, bounds=model.bounds, **self.extra_options, ) integration_time += timer.time() # Methods which use minimize are specified as either "minimize", # which uses the default method, or with "minimize__methodname" elif self.method.startswith("minimize"): # Adapt the root function for minimize def root_norm(y): return np.sum(root_fun(y)**2) if jac_fn is None: jac_norm = None else: def jac_norm(y): return np.sum(2 * root_fun(y) * jac_fn(y), 0) if self.method == "minimize": method = None else: method = self.method[10:] extra_options = self.extra_options if np.any(model.bounds[0] != -np.inf) or np.any( model.bounds[1] != np.inf): bounds = [(lb, ub) for lb, ub in zip( model.bounds[0], model.bounds[1])] extra_options["bounds"] = bounds timer.reset() sol = optimize.minimize( root_norm, y0_alg, method=method, tol=self.tol, jac=jac_norm, **extra_options, ) integration_time += timer.time() else: timer.reset() sol = optimize.root( root_fun, y0_alg, method=self.method, tol=self.tol, jac=jac_fn, options=self.extra_options, ) integration_time += timer.time() if sol.success and np.all(abs(sol.fun) < self.tol): # update initial guess for the next iteration y0_alg = sol.x # update solution array y_alg[:, idx] = y0_alg success = True elif not sol.success: raise pybamm.SolverError( "Could not find acceptable solution: {}".format( sol.message)) else: y0_alg = sol.x if itr > maxiter: raise pybamm.SolverError( "Could not find acceptable solution: solver terminated " "successfully, but maximum solution error " "({}) above tolerance ({})".format( np.max(abs(sol.fun)), self.tol)) itr += 1 # Concatenate differential part y_diff = np.r_[[y0_diff] * len(t_eval)].T y_sol = np.r_[y_diff, y_alg] # Return solution object (no events, so pass None to t_event, y_event) sol = pybamm.Solution(t_eval, y_sol, model, inputs_dict, termination="success") sol.integration_time = integration_time return sol
# rht = RHT(T_inf=T_inf, savesol=True, plot=False) # rht.direct_solve() # Try using scipy.minimize npoints = 129 rht = RHT(T_inf=10, savesol=False, plot=False, lambda_reg=1, npoints=npoints) x = np.linspace(0, 1, 129) rht.beta = 1 + 0.1 * np.sin(x) rht.direct_solve() data = rht.direct_solve() def objective(x): rht.beta = x return rht.getObj(data) def jac(x): rht.beta = x return rht.adjoint_solve(data) timer = pybamm.Timer() sol = minimize(objective, [1.1] * 129) print("Without jac: ", timer.time()) timer.reset() sol = minimize(objective, [1.1] * 129, jac=jac) print("With jac: ", timer.time()) # print(sol)
def _integrate(self, model, t_eval, inputs=None): """ Solve a model defined by dydt with initial conditions y0. Parameters ---------- model : :class:`pybamm.BaseModel` The model whose solution to calculate. t_eval : :class:`numpy.array`, size (k,) The times at which to compute the solution inputs : dict, optional Any input parameters to pass to the model when solving Returns ------- object An object containing the times and values of the solution, as well as various diagnostic messages. """ if model.convert_to_format == "casadi": inputs = casadi.vertcat(*[x for x in inputs.values()]) extra_options = { **self.extra_options, "rtol": self.rtol, "atol": self.atol } # Initial conditions y0 = model.y0 if isinstance(y0, casadi.DM): y0 = y0.full().flatten() # check for user-supplied Jacobian implicit_methods = ["Radau", "BDF", "LSODA"] if np.any([self.method in implicit_methods]): if model.jacobian_eval: extra_options.update({ "jac": lambda t, y: model.jacobian_eval(t, y, inputs) }) # make events terminal so that the solver stops when they are reached if model.terminate_events_eval: def event_wrapper(event): def event_fn(t, y): return event(t, y, inputs) event_fn.terminal = True return event_fn events = [ event_wrapper(event) for event in model.terminate_events_eval ] extra_options.update({"events": events}) timer = pybamm.Timer() sol = it.solve_ivp(lambda t, y: model.rhs_eval(t, y, inputs), (t_eval[0], t_eval[-1]), y0, t_eval=t_eval, method=self.method, dense_output=True, **extra_options) integration_time = timer.time() if sol.success: # Set the reason for termination if sol.message == "A termination event occurred.": termination = "event" t_event = [] for time in sol.t_events: if time.size > 0: t_event = np.append(t_event, np.max(time)) t_event = np.array([np.max(t_event)]) y_event = sol.sol(t_event) elif sol.message.startswith( "The solver successfully reached the end"): termination = "final time" t_event = None y_event = np.array(None) sol = pybamm.Solution(sol.t, sol.y, t_event, y_event, termination) sol.integration_time = integration_time return sol else: raise pybamm.SolverError(sol.message)
def step(self, model, dt, npts=2): """ Step the solution of the model forward by a given time increment. The first time this method is called it executes the necessary setup by calling `self.set_up(model)`. Parameters ---------- model : :class:`pybamm.BaseModel` The model whose solution to calculate. Must have attributes rhs and initial_conditions dt : numeric type The timestep over which to step the solution npts : int, optional The number of points at which the solution will be returned during the step dt. default is 2 (returns the solution at t0 and t0 + dt). Raises ------ :class:`pybamm.ModelError` If an empty model is passed (`model.rhs = {}` and `model.algebraic={}`) """ # Make sure model isn't empty if len(model.rhs) == 0 and len(model.algebraic) == 0: raise pybamm.ModelError("Cannot step empty model") # Set timer timer = pybamm.Timer() # Run set up on first step if not hasattr(self, "y0"): start_time = timer.time() self.set_up(model) self.t = 0.0 set_up_time = timer.time() - start_time else: set_up_time = None # Step pybamm.logger.info("Start stepping {}".format(model.name)) t_eval = np.linspace(self.t, self.t + dt, npts) solution, solve_time, termination = self.compute_solution(model, t_eval) # Assign times solution.solve_time = solve_time if set_up_time: solution.total_time = timer.time() - start_time solution.set_up_time = set_up_time # Set self.t and self.y0 to their values at the final step self.t = solution.t[-1] self.y0 = solution.y[:, -1] pybamm.logger.info("Finish stepping {} ({})".format(model.name, termination)) if set_up_time: pybamm.logger.info( "Set-up time: {}, Step time: {}, Total time: {}".format( timer.format(solution.set_up_time), timer.format(solution.solve_time), timer.format(solution.total_time), ) ) else: pybamm.logger.info( "Step time: {}".format(timer.format(solution.solve_time)) ) return solution
def _run_integrator(self, model, y0, inputs_dict, inputs, t_eval, use_grid=True): pybamm.logger.debug("Running CasADi integrator") if use_grid is True: t_eval_shifted = t_eval - t_eval[0] t_eval_shifted_rounded = np.round(t_eval_shifted, decimals=12).tobytes() integrator = self.integrators[model][t_eval_shifted_rounded] else: integrator = self.integrators[model]["no grid"] len_rhs = model.concatenated_rhs.size y0_diff = y0[:len_rhs] y0_alg = y0[len_rhs:] try: # Try solving if use_grid is True: t_min = t_eval[0] inputs_with_tmin = casadi.vertcat(inputs, t_min) # Call the integrator once, with the grid timer = pybamm.Timer() casadi_sol = integrator(x0=y0_diff, z0=y0_alg, p=inputs_with_tmin, **self.extra_options_call) integration_time = timer.time() y_sol = casadi.vertcat(casadi_sol["xf"], casadi_sol["zf"]) sol = pybamm.Solution(t_eval, y_sol, model, inputs_dict) sol.integration_time = integration_time return sol else: # Repeated calls to the integrator x = y0_diff z = y0_alg y_diff = x y_alg = z for i in range(len(t_eval) - 1): t_min = t_eval[i] t_max = t_eval[i + 1] inputs_with_tlims = casadi.vertcat(inputs, t_min, t_max) timer = pybamm.Timer() casadi_sol = integrator(x0=x, z0=z, p=inputs_with_tlims, **self.extra_options_call) integration_time = timer.time() x = casadi_sol["xf"] z = casadi_sol["zf"] y_diff = casadi.horzcat(y_diff, x) if not z.is_empty(): y_alg = casadi.horzcat(y_alg, z) if z.is_empty(): sol = pybamm.Solution(t_eval, y_diff, model, inputs_dict) else: y_sol = casadi.vertcat(y_diff, y_alg) sol = pybamm.Solution(t_eval, y_sol, model, inputs_dict) sol.integration_time = integration_time return sol except RuntimeError as e: # If it doesn't work raise error raise pybamm.SolverError(e.args[0])
def _integrate(self, model, t_eval, inputs=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 compute the solution """ if model.rhs_eval.form == "casadi": # stack inputs inputs = casadi.vertcat(*[x for x in inputs.values()]) if model.jacobian_eval is None: raise pybamm.SolverError( "KLU requires the Jacobian to be provided") try: atol = model.atol except AttributeError: atol = self._atol y0 = model.y0 if isinstance(y0, casadi.DM): y0 = y0.full().flatten() rtol = self._rtol atol = self._check_atol_type(atol, y0.size) mass_matrix = model.mass_matrix.entries if model.jacobian_eval: jac_y0_t0 = model.jacobian_eval(t_eval[0], y0, inputs) if sparse.issparse(jac_y0_t0): def jacfn(t, y, cj): j = model.jacobian_eval(t, y, inputs) - cj * mass_matrix return j else: def jacfn(t, y, cj): jac_eval = model.jacobian_eval(t, y, inputs) - cj * mass_matrix return sparse.csr_matrix(jac_eval) class SundialsJacobian: def __init__(self): self.J = None random = np.random.random(size=y0.size) J = jacfn(10, random, 20) self.nnz = J.nnz # hoping nnz remains constant... def jac_res(self, t, y, cj): # must be of form j_res = (dr/dy) - (cj) (dr/dy') # cj is just the input parameter # see p68 of the ida_guide.pdf for more details self.J = jacfn(t, y, cj) def get_jac_data(self): return self.J.data def get_jac_row_vals(self): return self.J.indices def get_jac_col_ptrs(self): return self.J.indptr # solver works with ydot0 set to zero ydot0 = np.zeros_like(y0) jac_class = SundialsJacobian() num_of_events = len(model.terminate_events_eval) use_jac = 1 def rootfn(t, y): return_root = np.ones((num_of_events, )) return_root[:] = [ event(t, y, inputs) for event in model.terminate_events_eval ] return return_root # get ids of rhs and algebraic variables rhs_ids = np.ones(model.rhs_eval(0, y0, inputs).shape) alg_ids = np.zeros(len(y0) - len(rhs_ids)) ids = np.concatenate((rhs_ids, alg_ids)) # solve timer = pybamm.Timer() sol = idaklu.solve( t_eval, y0, ydot0, lambda t, y, ydot: model.residuals_eval(t, y, ydot, inputs), jac_class.jac_res, jac_class.get_jac_data, jac_class.get_jac_row_vals, jac_class.get_jac_col_ptrs, jac_class.nnz, rootfn, num_of_events, use_jac, ids, atol, rtol, ) integration_time = timer.time() t = sol.t number_of_timesteps = t.size number_of_states = y0.size y_out = sol.y.reshape((number_of_timesteps, number_of_states)) # return solution, we need to tranpose y to match scipy's interface if sol.flag in [0, 2]: # 0 = solved for all t_eval if sol.flag == 0: termination = "final time" # 2 = found root(s) elif sol.flag == 2: termination = "event" sol = pybamm.Solution( sol.t, np.transpose(y_out), t[-1], np.transpose(y_out[-1])[:, np.newaxis], termination, ) sol.integration_time = integration_time return sol else: raise pybamm.SolverError(sol.message)
def solve( self, t_eval=None, solver=None, check_model=True, save_at_cycles=None, starting_solution=None, **kwargs, ): """ A method to solve the model. This method will automatically build and set the model parameters if not already done so. Parameters ---------- t_eval : numeric type, optional The times (in seconds) at which to compute the solution. Can be provided as an array of times at which to return the solution, or as a list `[t0, tf]` where `t0` is the initial time and `tf` is the final time. If provided as a list the solution is returned at 100 points within the interval `[t0, tf]`. If not using an experiment or running a drive cycle simulation (current provided as data) `t_eval` *must* be provided. If running an experiment the values in `t_eval` are ignored, and the solution times are specified by the experiment. If None and the parameter "Current function [A]" is read from data (i.e. drive cycle simulation) the model will be solved at the times provided in the data. solver : :class:`pybamm.BaseSolver`, optional The solver to use to solve the model. If None, Simulation.solver is used check_model : bool, optional If True, model checks are performed after discretisation (see :meth:`pybamm.Discretisation.process_model`). Default is True. save_at_cycles : int or list of ints, optional Which cycles to save the full sub-solutions for. If None, all cycles are saved. If int, every multiple of save_at_cycles is saved. If list, every cycle in the list is saved. starting_solution : :class:`pybamm.Solution` The solution to start stepping from. If None (default), then self._solution is used. Must be None if not using an experiment. **kwargs Additional key-word arguments passed to `solver.solve`. See :meth:`pybamm.BaseSolver.solve`. """ # Setup if solver is None: solver = self.solver if self.operating_mode in ["without experiment", "drive cycle"]: self.build(check_model=check_model) if save_at_cycles is not None: raise ValueError( "'save_at_cycles' option can only be used if simulating an " "Experiment ") if starting_solution is not None: raise ValueError( "starting_solution can only be provided if simulating an Experiment" ) if self.operating_mode == "without experiment": if t_eval is None: raise pybamm.SolverError( "'t_eval' must be provided if not using an experiment or " "simulating a drive cycle. 't_eval' can be provided as an " "array of times at which to return the solution, or as a " "list [t0, tf] where t0 is the initial time and tf is the " "final time. " "For a constant current (dis)charge the suggested 't_eval' " "is [0, 3700/C] where C is the C-rate. " "For example, run\n\n" "\tsim.solve([0, 3700])\n\n" "for a 1C discharge.") elif self.operating_mode == "drive cycle": # For drive cycles (current provided as data) we perform additional # tests on t_eval (if provided) to ensure the returned solution # captures the input. time_data = self._parameter_values["Current function [A]"].x[0] # If no t_eval is provided, we use the times provided in the data. if t_eval is None: pybamm.logger.info( "Setting t_eval as specified by the data") t_eval = time_data # If t_eval is provided we first check if it contains all of the # times in the data to within 10-12. If it doesn't, we then check # that the largest gap in t_eval is smaller than the smallest gap in # the time data (to ensure the resolution of t_eval is fine enough). # We only raise a warning here as users may genuinely only want # the solution returned at some specified points. elif (set(np.round(time_data, 12)).issubset( set(np.round(t_eval, 12)))) is False: warnings.warn( """ t_eval does not contain all of the time points in the data set. Note: passing t_eval = None automatically sets t_eval to be the points in the data. """, pybamm.SolverWarning, ) dt_data_min = np.min(np.diff(time_data)) dt_eval_max = np.max(np.diff(t_eval)) if dt_eval_max > dt_data_min + sys.float_info.epsilon: warnings.warn( """ The largest timestep in t_eval ({}) is larger than the smallest timestep in the data ({}). The returned solution may not have the correct resolution to accurately capture the input. Try refining t_eval. Alternatively, passing t_eval = None automatically sets t_eval to be the points in the data. """.format(dt_eval_max, dt_data_min), pybamm.SolverWarning, ) self._solution = solver.solve(self.built_model, t_eval, **kwargs) elif self.operating_mode == "with experiment": self.build_for_experiment(check_model=check_model) if t_eval is not None: pybamm.logger.warning( "Ignoring t_eval as solution times are specified by the experiment" ) # Re-initialize solution, e.g. for solving multiple times with different # inputs without having to build the simulation again self._solution = starting_solution # Step through all experimental conditions inputs = kwargs.get("inputs", {}) pybamm.logger.info("Start running experiment") timer = pybamm.Timer() if starting_solution is None: starting_solution_cycles = [] else: starting_solution_cycles = starting_solution.cycles.copy() cycle_offset = len(starting_solution_cycles) all_cycle_solutions = starting_solution_cycles current_solution = starting_solution idx = 0 num_cycles = len(self.experiment.cycle_lengths) feasible = True # simulation will stop if experiment is infeasible for cycle_num, cycle_length in enumerate( self.experiment.cycle_lengths, start=1): pybamm.logger.notice( f"Cycle {cycle_num+cycle_offset}/{num_cycles+cycle_offset} " f"({timer.time()} elapsed) " + "-" * 20) steps = [] cycle_solution = None for step_num in range(1, cycle_length + 1): exp_inputs = self._experiment_inputs[idx] dt = self._experiment_times[idx] op_conds_str = self.experiment.operating_conditions_strings[ idx] op_conds_elec = self.experiment.operating_conditions[ idx][:2] model = self.op_conds_to_built_models[op_conds_elec] # Use 1-indexing for printing cycle number as it is more # human-intuitive pybamm.logger.notice( f"Cycle {cycle_num+cycle_offset}/{num_cycles+cycle_offset}, " f"step {step_num}/{cycle_length}: {op_conds_str}") inputs.update(exp_inputs) kwargs["inputs"] = inputs # Make sure we take at least 2 timesteps npts = max(int(round(dt / exp_inputs["period"])) + 1, 2) step_solution = solver.step( current_solution, model, dt, npts=npts, save=False, **kwargs, ) steps.append(step_solution) current_solution = step_solution cycle_solution = cycle_solution + step_solution # Only allow events specified by experiment if not (cycle_solution is None or cycle_solution.termination == "final time" or "[experiment]" in cycle_solution.termination): feasible = False break # Increment index for next iteration idx += 1 # Break if the experiment is infeasible if feasible is False: pybamm.logger.warning( "\n\n\tExperiment is infeasible: '{}' ".format( cycle_solution.termination) + "was triggered during '{}'. ".format( self.experiment.operating_conditions_strings[idx]) + "The returned solution only contains the first " "{} cycles. ".format(cycle_num - 1 + cycle_offset) + "Try reducing the current, shortening the time interval, " "or reducing the period.\n\n") break # At the final step of the inner loop we save the cycle self._solution = self.solution + cycle_solution cycle_solution.steps = steps all_cycle_solutions.append(cycle_solution) if self.solution is not None: self.solution.cycles = all_cycle_solutions pybamm.logger.notice( "Finish experiment simulation, took {}".format(timer.time())) return self.solution
def solve(self, model, t_eval, external_variables=None, inputs=None): """ Execute the solver setup and calculate the solution of the model at specified times. Parameters ---------- model : :class:`pybamm.BaseModel` The model whose solution to calculate. Must have attributes rhs and initial_conditions t_eval : numeric type The times at which to compute the solution external_variables : dict A dictionary of external variables and their corresponding values at the current time inputs : dict, optional Any input parameters to pass to the model when solving Raises ------ :class:`pybamm.ModelError` If an empty model is passed (`model.rhs = {}` and `model.algebraic={}`) """ pybamm.logger.info("Start solving {} with {}".format( model.name, self.name)) # Make sure model isn't empty if len(model.rhs) == 0 and len(model.algebraic) == 0: raise pybamm.ModelError("Cannot solve empty model") # Make sure t_eval is monotonic if (np.diff(t_eval) < 0).any(): raise pybamm.SolverError("t_eval must increase monotonically") # Non-dimensionalise t_eval # Set up timer = pybamm.Timer() # Set up external variables and inputs external_variables = external_variables or {} inputs = inputs or {} ext_and_inputs = {**external_variables, **inputs} # Raise warning if t_eval looks like it was supposed to be dimensionless # already if t_eval[-1] < 0.5: raise pybamm.SolverError( """It looks like t_eval might be dimensionless. t_eval should now be provided in seconds""") # Set up (if not done already) if model not in self.models_set_up: self.set_up(model, ext_and_inputs) set_up_time = timer.time() self.models_set_up.add(model) else: set_up_time = 0 # Non-dimensionalise time t_eval_dimensionless = t_eval / model.timescale_eval # Solve # Set inputs and external self.set_inputs(model, ext_and_inputs) # Calculate discontinuities discontinuities = [ event.expression.evaluate(u=inputs) for event in model.discontinuity_events_eval ] # make sure they are increasing in time discontinuities = sorted(discontinuities) # remove any identical discontinuities discontinuities = [ v for i, v in enumerate(discontinuities) if (i == len(discontinuities) - 1 or discontinuities[i] < discontinuities[i + 1]) and v > 0 ] if len(discontinuities) > 0: pybamm.logger.info( "Discontinuity events found at t = {}".format(discontinuities)) else: pybamm.logger.info("No discontinuity events found") # insert time points around discontinuities in t_eval # keep track of sub sections to integrate by storing start and end indices start_indices = [0] end_indices = [] eps = sys.float_info.epsilon for dtime in discontinuities: dindex = np.searchsorted(t_eval_dimensionless, dtime, side="left") end_indices.append(dindex + 1) start_indices.append(dindex + 1) if dtime - eps < t_eval_dimensionless[dindex] < dtime + eps: t_eval_dimensionless[dindex] += eps t_eval_dimensionless = np.insert(t_eval_dimensionless, dindex, dtime - eps) else: t_eval_dimensionless = np.insert(t_eval_dimensionless, dindex, [dtime - eps, dtime + eps]) end_indices.append(len(t_eval_dimensionless)) # 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 solution = None for start_index, end_index in zip(start_indices, end_indices): pybamm.logger.info("Calling solver for {} < t < {}".format( t_eval_dimensionless[start_index] * model.timescale_eval, t_eval_dimensionless[end_index - 1] * model.timescale_eval, )) timer.reset() new_solution = self._integrate( model, t_eval_dimensionless[start_index:end_index], ext_and_inputs) new_solution.solve_time = timer.time() if solution is None: solution = new_solution else: solution.append(new_solution, start_index=0) if solution.termination != "final time": break if end_index != len(t_eval_dimensionless): # setup for next integration subsection last_state = solution.y[:, -1] if len(model.algebraic) > 0: model.y0 = self.calculate_consistent_state( model, t_eval_dimensionless[end_index], last_state, ext_and_inputs, ) else: model.y0 = last_state # restore old y0 model.y0 = old_y0 # Assign times solution.set_up_time = set_up_time # Add model and inputs to solution solution.model = model solution.inputs = ext_and_inputs # Identify the event that caused termination termination = self.get_termination_reason(solution, model.events) pybamm.logger.info("Finish solving {} ({})".format( model.name, termination)) pybamm.logger.info( "Set-up time: {}, Solve time: {}, Total time: {}".format( timer.format(solution.set_up_time), timer.format(solution.solve_time), timer.format(solution.total_time), )) return solution
def step( self, old_solution, model, dt, npts=2, external_variables=None, inputs=None, save=True, ): """ Step the solution of the model forward by a given time increment. The first time this method is called it executes the necessary setup by calling `self.set_up(model)`. Parameters ---------- old_solution : :class:`pybamm.Solution` or None The previous solution 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 dt : numeric type The timestep over which to step the solution npts : int, optional The number of points at which the solution will be returned during the step dt. default is 2 (returns the solution at t0 and t0 + dt). external_variables : dict A dictionary of external variables and their corresponding values at the current time inputs : dict, optional Any input parameters to pass to the model when solving save : bool Turn on to store the solution of all previous timesteps Raises ------ :class:`pybamm.ModelError` If an empty model is passed (`model.rhs = {}` and `model.algebraic={}`) """ if old_solution is not None and not ( 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 return old_solution # Make sure model isn't empty if len(model.rhs) == 0 and len(model.algebraic) == 0: raise pybamm.ModelError("Cannot step empty model") # Set timer timer = pybamm.Timer() # Set up external variables and inputs external_variables = external_variables or {} inputs = inputs or {} ext_and_inputs = {**external_variables, **inputs} # Run set up on first step if old_solution is None: pybamm.logger.info("Start stepping {} with {}".format( model.name, self.name)) self.set_up(model, ext_and_inputs) t = 0.0 set_up_time = timer.time() else: # initialize with old solution t = old_solution.t[-1] model.y0 = old_solution.y[:, -1] set_up_time = 0 # Non-dimensionalise dt dt_dimensionless = dt / model.timescale_eval # Step t_eval = np.linspace(t, t + dt_dimensionless, npts) # Set inputs and external self.set_inputs(model, ext_and_inputs) pybamm.logger.info("Calling solver") timer.reset() solution = self._integrate(model, t_eval, ext_and_inputs) # Assign times solution.set_up_time = set_up_time solution.solve_time = timer.time() # Add model and inputs to solution solution.model = model solution.inputs = ext_and_inputs # Identify the event that caused termination termination = self.get_termination_reason(solution, model.events) pybamm.logger.debug("Finish stepping {} ({})".format( model.name, termination)) if set_up_time: pybamm.logger.debug( "Set-up time: {}, Step time: {}, Total time: {}".format( timer.format(solution.set_up_time), timer.format(solution.solve_time), timer.format(solution.total_time), )) else: pybamm.logger.debug("Step time: {}".format( timer.format(solution.solve_time))) if save is False or old_solution is None: return solution else: return old_solution + solution
def solve(self, model, t_eval=None, external_variables=None, inputs=None): """ Execute the solver setup and calculate the solution of the model at specified times. Parameters ---------- model : :class:`pybamm.BaseModel` The model whose solution to calculate. Must have attributes rhs and initial_conditions t_eval : numeric type The times (in seconds) at which to compute the solution external_variables : dict A dictionary of external variables and their corresponding values at the current time inputs : dict, optional Any input parameters to pass to the model when solving Raises ------ :class:`pybamm.ModelError` If an empty model is passed (`model.rhs = {}` and `model.algebraic={}` and `model.variables = {}`) """ pybamm.logger.info("Start solving {} with {}".format( model.name, self.name)) # Make sure model isn't empty if len(model.rhs) == 0 and len(model.algebraic) == 0: if not isinstance(self, pybamm.DummySolver): raise pybamm.ModelError( "Cannot solve empty model, use `pybamm.DummySolver` instead" ) # t_eval can only be None if the solver is an algebraic solver. In that case # set it to 0 if t_eval is None: if self.algebraic_solver is True: t_eval = np.array([0]) else: raise ValueError("t_eval cannot be None") # If t_eval is provided as [t0, tf] return the solution at 100 points elif isinstance(t_eval, list): if len(t_eval) == 1 and self.algebraic_solver is True: pass elif len(t_eval) != 2: raise pybamm.SolverError( "'t_eval' can be provided as an array of times at which to " "return the solution, or as a list [t0, tf] where t0 is the " "initial time and tf is the final time, but has been provided " "as a list of length {}.".format(len(t_eval))) else: t_eval = np.linspace(t_eval[0], t_eval[-1], 100) # Make sure t_eval is monotonic if (np.diff(t_eval) < 0).any(): raise pybamm.SolverError("t_eval must increase monotonically") # Set up external variables and inputs ext_and_inputs = self._set_up_ext_and_inputs(model, external_variables, inputs) # Set up timer = pybamm.Timer() # Set up (if not done already) if model not in self.models_set_up: self.set_up(model, ext_and_inputs, t_eval) self.models_set_up.update({ model: { "initial conditions": model.concatenated_initial_conditions } }) else: ics_set_up = self.models_set_up[model]["initial conditions"] # Check that initial conditions have not been updated if ics_set_up.id != model.concatenated_initial_conditions.id: # If the new initial conditions are different, set up again # Doing the whole setup again might be slow, but no need to prematurely # optimize this self.set_up(model, ext_and_inputs, t_eval) self.models_set_up[model][ "initial conditions"] = model.concatenated_initial_conditions set_up_time = timer.time() timer.reset() # (Re-)calculate consistent initial conditions self._set_initial_conditions(model, ext_and_inputs, update_rhs=True) # Non-dimensionalise time t_eval_dimensionless = t_eval / model.timescale_eval # Calculate discontinuities discontinuities = [ event.expression.evaluate(inputs=inputs) for event in model.discontinuity_events_eval ] # make sure they are increasing in time discontinuities = sorted(discontinuities) # remove any identical discontinuities discontinuities = [ v for i, v in enumerate(discontinuities) if (i == len(discontinuities) - 1 or discontinuities[i] < discontinuities[i + 1]) and v > 0 ] # remove any discontinuities after end of t_eval discontinuities = [ v for v in discontinuities if v < t_eval_dimensionless[-1] ] if len(discontinuities) > 0: pybamm.logger.info( "Discontinuity events found at t = {}".format(discontinuities)) else: pybamm.logger.info("No discontinuity events found") # insert time points around discontinuities in t_eval # keep track of sub sections to integrate by storing start and end indices start_indices = [0] end_indices = [] eps = sys.float_info.epsilon for dtime in discontinuities: dindex = np.searchsorted(t_eval_dimensionless, dtime, side="left") end_indices.append(dindex + 1) start_indices.append(dindex + 1) if dtime - eps < t_eval_dimensionless[dindex] < dtime + eps: t_eval_dimensionless[dindex] += eps t_eval_dimensionless = np.insert(t_eval_dimensionless, dindex, dtime - eps) else: t_eval_dimensionless = np.insert(t_eval_dimensionless, dindex, [dtime - eps, dtime + eps]) end_indices.append(len(t_eval_dimensionless)) # 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 solution = None for start_index, end_index in zip(start_indices, end_indices): pybamm.logger.info("Calling solver for {} < t < {}".format( t_eval_dimensionless[start_index] * model.timescale_eval, t_eval_dimensionless[end_index - 1] * model.timescale_eval, )) new_solution = self._integrate( model, t_eval_dimensionless[start_index:end_index], ext_and_inputs) new_solution.solve_time = timer.time() if solution is None: solution = new_solution else: solution.append(new_solution, start_index=0) if solution.termination != "final time": break if end_index != len(t_eval_dimensionless): # setup for next integration subsection last_state = solution.y[:, -1] # update y0 (for DAE solvers, this updates the initial guess for the # rootfinder) model.y0 = last_state if len(model.algebraic) > 0: model.y0 = self.calculate_consistent_state( model, t_eval_dimensionless[end_index], ext_and_inputs) # Assign times solution.set_up_time = set_up_time solution.solve_time = timer.time() # restore old y0 model.y0 = old_y0 # Add model and inputs to solution solution.model = model solution.inputs = ext_and_inputs # Copy the timescale_eval and lengthscale_evals solution.timescale_eval = model.timescale_eval solution.length_scales_eval = model.length_scales_eval # Identify the event that caused termination termination = self.get_termination_reason(solution, model.events) pybamm.logger.info("Finish solving {} ({})".format( model.name, termination)) pybamm.logger.info( "Set-up time: {}, Solve time: {}, Total time: {}".format( timer.format(solution.set_up_time), timer.format(solution.solve_time), timer.format(solution.total_time), )) # Raise error if solution only contains one timestep (except for algebraic # solvers, where we may only expect one time in the solution) if self.algebraic_solver is False and len(solution.t) == 1: raise pybamm.SolverError( "Solution time vector has length 1. " "Check whether simulation terminated too early.") return solution
def _integrate(self, model, t_eval, inputs_dict=None): """ Solve a model defined by dydt 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_dict : dict, optional Any input parameters to pass to the model when solving """ inputs_dict = inputs_dict or {} if model.convert_to_format == "casadi": inputs = casadi.vertcat(*[x for x in inputs_dict.values()]) else: inputs = inputs_dict y0 = model.y0 if isinstance(y0, casadi.DM): y0 = y0.full().flatten() residuals = model.residuals_eval events = model.terminate_events_eval jacobian = model.jacobian_eval mass_matrix = model.mass_matrix.entries def eqsres(t, y, ydot, return_residuals): return_residuals[:] = residuals(t, y, ydot, inputs) def rootfn(t, y, ydot, return_root): return_root[:] = [event(t, y, inputs) for event in events] extra_options = { **self.extra_options, "old_api": False, "rtol": self.rtol, "atol": self.atol, } if jacobian: jac_y0_t0 = jacobian(t_eval[0], y0, inputs) if sparse.issparse(jac_y0_t0): def jacfn(t, y, ydot, residuals, cj, J): jac_eval = jacobian(t, y, inputs) - cj * mass_matrix J[:][:] = jac_eval.toarray() else: def jacfn(t, y, ydot, residuals, cj, J): jac_eval = jacobian(t, y, inputs) - cj * mass_matrix J[:][:] = jac_eval extra_options.update({"jacfn": jacfn}) if events: extra_options.update({"rootfn": rootfn, "nr_rootfns": len(events)}) # solver works with ydot0 set to zero ydot0 = np.zeros_like(y0) # set up and solve dae_solver = scikits_odes.dae(self.method, eqsres, **extra_options) timer = pybamm.Timer() sol = dae_solver.solve(t_eval, y0, ydot0) integration_time = timer.time() # return solution, we need to tranpose y to match scipy's interface if sol.flag in [0, 2]: # 0 = solved for all t_eval if sol.flag == 0: termination = "final time" # 2 = found root(s) elif sol.flag == 2: termination = "event" if sol.roots.t is None: t_root = None else: t_root = sol.roots.t sol = pybamm.Solution( sol.values.t, np.transpose(sol.values.y), model, inputs_dict, t_root, np.transpose(sol.roots.y), termination, ) sol.integration_time = integration_time return sol else: raise pybamm.SolverError(sol.message)
def step( self, old_solution, model, dt, npts=2, external_variables=None, inputs=None, save=True, ): """ Step the solution of the model forward by a given time increment. The first time this method is called it executes the necessary setup by calling `self.set_up(model)`. Parameters ---------- old_solution : :class:`pybamm.Solution` or None The previous solution 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 dt : numeric type The timestep (in seconds) over which to step the solution npts : int, optional The number of points at which the solution will be returned during the step dt. default is 2 (returns the solution at t0 and t0 + dt). external_variables : dict A dictionary of external variables and their corresponding values at the current time inputs_dict : dict, optional Any input parameters to pass to the model when solving save : bool Turn on to store the solution of all previous timesteps Raises ------ :class:`pybamm.ModelError` If an empty model is passed (`model.rhs = {}` and `model.algebraic = {}` and `model.variables = {}`) """ if old_solution is not None and not ( 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 return old_solution # Make sure model isn't empty if len(model.rhs) == 0 and len(model.algebraic) == 0: if not isinstance(self, pybamm.DummySolver): raise pybamm.ModelError( "Cannot step empty model, use `pybamm.DummySolver` instead" ) # Make sure dt is positive if dt <= 0: raise pybamm.SolverError("Step time must be positive") # Set timer timer = pybamm.Timer() # Set up external variables and inputs external_variables = external_variables or {} inputs = inputs or {} ext_and_inputs = {**external_variables, **inputs} # Check that any inputs that may affect the scaling have not changed # Set model timescale temp_timescale_eval = model.timescale.evaluate(inputs=inputs) # Set model lengthscales temp_length_scales_eval = { domain: scale.evaluate(inputs=inputs) for domain, scale in model.length_scales.items() } if old_solution is not None: if temp_timescale_eval != old_solution.timescale_eval: raise pybamm.SolverError( "The model timescale is a function of an input parameter " "and the value has changed between steps!" ) for domain in temp_length_scales_eval.keys(): old_dom_eval = old_solution.length_scales_eval[domain] if temp_length_scales_eval[domain] != old_dom_eval: pybamm.logger.error( "The {} domain lengthscale is a function of an input " "parameter and the value has changed between " "steps!".format(domain) ) if old_solution is None: # Run set up on first step pybamm.logger.verbose( "Start stepping {} with {}".format(model.name, self.name) ) self.set_up(model, ext_and_inputs) self.models_set_up.update( {model: {"initial conditions": model.concatenated_initial_conditions}} ) t = 0.0 elif model not in self.models_set_up: # Run set up if the model has changed self.set_up(model, ext_and_inputs) self.models_set_up.update( {model: {"initial conditions": model.concatenated_initial_conditions}} ) if old_solution is not None: t = old_solution.all_ts[-1][-1] if old_solution.all_models[-1] == model: # initialize with old solution model.y0 = old_solution.all_ys[-1][:, -1] else: model.y0 = ( model.set_initial_conditions_from(old_solution) .concatenated_initial_conditions.evaluate(0, inputs=ext_and_inputs) .flatten() ) set_up_time = timer.time() # (Re-)calculate consistent initial conditions self._set_initial_conditions(model, ext_and_inputs, update_rhs=False) # Non-dimensionalise dt dt_dimensionless = dt / model.timescale_eval # Step t_eval = np.linspace(t, t + dt_dimensionless, npts) pybamm.logger.verbose( "Stepping for {:.0f} < t < {:.0f}".format( t * model.timescale_eval, (t + dt_dimensionless) * model.timescale_eval, ) ) timer.reset() solution = self._integrate(model, t_eval, ext_and_inputs) solution.solve_time = timer.time() # Check if extrapolation occurred extrapolation = self.check_extrapolation(solution, model.events) if extrapolation: warnings.warn( "While solving {} extrapolation occurred for {}".format( model.name, extrapolation ), pybamm.SolverWarning, ) # 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 solution.set_up_time = set_up_time # Report times pybamm.logger.verbose("Finish stepping {} ({})".format(model.name, termination)) pybamm.logger.verbose( ( "Set-up time: {}, Step time: {} (of which integration time: {}), " "Total time: {}" ).format( solution.set_up_time, solution.solve_time, solution.integration_time, solution.total_time, ) ) # Return solution if save is False: return solution else: return old_solution + solution
def _integrate(self, model, t_eval, inputs_dict=None): """ Calculate the solution of the algebraic equations through root-finding Parameters ---------- model : :class:`pybamm.BaseModel` The model whose solution to calculate. t_eval : :class:`numpy.array`, size (k,) The times at which to compute the solution inputs_dict : dict, optional Any input parameters to pass to the model when solving. If any input parameters that are present in the model are missing from "inputs", then the solution will consist of `ProcessedSymbolicVariable` objects, which must be provided with inputs to obtain their value. """ # Record whether there are any symbolic inputs inputs_dict = inputs_dict or {} has_symbolic_inputs = any( isinstance(v, casadi.MX) for v in inputs_dict.values() ) symbolic_inputs = casadi.vertcat( *[v for v in inputs_dict.values() if isinstance(v, casadi.MX)] ) # Create casadi objects for the root-finder inputs = casadi.vertcat(*[v for v in inputs_dict.values()]) y0 = model.y0 # If y0 already satisfies the tolerance for all t then keep it if has_symbolic_inputs is False and all( np.all(abs(model.casadi_algebraic(t, y0, inputs).full()) < self.tol) for t in t_eval ): pybamm.logger.debug("Keeping same solution at all times") return pybamm.Solution( t_eval, y0, model, inputs_dict, termination="success" ) # The casadi algebraic solver can read rhs equations, but leaves them unchanged # i.e. the part of the solution vector that corresponds to the differential # equations will be equal to the initial condition provided. This allows this # solver to be used for initialising the DAE solvers if model.rhs == {}: len_rhs = 0 y0_diff = casadi.DM() y0_alg = y0 else: len_rhs = model.concatenated_rhs.size y0_diff = y0[:len_rhs] y0_alg = y0[len_rhs:] y_alg = None # Set up t_sym = casadi.MX.sym("t") y_alg_sym = casadi.MX.sym("y_alg", y0_alg.shape[0]) y_sym = casadi.vertcat(y0_diff, y_alg_sym) t_and_inputs_sym = casadi.vertcat(t_sym, symbolic_inputs) alg = model.casadi_algebraic(t_sym, y_sym, inputs) # Check interpolant extrapolation if model.interpolant_extrapolation_events_eval: extrap_event = [ event(0, y0, inputs) for event in model.interpolant_extrapolation_events_eval ] if extrap_event: if (np.concatenate(extrap_event) < self.extrap_tol).any(): extrap_event_names = [] for event in model.events: if ( event.event_type == pybamm.EventType.INTERPOLANT_EXTRAPOLATION and ( event.expression.evaluate( 0, y0.full(), inputs=inputs_dict ) < self.extrap_tol ) ): extrap_event_names.append(event.name[12:]) raise pybamm.SolverError( "CasADi solver failed because the following interpolation " "bounds were exceeded at the initial conditions: {}. " "You may need to provide additional interpolation points " "outside these bounds.".format(extrap_event_names) ) # Set constraints vector in the casadi format # Constrain the unknowns. 0 (default): no constraint on ui, 1: ui >= 0.0, # -1: ui <= 0.0, 2: ui > 0.0, -2: ui < 0.0. constraints = np.zeros_like(model.bounds[0], dtype=int) # If the lower bound is positive then the variable must always be positive constraints[model.bounds[0] >= 0] = 1 # If the upper bound is negative then the variable must always be negative constraints[model.bounds[1] <= 0] = -1 # Set up rootfinder roots = casadi.rootfinder( "roots", "newton", dict(x=y_alg_sym, p=t_and_inputs_sym, g=alg), { **self.extra_options, "abstol": self.tol, "constraints": list(constraints[len_rhs:]), }, ) timer = pybamm.Timer() integration_time = 0 for idx, t in enumerate(t_eval): # Evaluate algebraic with new t and previous y0, if it's already close # enough then keep it # We can't do this if there are symbolic inputs if has_symbolic_inputs is False and np.all( abs(model.casadi_algebraic(t, y0, inputs).full()) < self.tol ): pybamm.logger.debug( "Keeping same solution at t={}".format(t * model.timescale_eval) ) if y_alg is None: y_alg = y0_alg else: y_alg = casadi.horzcat(y_alg, y0_alg) # Otherwise calculate new y_sol else: t_eval_inputs_sym = casadi.vertcat(t, symbolic_inputs) # Solve try: timer.reset() y_alg_sol = roots(y0_alg, t_eval_inputs_sym) integration_time += timer.time() success = True message = None # Check final output y_sol = casadi.vertcat(y0_diff, y_alg_sol) fun = model.casadi_algebraic(t, y_sol, inputs) except RuntimeError as err: success = False message = err.args[0] fun = None # If there are no symbolic inputs, check the function is below the tol # Skip this check if there are symbolic inputs if success and ( has_symbolic_inputs is True or (not any(np.isnan(fun)) and np.all(casadi.fabs(fun) < self.tol)) ): # update initial guess for the next iteration y0_alg = y_alg_sol y0 = casadi.vertcat(y0_diff, y0_alg) # update solution array if y_alg is None: y_alg = y_alg_sol else: y_alg = casadi.horzcat(y_alg, y_alg_sol) elif not success: raise pybamm.SolverError( "Could not find acceptable solution: {}".format(message) ) elif any(np.isnan(fun)): raise pybamm.SolverError( "Could not find acceptable solution: solver returned NaNs" ) else: raise pybamm.SolverError( """ Could not find acceptable solution: solver terminated successfully, but maximum solution error ({}) above tolerance ({}) """.format( casadi.mmax(casadi.fabs(fun)), self.tol ) ) # Concatenate differential part y_diff = casadi.horzcat(*[y0_diff] * len(t_eval)) y_sol = casadi.vertcat(y_diff, y_alg) # Return solution object (no events, so pass None to t_event, y_event) sol = pybamm.Solution( [t_eval], y_sol, model, inputs_dict, termination="success" ) sol.integration_time = integration_time return sol
def solve( self, t_eval=None, solver=None, external_variables=None, inputs=None, check_model=True, ): """ A method to solve the model. This method will automatically build and set the model parameters if not already done so. Parameters ---------- t_eval : numeric type, optional The times (in seconds) at which to compute the solution. Can be provided as an array of times at which to return the solution, or as a list `[t0, tf]` where `t0` is the initial time and `tf` is the final time. If provided as a list the solution is returned at 100 points within the interval `[t0, tf]`. If not using an experiment or running a drive cycle simulation (current provided as data) `t_eval` *must* be provided. If running an experiment the values in `t_eval` are ignored, and the solution times are specified by the experiment. If None and the parameter "Current function [A]" is read from data (i.e. drive cycle simulation) the model will be solved at the times provided in the data. solver : :class:`pybamm.BaseSolver` The solver to use to solve the model. external_variables : dict A dictionary of external variables and their corresponding values at the current time. The variables must correspond to the variables that would normally be found by solving the submodels that have been made external. inputs : dict, optional Any input parameters to pass to the model when solving check_model : bool, optional If True, model checks are performed after discretisation (see :meth:`pybamm.Discretisation.process_model`). Default is True. """ # Setup self.build(check_model=check_model) if solver is None: solver = self.solver if self.operating_mode in ["without experiment", "drive cycle"]: if self.operating_mode == "without experiment": if t_eval is None: raise pybamm.SolverError( "'t_eval' must be provided if not using an experiment or " "simulating a drive cycle. 't_eval' can be provided as an " "array of times at which to return the solution, or as a " "list [t0, tf] where t0 is the initial time and tf is the " "final time. " "For a constant current (dis)charge the suggested 't_eval' " "is [0, 3700/C] where C is the C-rate.") elif self.operating_mode == "drive cycle": # For drive cycles (current provided as data) we perform additional # tests on t_eval (if provided) to ensure the returned solution # captures the input. time_data = self._parameter_values[ "Current function [A]"].data[:, 0] # If no t_eval is provided, we use the times provided in the data. if t_eval is None: pybamm.logger.info( "Setting t_eval as specified by the data") t_eval = time_data # If t_eval is provided we first check if it contains all of the # times in the data to within 10-12. If it doesn't, we then check # that the largest gap in t_eval is smaller than the smallest gap in # the time data (to ensure the resolution of t_eval is fine enough). # We only raise a warning here as users may genuinely only want # the solution returned at some specified points. elif (set(np.round(time_data, 12)).issubset( set(np.round(t_eval, 12)))) is False: warnings.warn( """ t_eval does not contain all of the time points in the data set. Note: passing t_eval = None automatically sets t_eval to be the points in the data. """, pybamm.SolverWarning, ) dt_data_min = np.min(np.diff(time_data)) dt_eval_max = np.max(np.diff(t_eval)) if dt_eval_max > dt_data_min + sys.float_info.epsilon: warnings.warn( """ The largest timestep in t_eval ({}) is larger than the smallest timestep in the data ({}). The returned solution may not have the correct resolution to accurately capture the input. Try refining t_eval. Alternatively, passing t_eval = None automatically sets t_eval to be the points in the data. """.format(dt_eval_max, dt_data_min), pybamm.SolverWarning, ) self._solution = solver.solve( self.built_model, t_eval, external_variables=external_variables, inputs=inputs, ) self.t_eval = self._solution.t * self.model.timescale.evaluate() elif self.operating_mode == "with experiment": if t_eval is not None: pybamm.logger.warning( "Ignoring t_eval as solution times are specified by the experiment" ) # Re-initialize solution, e.g. for solving multiple times with different # inputs without having to build the simulation again self._solution = None # Step through all experimental conditions inputs = inputs or {} pybamm.logger.info("Start running experiment") timer = pybamm.Timer() for idx, (exp_inputs, dt) in enumerate( zip(self._experiment_inputs, self._experiment_times)): pybamm.logger.info( self.experiment.operating_conditions_strings[idx]) inputs.update(exp_inputs) # Make sure we take at least 2 timesteps npts = max(int(round(dt / exp_inputs["period"])) + 1, 2) self.step( dt, solver=solver, npts=npts, external_variables=external_variables, inputs=inputs, ) # Only allow events specified by experiment if not (self._solution.termination == "final time" or "[experiment]" in self._solution.termination): pybamm.logger.warning( "\n\n\tExperiment is infeasible: '{}' ".format( self._solution.termination) + "was triggered during '{}'. ".format( self.experiment.operating_conditions_strings[idx]) + "Try reducing current, shortening the time interval, " "or reducing the period.\n\n") break pybamm.logger.info("Finish experiment simulation, took {}".format( timer.format(timer.time()))) return self.solution
def solve( self, model, t_eval=None, external_variables=None, inputs=None, initial_conditions=None, nproc=None, ): """ Execute the solver setup and calculate the solution of the model at specified times. Parameters ---------- model : :class:`pybamm.BaseModel` The model whose solution to calculate. Must have attributes rhs and initial_conditions t_eval : numeric type The times (in seconds) at which to compute the solution external_variables : dict A dictionary of external variables and their corresponding values at the current time inputs : dict or list, optional A dictionary or list of dictionaries describing any input parameters to pass to the model when solving initial_conditions : :class:`pybamm.Symbol`, optional Initial conditions to use when solving the model. If None (default), `model.concatenated_initial_conditions` is used. Otherwise, must be a symbol of size `len(model.rhs) + len(model.algebraic)`. nproc : int, optional Number of processes to use when solving for more than one set of input parameters. Defaults to value returned by "os.cpu_count()". Returns ------- :class:`pybamm.Solution` or list of :class:`pybamm.Solution` objects. If type of `inputs` is `list`, return a list of corresponding :class:`pybamm.Solution` objects. Raises ------ :class:`pybamm.ModelError` If an empty model is passed (`model.rhs = {}` and `model.algebraic={}` and `model.variables = {}`) """ pybamm.logger.info("Start solving {} with {}".format(model.name, self.name)) # Make sure model isn't empty if len(model.rhs) == 0 and len(model.algebraic) == 0: if not isinstance(self, pybamm.DummySolver): raise pybamm.ModelError( "Cannot solve empty model, use `pybamm.DummySolver` instead" ) # t_eval can only be None if the solver is an algebraic solver. In that case # set it to 0 if t_eval is None: if self.algebraic_solver is True: t_eval = np.array([0]) else: raise ValueError("t_eval cannot be None") # If t_eval is provided as [t0, tf] return the solution at 100 points elif isinstance(t_eval, list): if len(t_eval) == 1 and self.algebraic_solver is True: pass elif len(t_eval) != 2: raise pybamm.SolverError( "'t_eval' can be provided as an array of times at which to " "return the solution, or as a list [t0, tf] where t0 is the " "initial time and tf is the final time, but has been provided " "as a list of length {}.".format(len(t_eval)) ) else: t_eval = np.linspace(t_eval[0], t_eval[-1], 100) # Make sure t_eval is monotonic if (np.diff(t_eval) < 0).any(): raise pybamm.SolverError("t_eval must increase monotonically") # Set up external variables and 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] ext_and_inputs_list = [ self._set_up_ext_and_inputs(model, external_variables, inputs) for inputs in inputs_list ] # Cannot use multiprocessing with model in "jax" format if (len(inputs_list) > 1) and model.convert_to_format == "jax": raise pybamm.SolverError( "Cannot solve list of inputs with multiprocessing " 'when model in format "jax".' ) # Set up (if not done already) timer = pybamm.Timer() if model not in self.models_set_up: # It is assumed that when len(inputs_list) > 1, model set # up (initial condition, time-scale and length-scale) does # not depend on input parameters. Thefore only `ext_and_inputs[0]` # is passed to `set_up`. # See https://github.com/pybamm-team/PyBaMM/pull/1261 self.set_up(model, ext_and_inputs_list[0], t_eval) self.models_set_up.update( {model: {"initial conditions": model.concatenated_initial_conditions}} ) else: ics_set_up = self.models_set_up[model]["initial conditions"] # Check that initial conditions have not been updated if ics_set_up.id != model.concatenated_initial_conditions.id: # If the new initial conditions are different, set up again # Doing the whole setup again might be slow, but no need to prematurely # optimize this self.set_up(model, ext_and_inputs_list[0], t_eval) self.models_set_up[model][ "initial conditions" ] = model.concatenated_initial_conditions set_up_time = timer.time() timer.reset() # (Re-)calculate consistent initial conditions # Assuming initial conditions do not depend on input parameters # when len(inputs_list) > 1, only `ext_and_inputs_list[0]` # is passed to `_set_initial_conditions`. # See https://github.com/pybamm-team/PyBaMM/pull/1261 if len(inputs_list) > 1: all_inputs_names = set( itertools.chain.from_iterable( [ext_and_inputs.keys() for ext_and_inputs in ext_and_inputs_list] ) ) initial_conditions_node_names = set( [it.name for it in model.concatenated_initial_conditions.pre_order()] ) if all_inputs_names.issubset(initial_conditions_node_names): raise pybamm.SolverError( "Input parameters cannot appear in expression " "for initial conditions." ) self._set_initial_conditions(model, ext_and_inputs_list[0], update_rhs=True) # Non-dimensionalise time t_eval_dimensionless = t_eval / model.timescale_eval # Calculate discontinuities discontinuities = [ # Assuming that discontinuities do not depend on # input parameters when len(input_list) > 1, only # `input_list[0]` is passed to `evaluate`. # See https://github.com/pybamm-team/PyBaMM/pull/1261 event.expression.evaluate(inputs=inputs_list[0]) for event in model.discontinuity_events_eval ] # make sure they are increasing in time discontinuities = sorted(discontinuities) # remove any identical discontinuities discontinuities = [ v for i, v in enumerate(discontinuities) if ( i == len(discontinuities) - 1 or discontinuities[i] < discontinuities[i + 1] ) and v > 0 ] # remove any discontinuities after end of t_eval discontinuities = [v for v in discontinuities if v < t_eval_dimensionless[-1]] if len(discontinuities) > 0: pybamm.logger.verbose( "Discontinuity events found at t = {}".format(discontinuities) ) if isinstance(inputs, list): raise pybamm.SolverError( "Cannot solve for a list of input parameters" " sets with discontinuities" ) else: pybamm.logger.verbose("No discontinuity events found") # insert time points around discontinuities in t_eval # keep track of sub sections to integrate by storing start and end indices start_indices = [0] end_indices = [] eps = sys.float_info.epsilon for dtime in discontinuities: dindex = np.searchsorted(t_eval_dimensionless, dtime, side="left") end_indices.append(dindex + 1) start_indices.append(dindex + 1) if dtime - eps < t_eval_dimensionless[dindex] < dtime + eps: t_eval_dimensionless[dindex] += eps t_eval_dimensionless = np.insert( t_eval_dimensionless, dindex, dtime - eps ) else: t_eval_dimensionless = np.insert( t_eval_dimensionless, dindex, [dtime - eps, dtime + eps] ) end_indices.append(len(t_eval_dimensionless)) # 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 solutions = None for start_index, end_index in zip(start_indices, end_indices): pybamm.logger.verbose( "Calling solver for {} < t < {}".format( t_eval_dimensionless[start_index] * model.timescale_eval, t_eval_dimensionless[end_index - 1] * model.timescale_eval, ) ) ninputs = len(ext_and_inputs_list) if ninputs == 1: new_solution = self._integrate( model, t_eval_dimensionless[start_index:end_index], ext_and_inputs_list[0], ) new_solutions = [new_solution] else: with mp.Pool(processes=nproc) as p: new_solutions = p.starmap( self._integrate, zip( [model] * ninputs, [t_eval_dimensionless[start_index:end_index]] * ninputs, ext_and_inputs_list, ), ) p.close() p.join() # Setting the solve time for each segment. # pybamm.Solution.__add__ assumes attribute solve_time. solve_time = timer.time() for sol in new_solutions: sol.solve_time = solve_time if start_index == start_indices[0]: solutions = [sol for sol in new_solutions] else: for i, new_solution in enumerate(new_solutions): solutions[i] = solutions[i] + new_solution if solutions[0].termination != "final time": break if end_index != len(t_eval_dimensionless): # 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 if len(model.algebraic) > 0: model.y0 = self.calculate_consistent_state( model, t_eval_dimensionless[end_index], ext_and_inputs_list[0] ) solve_time = timer.time() for i, solution in enumerate(solutions): # Check if extrapolation occurred extrapolation = self.check_extrapolation(solution, model.events) if extrapolation: warnings.warn( "While solving {} extrapolation occurred for {}".format( model.name, extrapolation ), pybamm.SolverWarning, ) # Identify the event that caused termination and update the solution to # include the event time and state solutions[i], termination = self.get_termination_reason( solution, model.events ) # Assign times solutions[i].set_up_time = set_up_time # all solutions get the same solve time, but their integration time # will be different (see https://github.com/pybamm-team/PyBaMM/pull/1261) solutions[i].solve_time = solve_time # Restore old y0 model.y0 = old_y0 # Report times if len(solutions) == 1: pybamm.logger.info("Finish solving {} ({})".format(model.name, termination)) pybamm.logger.info( ( "Set-up time: {}, Solve time: {} (of which integration time: {}), " "Total time: {}" ).format( solutions[0].set_up_time, solutions[0].solve_time, solutions[0].integration_time, solutions[0].total_time, ) ) else: pybamm.logger.info("Finish solving {} for all inputs".format(model.name)) pybamm.logger.info( ("Set-up time: {}, Solve time: {}, Total time: {}").format( solutions[0].set_up_time, solutions[0].solve_time, solutions[0].total_time, ) ) # Raise error if solutions[0] only contains one timestep (except for algebraic # solvers, where we may only expect one time in the solution) if ( self.algebraic_solver is False and len(solution.all_ts) == 1 and len(solution.all_ts[0]) == 1 ): raise pybamm.SolverError( "Solution time vector has length 1. " "Check whether simulation terminated too early." ) # Return solution(s) if ninputs == 1: return solutions[0] else: return solutions
def _integrate(self, model, t_eval, inputs_dict=None): """ Solve a model defined by dydt 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_dict : dict, optional Any input parameters to pass to the model when solving """ if model.rhs_eval.form == "casadi": inputs = casadi.vertcat(*[x for x in inputs_dict.values()]) else: inputs = inputs_dict y0 = model.y0 if isinstance(y0, casadi.DM): y0 = y0.full().flatten() derivs = model.rhs_eval events = model.terminate_events_eval jacobian = model.jacobian_eval def eqsydot(t, y, return_ydot): return_ydot[:] = derivs(t, y, inputs) def rootfn(t, y, return_root): return_root[:] = [event(t, y, inputs) for event in events] if jacobian: jac_y0_t0 = jacobian(t_eval[0], y0, inputs) if sparse.issparse(jac_y0_t0): def jacfn(t, y, fy, J): J[:][:] = jacobian(t, y, inputs).toarray() def jac_times_vecfn(v, Jv, t, y, userdata): Jv[:] = userdata._jac_eval * v return 0 else: def jacfn(t, y, fy, J): J[:][:] = jacobian(t, y, inputs) def jac_times_vecfn(v, Jv, t, y, userdata): Jv[:] = np.matmul(userdata._jac_eval, v) return 0 def jac_times_setupfn(t, y, fy, userdata): userdata._jac_eval = jacobian(t, y, inputs) return 0 extra_options = { **self.extra_options, "old_api": False, "rtol": self.rtol, "atol": self.atol, } # Read linsolver (defaults to dense) linsolver = extra_options.get("linsolver", "dense") if jacobian: if linsolver in ("dense", "lapackdense"): extra_options.update({"jacfn": jacfn}) elif linsolver in ("spgmr", "spbcgs", "sptfqmr"): extra_options.update( { "jac_times_setupfn": jac_times_setupfn, "jac_times_vecfn": jac_times_vecfn, "user_data": self, } ) if events: extra_options.update({"rootfn": rootfn, "nr_rootfns": len(events)}) ode_solver = scikits_odes.ode(self.method, eqsydot, **extra_options) timer = pybamm.Timer() sol = ode_solver.solve(t_eval, y0) integration_time = timer.time() # return solution, we need to tranpose y to match scipy's ivp interface if sol.flag in [0, 2]: # 0 = solved for all t_eval if sol.flag == 0: termination = "final time" # 2 = found root(s) elif sol.flag == 2: termination = "event" if sol.roots.t is None: t_root = None else: t_root = sol.roots.t sol = pybamm.Solution( sol.values.t, np.transpose(sol.values.y), model, inputs_dict, t_root, np.transpose(sol.roots.y), termination, ) sol.integration_time = integration_time return sol else: raise pybamm.SolverError(sol.message)