From 13ba1775941ca51cdcadaf990f5a10822915446c Mon Sep 17 00:00:00 2001 From: Valentin Sulzer Date: Sat, 27 Jun 2020 10:48:52 -0400 Subject: [PATCH 1/8] rescaling integrator --- pybamm/solvers/casadi_solver.py | 67 +- test.py | 128 ++++ tests/unit/test_solvers/test_casadi_solver.py | 723 +++++++++--------- 3 files changed, 526 insertions(+), 392 deletions(-) create mode 100644 test.py diff --git a/pybamm/solvers/casadi_solver.py b/pybamm/solvers/casadi_solver.py index 555db10e95..47988f80a0 100644 --- a/pybamm/solvers/casadi_solver.py +++ b/pybamm/solvers/casadi_solver.py @@ -92,9 +92,7 @@ def __init__( self.name = "CasADi solver with '{}' mode".format(mode) # Initialize - self.problems = {} - self.options = {} - self.methods = {} + self.integrators = {} pybamm.citations.register("Andersson2019") @@ -114,15 +112,14 @@ def _integrate(self, model, t_eval, inputs=None): inputs = inputs or {} # convert inputs to casadi format inputs = casadi.vertcat(*[x for x in inputs.values()]) + integrator = self.get_integrator(model, inputs) if self.mode == "fast": - integrator = self.get_integrator(model, t_eval, inputs) solution = self._run_integrator(integrator, model, model.y0, inputs, t_eval) solution.termination = "final time" return solution elif not model.events: pybamm.logger.info("No events found, running fast mode") - integrator = self.get_integrator(model, t_eval, inputs) solution = self._run_integrator(integrator, model, model.y0, inputs, t_eval) solution.termination = "final time" return solution @@ -151,7 +148,7 @@ def _integrate(self, model, t_eval, inputs=None): # Non-dimensionalise provided dt_max dt_max = self.dt_max / model.timescale_eval else: - dt_max = 0.01 + dt_max = 0.05 * min(model.timescale_eval, t_f) / model.timescale_eval dt_eval_max = np.max(np.diff(t_eval)) * 1.01 dt_max = np.max([dt_max, dt_eval_max]) while t < t_f: @@ -248,8 +245,13 @@ def event_fun(t): # return truncated solution t_truncated = current_step_sol.t[current_step_sol.t < t_event] - y_trunctaed = current_step_sol.y[:, 0 : len(t_truncated)] - truncated_step_sol = pybamm.Solution(t_truncated, y_trunctaed) + y_truncated = current_step_sol.y[:, 0 : len(t_truncated)] + # add the event to the truncated solution + t_truncated = np.concatenate([t_truncated, np.array([t_event])]) + y_truncated = np.concatenate( + [y_truncated, y_event[:, np.newaxis]], axis=1 + ) + truncated_step_sol = pybamm.Solution(t_truncated, y_truncated) # assign temporary solve time truncated_step_sol.solve_time = np.nan # append solution from the current step to solution @@ -340,9 +342,11 @@ def event_fun(t): y0 = solution.y[:, -1] return solution - def get_integrator(self, model, t_eval, inputs): + def get_integrator(self, model, inputs): # Only set up problem once - if model not in self.problems: + if model in self.integrators: + return self.integrators[model] + else: y0 = model.y0 rhs = model.casadi_rhs algebraic = model.casadi_algebraic @@ -358,50 +362,53 @@ def get_integrator(self, model, t_eval, inputs): options = { **self.extra_options_setup, - "grid": t_eval, "reltol": self.rtol, "abstol": self.atol, - "output_t0": True, "show_eval_warnings": show_eval_warnings, } # set up and solve + # rescale time so that the integrator is always [0,1] + # this also requires multiplying the rhs by (t_max - t_min) further down t = casadi.MX.sym("t") + t_min = casadi.MX.sym("t_min") + t_max = casadi.MX.sym("t_max") + t_scaled = t_min + (t_max - t_min) * t + # add time limits as inputs p = casadi.MX.sym("p", inputs.shape[0]) - y_diff = casadi.MX.sym("y_diff", rhs(t_eval[0], y0, p).shape[0]) - problem = {"t": t, "x": y_diff, "p": p} - if algebraic(t_eval[0], y0, p).is_empty(): + p_with_tlims = casadi.vertcat(p, t_min, t_max) + y_diff = casadi.MX.sym("y_diff", rhs(0, y0, p).shape[0]) + problem = {"t": t, "x": y_diff, "p": p_with_tlims} + if algebraic(0, y0, p).is_empty(): method = "cvodes" - problem.update({"ode": rhs(t, y_diff, p)}) + # rescale rhs by (t_max - t_min) + problem.update({"ode": (t_max - t_min) * rhs(t_scaled, y_diff, p)}) else: options["calc_ic"] = True method = "idas" - y_alg = casadi.MX.sym("y_alg", algebraic(t_eval[0], y0, p).shape[0]) + y_alg = casadi.MX.sym("y_alg", algebraic(0, y0, p).shape[0]) y_full = casadi.vertcat(y_diff, y_alg) + # rescale rhs by (t_max - t_min) problem.update( { "z": y_alg, - "ode": rhs(t, y_full, p), - "alg": algebraic(t, y_full, p), + "ode": (t_max - t_min) * rhs(t_scaled, y_full, p), + "alg": algebraic(t_scaled, y_full, p), } ) - self.problems[model] = problem - self.options[model] = options - self.methods[model] = method - else: - # problem stays the same - # just update options - self.options[model]["grid"] = t_eval - return casadi.integrator( - "F", self.methods[model], self.problems[model], self.options[model] - ) + integrator = casadi.integrator("F", method, problem, options) + self.integrators[model] = integrator + return integrator def _run_integrator(self, integrator, model, y0, inputs, t_eval): rhs_size = model.concatenated_rhs.size y0_diff, y0_alg = np.split(y0, [rhs_size]) + inputs_with_tlims = casadi.vertcat(inputs, t_eval[0], t_eval[-1]) try: # Try solving - sol = integrator(x0=y0_diff, z0=y0_alg, p=inputs, **self.extra_options_call) + sol = integrator( + x0=y0_diff, z0=y0_alg, p=inputs_with_tlims, **self.extra_options_call + ) y_values = np.concatenate([sol["xf"].full(), sol["zf"].full()]) return pybamm.Solution(t_eval, y_values) except RuntimeError as e: diff --git a/test.py b/test.py new file mode 100644 index 0000000000..4947954e5c --- /dev/null +++ b/test.py @@ -0,0 +1,128 @@ +from casadi import * +import time + +# Define ode +t = MX.sym("t") +x = MX.sym("x") +p = MX.sym("p") + +x0 = np.ones(x.shape[0]) +t_eval = np.linspace(0, 1, 100) + +t_max = MX.sym("t_min") +t_min = MX.sym("t_max") +tlims = casadi.vertcat(t_min, t_max) + +ode = -(t_max - t_min) * p * x + +# value of the parameter for evaluating +p_eval = 1 + +# First approach: simple integrator without a grid +# fastest but doesn't give the intermediate points +print("no grid") +print("*" * 10) +itg_nogrid = integrator( + "F", "cvodes", {"t": t, "x": x, "ode": ode, "p": casadi.vertcat(p, tlims)} +) + +start = time.time() +itg_nogrid(x0=1, p=[p_eval, 0, 1]) +print("value:", time.time() - start) + +jac_nogrid = Function( + "j", [p], [jacobian(itg_nogrid(x0=x0, p=casadi.vertcat(p, 0, 1))["xf"], p)] +) +start = time.time() +jac_nogrid(1) +print("jacobian:", time.time() - start) + +# Second approach: integrator with a grid +# fast, gives intermediate points, but can't take the jacobian +print("With grid") +print("*" * 10) +itg_grid_auto = integrator( + "F", + "cvodes", + {"t": t, "x": x, "ode": -p * x, "p": p}, + {"grid": t_eval, "output_t0": True}, +) + +start = time.time() +itg_grid_auto(x0=1, p=p_eval) +print("value:", time.time() - start) + +# Fails +rep_p = repmat(p, 1, len(t_eval)) +jac_grid_auto = Function("j", [p], [itg_grid_auto(x0=x0, p=p * DM.ones(100, 1))["xf"]]) +jac_grid_auto(1) +# print("jacobian: fails") + +# Third approach: multiple calls through manual for loop +print("For loop") +print("*" * 10) + + +def itg_grid_manual(x0, p_eval, t_eval): + X = x0 + for i in range(t_eval.shape[0] - 1): + t_min = t_eval[i] + t_max = t_eval[i + 1] + xnew = itg_nogrid(x0=x0, p=casadi.vertcat(p_eval, t_min, t_max))["xf"] + X = casadi.horzcat(X, xnew) + x0 = xnew + return X + + +start = time.time() +itg_grid_manual(x0, p_eval, t_eval) +print("value:", time.time() - start) + +jac_grid_manual = Function("j", [p], [jacobian(itg_grid_manual(x0, p, t_eval), p)],) +start = time.time() +jac_grid_manual(1) +print("jacobian:", time.time() - start) + + +# Fourth approach: multiple calls through mapaccum +print("mapaccum") +print("*" * 10) + +x0_sym = MX.sym("x0", x.shape[0]) +itg_fn = Function( + "itg_fn", + [x0_sym, p, tlims], + [itg_nogrid(x0=x0_sym, p=casadi.vertcat(p, tlims))["xf"]], +) +itg_grid_mapaccum = itg_fn.mapaccum("Fn", len(t_eval) - 1) + +tlims_eval = casadi.horzcat(t_eval[:-1], t_eval[1:]).T + +start = time.time() +itg_grid_mapaccum(x0, p_eval, tlims_eval) +print("value:", time.time() - start) + +# Jacobians +jac_grid_mapaccum = Function( + "j", [p], [jacobian(itg_grid_mapaccum(x0, p, tlims_eval), p)] +) +start = time.time() +jac_grid_mapaccum(1) +print("jacobian:", time.time() - start) + +# +# +# +# Creating vs using integrator +# print("no grid") +# print("*" * 10) +# start = time.time() +# itg_nogrid = integrator( +# "F", "cvodes", {"t": t, "x": x, "ode": ode, "p": casadi.vertcat(p, tlims)} +# ) +# print(time.time() - start) + +# start = time.time() +# itg_nogrid(x0=1, p=[p_eval, 0, 1]) +# print(time.time() - start) + diff --git a/tests/unit/test_solvers/test_casadi_solver.py b/tests/unit/test_solvers/test_casadi_solver.py index 1d7e0f2649..69652bdb90 100644 --- a/tests/unit/test_solvers/test_casadi_solver.py +++ b/tests/unit/test_solvers/test_casadi_solver.py @@ -44,368 +44,367 @@ def test_model_solver(self): solution.y[0], np.exp(0.1 * solution.t), decimal=5 ) - def test_model_solver_python(self): - # Create model - pybamm.set_logging_level("ERROR") - model = pybamm.BaseModel() - model.convert_to_format = "python" - var = pybamm.Variable("var") - model.rhs = {var: 0.1 * var} - model.initial_conditions = {var: 1} - # No need to set parameters; can use base discretisation (no spatial operators) - - # create discretisation - disc = pybamm.Discretisation() - disc.process_model(model) - # Solve - solver = pybamm.CasadiSolver(mode="fast", rtol=1e-8, atol=1e-8) - t_eval = np.linspace(0, 1, 100) - solution = solver.solve(model, t_eval) - np.testing.assert_array_equal(solution.t, t_eval) - np.testing.assert_array_almost_equal( - solution.y[0], np.exp(0.1 * solution.t), decimal=5 - ) - pybamm.set_logging_level("WARNING") - - def test_model_solver_failure(self): - # Create model - model = pybamm.BaseModel() - var = pybamm.Variable("var") - model.rhs = {var: -pybamm.sqrt(var)} - model.initial_conditions = {var: 1} - # add events so that safe mode is used (won't be triggered) - model.events = [pybamm.Event("10", var - 10)] - # No need to set parameters; can use base discretisation (no spatial operators) - - # create discretisation - disc = pybamm.Discretisation() - model_disc = disc.process_model(model, inplace=False) - - solver = pybamm.CasadiSolver(extra_options_call={"regularity_check": False}) - solver_old = pybamm.CasadiSolver( - mode="old safe", extra_options_call={"regularity_check": False} - ) - # Solve with failure at t=2 - t_eval = np.linspace(0, 20, 100) - with self.assertRaises(pybamm.SolverError): - solver.solve(model_disc, t_eval) - with self.assertRaises(pybamm.SolverError): - solver_old.solve(model_disc, t_eval) - # Solve with failure at t=0 - model.initial_conditions = {var: 0} - model_disc = disc.process_model(model, inplace=False) - t_eval = np.linspace(0, 20, 100) - with self.assertRaises(pybamm.SolverError): - solver.solve(model_disc, t_eval) - - def test_model_solver_events(self): - # Create model - model = pybamm.BaseModel() - whole_cell = ["negative electrode", "separator", "positive electrode"] - var1 = pybamm.Variable("var1", domain=whole_cell) - var2 = pybamm.Variable("var2", domain=whole_cell) - model.rhs = {var1: 0.1 * var1} - model.algebraic = {var2: 2 * var1 - var2} - model.initial_conditions = {var1: 1, var2: 2} - model.events = [ - pybamm.Event("var1 = 1.5", pybamm.min(var1 - 1.5)), - pybamm.Event("var2 = 2.5", pybamm.min(var2 - 2.5)), - ] - disc = get_discretisation_for_testing() - disc.process_model(model) - - # Solve using "safe" mode - solver = pybamm.CasadiSolver(mode="safe", rtol=1e-8, atol=1e-8) - t_eval = np.linspace(0, 5, 100) - solution = solver.solve(model, t_eval) - np.testing.assert_array_less(solution.y[0], 1.5) - np.testing.assert_array_less(solution.y[-1], 2.5) - np.testing.assert_array_almost_equal( - solution.y[0], np.exp(0.1 * solution.t), decimal=5 - ) - np.testing.assert_array_almost_equal( - solution.y[-1], 2 * np.exp(0.1 * solution.t), decimal=5 - ) - - # Solve using "safe" mode with debug off - pybamm.settings.debug_mode = False - solver = pybamm.CasadiSolver(mode="safe", rtol=1e-8, atol=1e-8, dt_max=1) - t_eval = np.linspace(0, 5, 100) - solution = solver.solve(model, t_eval) - np.testing.assert_array_less(solution.y[0], 1.5) - np.testing.assert_array_less(solution.y[-1], 2.5) - np.testing.assert_array_almost_equal( - solution.y[0], np.exp(0.1 * solution.t), decimal=5 - ) - np.testing.assert_array_almost_equal( - solution.y[-1], 2 * np.exp(0.1 * solution.t), decimal=5 - ) - pybamm.settings.debug_mode = True - - # Solve using "old safe" mode - solver = pybamm.CasadiSolver(mode="old safe", rtol=1e-8, atol=1e-8) - t_eval = np.linspace(0, 5, 100) - solution = solver.solve(model, t_eval) - np.testing.assert_array_less(solution.y[0], 1.5) - np.testing.assert_array_less(solution.y[-1], 2.5) - np.testing.assert_array_almost_equal( - solution.y[0], np.exp(0.1 * solution.t), decimal=5 - ) - np.testing.assert_array_almost_equal( - solution.y[-1], 2 * np.exp(0.1 * solution.t), decimal=5 - ) - - # Test when an event returns nan - model = pybamm.BaseModel() - var = pybamm.Variable("var") - model.rhs = {var: 0.1 * var} - model.initial_conditions = {var: 1} - model.events = [ - pybamm.Event("event", var - 1.02), - pybamm.Event("sqrt event", pybamm.sqrt(1.0199 - var)), - ] - disc = pybamm.Discretisation() - disc.process_model(model) - solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) - solution = solver.solve(model, t_eval) - np.testing.assert_array_less(solution.y[0], 1.02) - - def test_model_step(self): - # Create model - model = pybamm.BaseModel() - domain = ["negative electrode", "separator", "positive electrode"] - var = pybamm.Variable("var", domain=domain) - model.rhs = {var: 0.1 * var} - model.initial_conditions = {var: 1} - # No need to set parameters; can use base discretisation (no spatial operators) - - # create discretisation - mesh = get_mesh_for_testing() - spatial_methods = {"macroscale": pybamm.FiniteVolume()} - disc = pybamm.Discretisation(mesh, spatial_methods) - disc.process_model(model) - - solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) - - # Step once - dt = 1 - step_sol = solver.step(None, model, dt) - np.testing.assert_array_equal(step_sol.t, [0, dt]) - np.testing.assert_array_almost_equal(step_sol.y[0], np.exp(0.1 * step_sol.t)) - - # Step again (return 5 points) - step_sol_2 = solver.step(step_sol, model, dt, npts=5) - np.testing.assert_array_equal( - step_sol_2.t, np.concatenate([np.array([0]), np.linspace(dt, 2 * dt, 5)]) - ) - np.testing.assert_array_almost_equal( - step_sol_2.y[0], np.exp(0.1 * step_sol_2.t) - ) - - # Check steps give same solution as solve - t_eval = step_sol.t - solution = solver.solve(model, t_eval) - np.testing.assert_array_almost_equal(solution.y[0], step_sol.y[0]) - - def test_model_step_with_input(self): - # Create model - model = pybamm.BaseModel() - var = pybamm.Variable("var") - a = pybamm.InputParameter("a") - model.rhs = {var: a * var} - model.initial_conditions = {var: 1} - model.variables = {"a": a} - # No need to set parameters; can use base discretisation (no spatial operators) - - # create discretisation - disc = pybamm.Discretisation() - disc.process_model(model) - - solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) - - # Step with an input - dt = 0.1 - step_sol = solver.step(None, model, dt, npts=5, inputs={"a": 0.1}) - np.testing.assert_array_equal(step_sol.t, np.linspace(0, dt, 5)) - np.testing.assert_allclose(step_sol.y[0], np.exp(0.1 * step_sol.t)) - - # Step again with different inputs - step_sol_2 = solver.step(step_sol, model, dt, npts=5, inputs={"a": -1}) - np.testing.assert_array_equal(step_sol_2.t, np.linspace(0, 2 * dt, 9)) - np.testing.assert_array_equal( - step_sol_2["a"].entries, np.array([0.1, 0.1, 0.1, 0.1, 0.1, -1, -1, -1, -1]) - ) - np.testing.assert_allclose( - step_sol_2.y[0], - np.concatenate( - [ - np.exp(0.1 * step_sol.t[:5]), - np.exp(0.1 * step_sol.t[4]) * np.exp(-(step_sol.t[5:] - dt)), - ] - ), - ) - - def test_model_step_events(self): - # Create model - model = pybamm.BaseModel() - var1 = pybamm.Variable("var1") - var2 = pybamm.Variable("var2") - model.rhs = {var1: 0.1 * var1} - model.algebraic = {var2: 2 * var1 - var2} - model.initial_conditions = {var1: 1, var2: 2} - model.events = [ - pybamm.Event("var1 = 1.5", pybamm.min(var1 - 1.5)), - pybamm.Event("var2 = 2.5", pybamm.min(var2 - 2.5)), - ] - disc = pybamm.Discretisation() - disc.process_model(model) - - # Solve - step_solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) - dt = 0.05 - time = 0 - end_time = 5 - step_solution = None - while time < end_time: - step_solution = step_solver.step(step_solution, model, dt=dt, npts=10) - time += dt - np.testing.assert_array_less(step_solution.y[0], 1.5) - np.testing.assert_array_less(step_solution.y[-1], 2.5001) - np.testing.assert_array_almost_equal( - step_solution.y[0], np.exp(0.1 * step_solution.t), decimal=5 - ) - np.testing.assert_array_almost_equal( - step_solution.y[-1], 2 * np.exp(0.1 * step_solution.t), decimal=4 - ) - - def test_model_solver_with_inputs(self): - # Create model - model = pybamm.BaseModel() - domain = ["negative electrode", "separator", "positive electrode"] - var = pybamm.Variable("var", domain=domain) - model.rhs = {var: -pybamm.InputParameter("rate") * var} - model.initial_conditions = {var: 1} - model.events = [pybamm.Event("var=0.5", pybamm.min(var - 0.5))] - # No need to set parameters; can use base discretisation (no spatial - # operators) - - # create discretisation - mesh = get_mesh_for_testing() - spatial_methods = {"macroscale": pybamm.FiniteVolume()} - disc = pybamm.Discretisation(mesh, spatial_methods) - disc.process_model(model) - # Solve - solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) - t_eval = np.linspace(0, 10, 100) - solution = solver.solve(model, t_eval, inputs={"rate": 0.1}) - self.assertLess(len(solution.t), len(t_eval)) - np.testing.assert_array_equal(solution.t, t_eval[: len(solution.t)]) - np.testing.assert_allclose(solution.y[0], np.exp(-0.1 * solution.t), rtol=1e-06) - - def test_model_solver_dae_inputs_in_initial_conditions(self): - # Create model - model = pybamm.BaseModel() - var1 = pybamm.Variable("var1") - var2 = pybamm.Variable("var2") - model.rhs = {var1: pybamm.InputParameter("rate") * var1} - model.algebraic = {var2: var1 - var2} - model.initial_conditions = { - var1: pybamm.InputParameter("ic 1"), - var2: pybamm.InputParameter("ic 2"), - } - - # Solve - solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) - t_eval = np.linspace(0, 5, 100) - solution = solver.solve( - model, t_eval, inputs={"rate": -1, "ic 1": 0.1, "ic 2": 2} - ) - np.testing.assert_array_almost_equal( - solution.y[0], 0.1 * np.exp(-solution.t), decimal=5 - ) - np.testing.assert_array_almost_equal( - solution.y[-1], 0.1 * np.exp(-solution.t), decimal=5 - ) - - # Solve again with different initial conditions - solution = solver.solve( - model, t_eval, inputs={"rate": -0.1, "ic 1": 1, "ic 2": 3} - ) - np.testing.assert_array_almost_equal( - solution.y[0], 1 * np.exp(-0.1 * solution.t), decimal=5 - ) - np.testing.assert_array_almost_equal( - solution.y[-1], 1 * np.exp(-0.1 * solution.t), decimal=5 - ) - - def test_model_solver_with_external(self): - # Create model - model = pybamm.BaseModel() - domain = ["negative electrode", "separator", "positive electrode"] - var1 = pybamm.Variable("var1", domain=domain) - var2 = pybamm.Variable("var2", domain=domain) - model.rhs = {var1: -var2} - model.initial_conditions = {var1: 1} - model.external_variables = [var2] - model.variables = {"var1": var1, "var2": var2} - # No need to set parameters; can use base discretisation (no spatial - # operators) - - # create discretisation - mesh = get_mesh_for_testing() - spatial_methods = {"macroscale": pybamm.FiniteVolume()} - disc = pybamm.Discretisation(mesh, spatial_methods) - disc.process_model(model) - # Solve - solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) - t_eval = np.linspace(0, 10, 100) - solution = solver.solve(model, t_eval, external_variables={"var2": 0.5}) - np.testing.assert_allclose(solution.y[0], 1 - 0.5 * solution.t, rtol=1e-06) - - def test_model_solver_with_non_identity_mass(self): - model = pybamm.BaseModel() - var1 = pybamm.Variable("var1", domain="negative electrode") - var2 = pybamm.Variable("var2", domain="negative electrode") - model.rhs = {var1: var1} - model.algebraic = {var2: 2 * var1 - var2} - model.initial_conditions = {var1: 1, var2: 2} - disc = get_discretisation_for_testing() - disc.process_model(model) - - # FV discretisation has identity mass. Manually set the mass matrix to - # be a diag of 10s here for testing. Note that the algebraic part is all - # zeros - mass_matrix = 10 * model.mass_matrix.entries - model.mass_matrix = pybamm.Matrix(mass_matrix) - - # Note that mass_matrix_inv is just the inverse of the ode block of the - # mass matrix - mass_matrix_inv = 0.1 * eye(int(mass_matrix.shape[0] / 2)) - model.mass_matrix_inv = pybamm.Matrix(mass_matrix_inv) - - # Solve - solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) - t_eval = np.linspace(0, 1, 100) - solution = solver.solve(model, t_eval) - np.testing.assert_array_equal(solution.t, t_eval) - np.testing.assert_allclose(solution.y[0], np.exp(0.1 * solution.t)) - np.testing.assert_allclose(solution.y[-1], 2 * np.exp(0.1 * solution.t)) - - def test_dae_solver_algebraic_model(self): - model = pybamm.BaseModel() - var = pybamm.Variable("var") - model.algebraic = {var: var + 1} - model.initial_conditions = {var: 0} - - disc = pybamm.Discretisation() - disc.process_model(model) - - solver = pybamm.CasadiSolver() - t_eval = np.linspace(0, 1) - with self.assertRaisesRegex( - pybamm.SolverError, "Cannot use CasadiSolver to solve algebraic model" - ): - solver.solve(model, t_eval) + # def test_model_solver_python(self): + # # Create model + # pybamm.set_logging_level("ERROR") + # model = pybamm.BaseModel() + # model.convert_to_format = "python" + # var = pybamm.Variable("var") + # model.rhs = {var: 0.1 * var} + # model.initial_conditions = {var: 1} + # # No need to set parameters; can use base discretisation (no spatial operators) + + # # create discretisation + # disc = pybamm.Discretisation() + # disc.process_model(model) + # # Solve + # solver = pybamm.CasadiSolver(mode="fast", rtol=1e-8, atol=1e-8) + # t_eval = np.linspace(0, 1, 100) + # solution = solver.solve(model, t_eval) + # np.testing.assert_array_equal(solution.t, t_eval) + # np.testing.assert_array_almost_equal( + # solution.y[0], np.exp(0.1 * solution.t), decimal=5 + # ) + # pybamm.set_logging_level("WARNING") + + # def test_model_solver_failure(self): + # # Create model + # model = pybamm.BaseModel() + # var = pybamm.Variable("var") + # model.rhs = {var: -pybamm.sqrt(var)} + # model.initial_conditions = {var: 1} + # # add events so that safe mode is used (won't be triggered) + # model.events = [pybamm.Event("10", var - 10)] + # # No need to set parameters; can use base discretisation (no spatial operators) + + # # create discretisation + # disc = pybamm.Discretisation() + # model_disc = disc.process_model(model, inplace=False) + + # solver = pybamm.CasadiSolver(extra_options_call={"regularity_check": False}) + # solver_old = pybamm.CasadiSolver( + # mode="old safe", extra_options_call={"regularity_check": False} + # ) + # # Solve with failure at t=2 + # t_eval = np.linspace(0, 20, 100) + # with self.assertRaises(pybamm.SolverError): + # solver.solve(model_disc, t_eval) + # with self.assertRaises(pybamm.SolverError): + # solver_old.solve(model_disc, t_eval) + # # Solve with failure at t=0 + # model.initial_conditions = {var: 0} + # model_disc = disc.process_model(model, inplace=False) + # t_eval = np.linspace(0, 20, 100) + # with self.assertRaises(pybamm.SolverError): + # solver.solve(model_disc, t_eval) + + # def test_model_solver_events(self): + # # Create model + # model = pybamm.BaseModel() + # whole_cell = ["negative electrode", "separator", "positive electrode"] + # var1 = pybamm.Variable("var1", domain=whole_cell) + # var2 = pybamm.Variable("var2", domain=whole_cell) + # model.rhs = {var1: 0.1 * var1} + # model.algebraic = {var2: 2 * var1 - var2} + # model.initial_conditions = {var1: 1, var2: 2} + # model.events = [ + # pybamm.Event("var1 = 1.5", pybamm.min(var1 - 1.5)), + # pybamm.Event("var2 = 2.5", pybamm.min(var2 - 2.5)), + # ] + # disc = get_discretisation_for_testing() + # disc.process_model(model) + + # # Solve using "safe" mode + # solver = pybamm.CasadiSolver(mode="safe", rtol=1e-8, atol=1e-8) + # t_eval = np.linspace(0, 5, 100) + # solution = solver.solve(model, t_eval) + # np.testing.assert_array_less(solution.y[0], 1.5) + # np.testing.assert_array_less(solution.y[-1], 2.5 + 1e-10) + # np.testing.assert_array_almost_equal( + # solution.y[0], np.exp(0.1 * solution.t), decimal=5 + # ) + # np.testing.assert_array_almost_equal( + # solution.y[-1], 2 * np.exp(0.1 * solution.t), decimal=5 + # ) + + # # Solve using "safe" mode with debug off + # pybamm.settings.debug_mode = False + # solver = pybamm.CasadiSolver(mode="safe", rtol=1e-8, atol=1e-8, dt_max=1) + # t_eval = np.linspace(0, 5, 100) + # solution = solver.solve(model, t_eval) + # np.testing.assert_array_less(solution.y[0], 1.5) + # np.testing.assert_array_less(solution.y[-1], 2.5 + 1e-10) + # np.testing.assert_array_almost_equal( + # solution.y[0], np.exp(0.1 * solution.t), decimal=5 + # ) + # np.testing.assert_array_almost_equal( + # solution.y[-1], 2 * np.exp(0.1 * solution.t), decimal=5 + # ) + # pybamm.settings.debug_mode = True + + # # Solve using "old safe" mode + # solver = pybamm.CasadiSolver(mode="old safe", rtol=1e-8, atol=1e-8) + # t_eval = np.linspace(0, 5, 100) + # solution = solver.solve(model, t_eval) + # np.testing.assert_array_less(solution.y[0], 1.5) + # np.testing.assert_array_less(solution.y[-1], 2.5) + # np.testing.assert_array_almost_equal( + # solution.y[0], np.exp(0.1 * solution.t), decimal=5 + # ) + # np.testing.assert_array_almost_equal( + # solution.y[-1], 2 * np.exp(0.1 * solution.t), decimal=5 + # ) + + # # Test when an event returns nan + # model = pybamm.BaseModel() + # var = pybamm.Variable("var") + # model.rhs = {var: 0.1 * var} + # model.initial_conditions = {var: 1} + # model.events = [ + # pybamm.Event("event", var - 1.02), + # pybamm.Event("sqrt event", pybamm.sqrt(1.0199 - var)), + # ] + # disc = pybamm.Discretisation() + # disc.process_model(model) + # solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) + # solution = solver.solve(model, t_eval) + # np.testing.assert_array_less(solution.y[0], 1.02 + 1e-10) + + # def test_model_step(self): + # # Create model + # model = pybamm.BaseModel() + # domain = ["negative electrode", "separator", "positive electrode"] + # var = pybamm.Variable("var", domain=domain) + # model.rhs = {var: 0.1 * var} + # model.initial_conditions = {var: 1} + # # No need to set parameters; can use base discretisation (no spatial operators) + + # # create discretisation + # mesh = get_mesh_for_testing() + # spatial_methods = {"macroscale": pybamm.FiniteVolume()} + # disc = pybamm.Discretisation(mesh, spatial_methods) + # disc.process_model(model) + + # solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) + + # # Step once + # dt = 1 + # step_sol = solver.step(None, model, dt) + # np.testing.assert_array_equal(step_sol.t, [0, dt]) + # np.testing.assert_array_almost_equal(step_sol.y[0], np.exp(0.1 * step_sol.t)) + + # # Step again (return 5 points) + # step_sol_2 = solver.step(step_sol, model, dt, npts=5) + # np.testing.assert_array_equal( + # step_sol_2.t, np.concatenate([np.array([0]), np.linspace(dt, 2 * dt, 5)]) + # ) + # np.testing.assert_array_almost_equal( + # step_sol_2.y[0], np.exp(0.1 * step_sol_2.t) + # ) + + # # Check steps give same solution as solve + # t_eval = step_sol.t + # solution = solver.solve(model, t_eval) + # np.testing.assert_array_almost_equal(solution.y[0], step_sol.y[0]) + + # def test_model_step_with_input(self): + # # Create model + # model = pybamm.BaseModel() + # var = pybamm.Variable("var") + # a = pybamm.InputParameter("a") + # model.rhs = {var: a * var} + # model.initial_conditions = {var: 1} + # model.variables = {"a": a} + # # No need to set parameters; can use base discretisation (no spatial operators) + + # # create discretisation + # disc = pybamm.Discretisation() + # disc.process_model(model) + + # solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) + + # # Step with an input + # dt = 0.1 + # step_sol = solver.step(None, model, dt, npts=5, inputs={"a": 0.1}) + # np.testing.assert_array_equal(step_sol.t, np.linspace(0, dt, 5)) + # np.testing.assert_allclose(step_sol.y[0], np.exp(0.1 * step_sol.t)) + + # # Step again with different inputs + # step_sol_2 = solver.step(step_sol, model, dt, npts=5, inputs={"a": -1}) + # np.testing.assert_array_equal(step_sol_2.t, np.linspace(0, 2 * dt, 9)) + # np.testing.assert_array_equal( + # step_sol_2["a"].entries, np.array([0.1, 0.1, 0.1, 0.1, 0.1, -1, -1, -1, -1]) + # ) + # np.testing.assert_allclose( + # step_sol_2.y[0], + # np.concatenate( + # [ + # np.exp(0.1 * step_sol.t[:5]), + # np.exp(0.1 * step_sol.t[4]) * np.exp(-(step_sol.t[5:] - dt)), + # ] + # ), + # ) + + # def test_model_step_events(self): + # # Create model + # model = pybamm.BaseModel() + # var1 = pybamm.Variable("var1") + # var2 = pybamm.Variable("var2") + # model.rhs = {var1: 0.1 * var1} + # model.algebraic = {var2: 2 * var1 - var2} + # model.initial_conditions = {var1: 1, var2: 2} + # model.events = [ + # pybamm.Event("var1 = 1.5", pybamm.min(var1 - 1.5)), + # pybamm.Event("var2 = 2.5", pybamm.min(var2 - 2.5)), + # ] + # disc = pybamm.Discretisation() + # disc.process_model(model) + + # # Solve + # step_solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) + # dt = 0.05 + # time = 0 + # end_time = 5 + # step_solution = None + # while time < end_time: + # step_solution = step_solver.step(step_solution, model, dt=dt, npts=10) + # time += dt + # np.testing.assert_array_less(step_solution.y[0], 1.5) + # np.testing.assert_array_less(step_solution.y[-1], 2.5001) + # np.testing.assert_array_almost_equal( + # step_solution.y[0], np.exp(0.1 * step_solution.t), decimal=5 + # ) + # np.testing.assert_array_almost_equal( + # step_solution.y[-1], 2 * np.exp(0.1 * step_solution.t), decimal=4 + # ) + + # def test_model_solver_with_inputs(self): + # # Create model + # model = pybamm.BaseModel() + # domain = ["negative electrode", "separator", "positive electrode"] + # var = pybamm.Variable("var", domain=domain) + # model.rhs = {var: -pybamm.InputParameter("rate") * var} + # model.initial_conditions = {var: 1} + # model.events = [pybamm.Event("var=0.5", pybamm.min(var - 0.5))] + # # No need to set parameters; can use base discretisation (no spatial + # # operators) + + # # create discretisation + # mesh = get_mesh_for_testing() + # spatial_methods = {"macroscale": pybamm.FiniteVolume()} + # disc = pybamm.Discretisation(mesh, spatial_methods) + # disc.process_model(model) + # # Solve + # solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) + # t_eval = np.linspace(0, 10, 100) + # solution = solver.solve(model, t_eval, inputs={"rate": 0.1}) + # self.assertLess(len(solution.t), len(t_eval)) + # np.testing.assert_allclose(solution.y[0], np.exp(-0.1 * solution.t), rtol=1e-04) + + # def test_model_solver_dae_inputs_in_initial_conditions(self): + # # Create model + # model = pybamm.BaseModel() + # var1 = pybamm.Variable("var1") + # var2 = pybamm.Variable("var2") + # model.rhs = {var1: pybamm.InputParameter("rate") * var1} + # model.algebraic = {var2: var1 - var2} + # model.initial_conditions = { + # var1: pybamm.InputParameter("ic 1"), + # var2: pybamm.InputParameter("ic 2"), + # } + + # # Solve + # solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) + # t_eval = np.linspace(0, 5, 100) + # solution = solver.solve( + # model, t_eval, inputs={"rate": -1, "ic 1": 0.1, "ic 2": 2} + # ) + # np.testing.assert_array_almost_equal( + # solution.y[0], 0.1 * np.exp(-solution.t), decimal=5 + # ) + # np.testing.assert_array_almost_equal( + # solution.y[-1], 0.1 * np.exp(-solution.t), decimal=5 + # ) + + # # Solve again with different initial conditions + # solution = solver.solve( + # model, t_eval, inputs={"rate": -0.1, "ic 1": 1, "ic 2": 3} + # ) + # np.testing.assert_array_almost_equal( + # solution.y[0], 1 * np.exp(-0.1 * solution.t), decimal=5 + # ) + # np.testing.assert_array_almost_equal( + # solution.y[-1], 1 * np.exp(-0.1 * solution.t), decimal=5 + # ) + + # def test_model_solver_with_external(self): + # # Create model + # model = pybamm.BaseModel() + # domain = ["negative electrode", "separator", "positive electrode"] + # var1 = pybamm.Variable("var1", domain=domain) + # var2 = pybamm.Variable("var2", domain=domain) + # model.rhs = {var1: -var2} + # model.initial_conditions = {var1: 1} + # model.external_variables = [var2] + # model.variables = {"var1": var1, "var2": var2} + # # No need to set parameters; can use base discretisation (no spatial + # # operators) + + # # create discretisation + # mesh = get_mesh_for_testing() + # spatial_methods = {"macroscale": pybamm.FiniteVolume()} + # disc = pybamm.Discretisation(mesh, spatial_methods) + # disc.process_model(model) + # # Solve + # solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) + # t_eval = np.linspace(0, 10, 100) + # solution = solver.solve(model, t_eval, external_variables={"var2": 0.5}) + # np.testing.assert_allclose(solution.y[0], 1 - 0.5 * solution.t, rtol=1e-06) + + # def test_model_solver_with_non_identity_mass(self): + # model = pybamm.BaseModel() + # var1 = pybamm.Variable("var1", domain="negative electrode") + # var2 = pybamm.Variable("var2", domain="negative electrode") + # model.rhs = {var1: var1} + # model.algebraic = {var2: 2 * var1 - var2} + # model.initial_conditions = {var1: 1, var2: 2} + # disc = get_discretisation_for_testing() + # disc.process_model(model) + + # # FV discretisation has identity mass. Manually set the mass matrix to + # # be a diag of 10s here for testing. Note that the algebraic part is all + # # zeros + # mass_matrix = 10 * model.mass_matrix.entries + # model.mass_matrix = pybamm.Matrix(mass_matrix) + + # # Note that mass_matrix_inv is just the inverse of the ode block of the + # # mass matrix + # mass_matrix_inv = 0.1 * eye(int(mass_matrix.shape[0] / 2)) + # model.mass_matrix_inv = pybamm.Matrix(mass_matrix_inv) + + # # Solve + # solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) + # t_eval = np.linspace(0, 1, 100) + # solution = solver.solve(model, t_eval) + # np.testing.assert_array_equal(solution.t, t_eval) + # np.testing.assert_allclose(solution.y[0], np.exp(0.1 * solution.t)) + # np.testing.assert_allclose(solution.y[-1], 2 * np.exp(0.1 * solution.t)) + + # def test_dae_solver_algebraic_model(self): + # model = pybamm.BaseModel() + # var = pybamm.Variable("var") + # model.algebraic = {var: var + 1} + # model.initial_conditions = {var: 0} + + # disc = pybamm.Discretisation() + # disc.process_model(model) + + # solver = pybamm.CasadiSolver() + # t_eval = np.linspace(0, 1) + # with self.assertRaisesRegex( + # pybamm.SolverError, "Cannot use CasadiSolver to solve algebraic model" + # ): + # solver.solve(model, t_eval) if __name__ == "__main__": From 667b8f3f63a7cde0b4dd3a27af48d03e7ddd2e4d Mon Sep 17 00:00:00 2001 From: Valentin Sulzer Date: Sun, 28 Jun 2020 17:49:34 -0400 Subject: [PATCH 2/8] #1082 some easy casadi solver fixes --- pybamm/solvers/casadi_solver.py | 60 +- tests/unit/test_solvers/test_casadi_solver.py | 725 +++++++++--------- 2 files changed, 393 insertions(+), 392 deletions(-) diff --git a/pybamm/solvers/casadi_solver.py b/pybamm/solvers/casadi_solver.py index 47988f80a0..40c9646f2b 100644 --- a/pybamm/solvers/casadi_solver.py +++ b/pybamm/solvers/casadi_solver.py @@ -92,7 +92,9 @@ def __init__( self.name = "CasADi solver with '{}' mode".format(mode) # Initialize - self.integrators = {} + self.problems = {} + self.options = {} + self.methods = {} pybamm.citations.register("Andersson2019") @@ -112,14 +114,15 @@ def _integrate(self, model, t_eval, inputs=None): inputs = inputs or {} # convert inputs to casadi format inputs = casadi.vertcat(*[x for x in inputs.values()]) - integrator = self.get_integrator(model, inputs) if self.mode == "fast": + integrator = self.get_integrator(model, t_eval, inputs) solution = self._run_integrator(integrator, model, model.y0, inputs, t_eval) solution.termination = "final time" return solution elif not model.events: pybamm.logger.info("No events found, running fast mode") + integrator = self.get_integrator(model, t_eval, inputs) solution = self._run_integrator(integrator, model, model.y0, inputs, t_eval) solution.termination = "final time" return solution @@ -148,7 +151,7 @@ def _integrate(self, model, t_eval, inputs=None): # Non-dimensionalise provided dt_max dt_max = self.dt_max / model.timescale_eval else: - dt_max = 0.05 * min(model.timescale_eval, t_f) / model.timescale_eval + dt_max = 0.01 * min(model.timescale_eval, t_f) / model.timescale_eval dt_eval_max = np.max(np.diff(t_eval)) * 1.01 dt_max = np.max([dt_max, dt_eval_max]) while t < t_f: @@ -256,10 +259,10 @@ def event_fun(t): truncated_step_sol.solve_time = np.nan # append solution from the current step to solution solution.append(truncated_step_sol) - solution.termination = "event" solution.t_event = t_event solution.y_event = y_event + break else: # assign temporary solve time @@ -342,11 +345,9 @@ def event_fun(t): y0 = solution.y[:, -1] return solution - def get_integrator(self, model, inputs): + def get_integrator(self, model, t_eval, inputs): # Only set up problem once - if model in self.integrators: - return self.integrators[model] - else: + if model not in self.problems: y0 = model.y0 rhs = model.casadi_rhs algebraic = model.casadi_algebraic @@ -362,53 +363,50 @@ def get_integrator(self, model, inputs): options = { **self.extra_options_setup, + "grid": t_eval, "reltol": self.rtol, "abstol": self.atol, + "output_t0": True, "show_eval_warnings": show_eval_warnings, } # set up and solve - # rescale time so that the integrator is always [0,1] - # this also requires multiplying the rhs by (t_max - t_min) further down t = casadi.MX.sym("t") - t_min = casadi.MX.sym("t_min") - t_max = casadi.MX.sym("t_max") - t_scaled = t_min + (t_max - t_min) * t - # add time limits as inputs p = casadi.MX.sym("p", inputs.shape[0]) - p_with_tlims = casadi.vertcat(p, t_min, t_max) - y_diff = casadi.MX.sym("y_diff", rhs(0, y0, p).shape[0]) - problem = {"t": t, "x": y_diff, "p": p_with_tlims} - if algebraic(0, y0, p).is_empty(): + y_diff = casadi.MX.sym("y_diff", rhs(t_eval[0], y0, p).shape[0]) + problem = {"t": t, "x": y_diff, "p": p} + if algebraic(t_eval[0], y0, p).is_empty(): method = "cvodes" - # rescale rhs by (t_max - t_min) - problem.update({"ode": (t_max - t_min) * rhs(t_scaled, y_diff, p)}) + problem.update({"ode": rhs(t, y_diff, p)}) else: options["calc_ic"] = True method = "idas" - y_alg = casadi.MX.sym("y_alg", algebraic(0, y0, p).shape[0]) + y_alg = casadi.MX.sym("y_alg", algebraic(t_eval[0], y0, p).shape[0]) y_full = casadi.vertcat(y_diff, y_alg) - # rescale rhs by (t_max - t_min) problem.update( { "z": y_alg, - "ode": (t_max - t_min) * rhs(t_scaled, y_full, p), - "alg": algebraic(t_scaled, y_full, p), + "ode": rhs(t, y_full, p), + "alg": algebraic(t, y_full, p), } ) - integrator = casadi.integrator("F", method, problem, options) - self.integrators[model] = integrator - return integrator + self.problems[model] = problem + self.options[model] = options + self.methods[model] = method + else: + # problem stays the same + # just update options + self.options[model]["grid"] = t_eval + return casadi.integrator( + "F", self.methods[model], self.problems[model], self.options[model] + ) def _run_integrator(self, integrator, model, y0, inputs, t_eval): rhs_size = model.concatenated_rhs.size y0_diff, y0_alg = np.split(y0, [rhs_size]) - inputs_with_tlims = casadi.vertcat(inputs, t_eval[0], t_eval[-1]) try: # Try solving - sol = integrator( - x0=y0_diff, z0=y0_alg, p=inputs_with_tlims, **self.extra_options_call - ) + sol = integrator(x0=y0_diff, z0=y0_alg, p=inputs, **self.extra_options_call) y_values = np.concatenate([sol["xf"].full(), sol["zf"].full()]) return pybamm.Solution(t_eval, y_values) except RuntimeError as e: diff --git a/tests/unit/test_solvers/test_casadi_solver.py b/tests/unit/test_solvers/test_casadi_solver.py index 69652bdb90..54b63380cd 100644 --- a/tests/unit/test_solvers/test_casadi_solver.py +++ b/tests/unit/test_solvers/test_casadi_solver.py @@ -44,367 +44,370 @@ def test_model_solver(self): solution.y[0], np.exp(0.1 * solution.t), decimal=5 ) - # def test_model_solver_python(self): - # # Create model - # pybamm.set_logging_level("ERROR") - # model = pybamm.BaseModel() - # model.convert_to_format = "python" - # var = pybamm.Variable("var") - # model.rhs = {var: 0.1 * var} - # model.initial_conditions = {var: 1} - # # No need to set parameters; can use base discretisation (no spatial operators) - - # # create discretisation - # disc = pybamm.Discretisation() - # disc.process_model(model) - # # Solve - # solver = pybamm.CasadiSolver(mode="fast", rtol=1e-8, atol=1e-8) - # t_eval = np.linspace(0, 1, 100) - # solution = solver.solve(model, t_eval) - # np.testing.assert_array_equal(solution.t, t_eval) - # np.testing.assert_array_almost_equal( - # solution.y[0], np.exp(0.1 * solution.t), decimal=5 - # ) - # pybamm.set_logging_level("WARNING") - - # def test_model_solver_failure(self): - # # Create model - # model = pybamm.BaseModel() - # var = pybamm.Variable("var") - # model.rhs = {var: -pybamm.sqrt(var)} - # model.initial_conditions = {var: 1} - # # add events so that safe mode is used (won't be triggered) - # model.events = [pybamm.Event("10", var - 10)] - # # No need to set parameters; can use base discretisation (no spatial operators) - - # # create discretisation - # disc = pybamm.Discretisation() - # model_disc = disc.process_model(model, inplace=False) - - # solver = pybamm.CasadiSolver(extra_options_call={"regularity_check": False}) - # solver_old = pybamm.CasadiSolver( - # mode="old safe", extra_options_call={"regularity_check": False} - # ) - # # Solve with failure at t=2 - # t_eval = np.linspace(0, 20, 100) - # with self.assertRaises(pybamm.SolverError): - # solver.solve(model_disc, t_eval) - # with self.assertRaises(pybamm.SolverError): - # solver_old.solve(model_disc, t_eval) - # # Solve with failure at t=0 - # model.initial_conditions = {var: 0} - # model_disc = disc.process_model(model, inplace=False) - # t_eval = np.linspace(0, 20, 100) - # with self.assertRaises(pybamm.SolverError): - # solver.solve(model_disc, t_eval) - - # def test_model_solver_events(self): - # # Create model - # model = pybamm.BaseModel() - # whole_cell = ["negative electrode", "separator", "positive electrode"] - # var1 = pybamm.Variable("var1", domain=whole_cell) - # var2 = pybamm.Variable("var2", domain=whole_cell) - # model.rhs = {var1: 0.1 * var1} - # model.algebraic = {var2: 2 * var1 - var2} - # model.initial_conditions = {var1: 1, var2: 2} - # model.events = [ - # pybamm.Event("var1 = 1.5", pybamm.min(var1 - 1.5)), - # pybamm.Event("var2 = 2.5", pybamm.min(var2 - 2.5)), - # ] - # disc = get_discretisation_for_testing() - # disc.process_model(model) - - # # Solve using "safe" mode - # solver = pybamm.CasadiSolver(mode="safe", rtol=1e-8, atol=1e-8) - # t_eval = np.linspace(0, 5, 100) - # solution = solver.solve(model, t_eval) - # np.testing.assert_array_less(solution.y[0], 1.5) - # np.testing.assert_array_less(solution.y[-1], 2.5 + 1e-10) - # np.testing.assert_array_almost_equal( - # solution.y[0], np.exp(0.1 * solution.t), decimal=5 - # ) - # np.testing.assert_array_almost_equal( - # solution.y[-1], 2 * np.exp(0.1 * solution.t), decimal=5 - # ) - - # # Solve using "safe" mode with debug off - # pybamm.settings.debug_mode = False - # solver = pybamm.CasadiSolver(mode="safe", rtol=1e-8, atol=1e-8, dt_max=1) - # t_eval = np.linspace(0, 5, 100) - # solution = solver.solve(model, t_eval) - # np.testing.assert_array_less(solution.y[0], 1.5) - # np.testing.assert_array_less(solution.y[-1], 2.5 + 1e-10) - # np.testing.assert_array_almost_equal( - # solution.y[0], np.exp(0.1 * solution.t), decimal=5 - # ) - # np.testing.assert_array_almost_equal( - # solution.y[-1], 2 * np.exp(0.1 * solution.t), decimal=5 - # ) - # pybamm.settings.debug_mode = True - - # # Solve using "old safe" mode - # solver = pybamm.CasadiSolver(mode="old safe", rtol=1e-8, atol=1e-8) - # t_eval = np.linspace(0, 5, 100) - # solution = solver.solve(model, t_eval) - # np.testing.assert_array_less(solution.y[0], 1.5) - # np.testing.assert_array_less(solution.y[-1], 2.5) - # np.testing.assert_array_almost_equal( - # solution.y[0], np.exp(0.1 * solution.t), decimal=5 - # ) - # np.testing.assert_array_almost_equal( - # solution.y[-1], 2 * np.exp(0.1 * solution.t), decimal=5 - # ) - - # # Test when an event returns nan - # model = pybamm.BaseModel() - # var = pybamm.Variable("var") - # model.rhs = {var: 0.1 * var} - # model.initial_conditions = {var: 1} - # model.events = [ - # pybamm.Event("event", var - 1.02), - # pybamm.Event("sqrt event", pybamm.sqrt(1.0199 - var)), - # ] - # disc = pybamm.Discretisation() - # disc.process_model(model) - # solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) - # solution = solver.solve(model, t_eval) - # np.testing.assert_array_less(solution.y[0], 1.02 + 1e-10) - - # def test_model_step(self): - # # Create model - # model = pybamm.BaseModel() - # domain = ["negative electrode", "separator", "positive electrode"] - # var = pybamm.Variable("var", domain=domain) - # model.rhs = {var: 0.1 * var} - # model.initial_conditions = {var: 1} - # # No need to set parameters; can use base discretisation (no spatial operators) - - # # create discretisation - # mesh = get_mesh_for_testing() - # spatial_methods = {"macroscale": pybamm.FiniteVolume()} - # disc = pybamm.Discretisation(mesh, spatial_methods) - # disc.process_model(model) - - # solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) - - # # Step once - # dt = 1 - # step_sol = solver.step(None, model, dt) - # np.testing.assert_array_equal(step_sol.t, [0, dt]) - # np.testing.assert_array_almost_equal(step_sol.y[0], np.exp(0.1 * step_sol.t)) - - # # Step again (return 5 points) - # step_sol_2 = solver.step(step_sol, model, dt, npts=5) - # np.testing.assert_array_equal( - # step_sol_2.t, np.concatenate([np.array([0]), np.linspace(dt, 2 * dt, 5)]) - # ) - # np.testing.assert_array_almost_equal( - # step_sol_2.y[0], np.exp(0.1 * step_sol_2.t) - # ) - - # # Check steps give same solution as solve - # t_eval = step_sol.t - # solution = solver.solve(model, t_eval) - # np.testing.assert_array_almost_equal(solution.y[0], step_sol.y[0]) - - # def test_model_step_with_input(self): - # # Create model - # model = pybamm.BaseModel() - # var = pybamm.Variable("var") - # a = pybamm.InputParameter("a") - # model.rhs = {var: a * var} - # model.initial_conditions = {var: 1} - # model.variables = {"a": a} - # # No need to set parameters; can use base discretisation (no spatial operators) - - # # create discretisation - # disc = pybamm.Discretisation() - # disc.process_model(model) - - # solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) - - # # Step with an input - # dt = 0.1 - # step_sol = solver.step(None, model, dt, npts=5, inputs={"a": 0.1}) - # np.testing.assert_array_equal(step_sol.t, np.linspace(0, dt, 5)) - # np.testing.assert_allclose(step_sol.y[0], np.exp(0.1 * step_sol.t)) - - # # Step again with different inputs - # step_sol_2 = solver.step(step_sol, model, dt, npts=5, inputs={"a": -1}) - # np.testing.assert_array_equal(step_sol_2.t, np.linspace(0, 2 * dt, 9)) - # np.testing.assert_array_equal( - # step_sol_2["a"].entries, np.array([0.1, 0.1, 0.1, 0.1, 0.1, -1, -1, -1, -1]) - # ) - # np.testing.assert_allclose( - # step_sol_2.y[0], - # np.concatenate( - # [ - # np.exp(0.1 * step_sol.t[:5]), - # np.exp(0.1 * step_sol.t[4]) * np.exp(-(step_sol.t[5:] - dt)), - # ] - # ), - # ) - - # def test_model_step_events(self): - # # Create model - # model = pybamm.BaseModel() - # var1 = pybamm.Variable("var1") - # var2 = pybamm.Variable("var2") - # model.rhs = {var1: 0.1 * var1} - # model.algebraic = {var2: 2 * var1 - var2} - # model.initial_conditions = {var1: 1, var2: 2} - # model.events = [ - # pybamm.Event("var1 = 1.5", pybamm.min(var1 - 1.5)), - # pybamm.Event("var2 = 2.5", pybamm.min(var2 - 2.5)), - # ] - # disc = pybamm.Discretisation() - # disc.process_model(model) - - # # Solve - # step_solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) - # dt = 0.05 - # time = 0 - # end_time = 5 - # step_solution = None - # while time < end_time: - # step_solution = step_solver.step(step_solution, model, dt=dt, npts=10) - # time += dt - # np.testing.assert_array_less(step_solution.y[0], 1.5) - # np.testing.assert_array_less(step_solution.y[-1], 2.5001) - # np.testing.assert_array_almost_equal( - # step_solution.y[0], np.exp(0.1 * step_solution.t), decimal=5 - # ) - # np.testing.assert_array_almost_equal( - # step_solution.y[-1], 2 * np.exp(0.1 * step_solution.t), decimal=4 - # ) - - # def test_model_solver_with_inputs(self): - # # Create model - # model = pybamm.BaseModel() - # domain = ["negative electrode", "separator", "positive electrode"] - # var = pybamm.Variable("var", domain=domain) - # model.rhs = {var: -pybamm.InputParameter("rate") * var} - # model.initial_conditions = {var: 1} - # model.events = [pybamm.Event("var=0.5", pybamm.min(var - 0.5))] - # # No need to set parameters; can use base discretisation (no spatial - # # operators) - - # # create discretisation - # mesh = get_mesh_for_testing() - # spatial_methods = {"macroscale": pybamm.FiniteVolume()} - # disc = pybamm.Discretisation(mesh, spatial_methods) - # disc.process_model(model) - # # Solve - # solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) - # t_eval = np.linspace(0, 10, 100) - # solution = solver.solve(model, t_eval, inputs={"rate": 0.1}) - # self.assertLess(len(solution.t), len(t_eval)) - # np.testing.assert_allclose(solution.y[0], np.exp(-0.1 * solution.t), rtol=1e-04) - - # def test_model_solver_dae_inputs_in_initial_conditions(self): - # # Create model - # model = pybamm.BaseModel() - # var1 = pybamm.Variable("var1") - # var2 = pybamm.Variable("var2") - # model.rhs = {var1: pybamm.InputParameter("rate") * var1} - # model.algebraic = {var2: var1 - var2} - # model.initial_conditions = { - # var1: pybamm.InputParameter("ic 1"), - # var2: pybamm.InputParameter("ic 2"), - # } - - # # Solve - # solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) - # t_eval = np.linspace(0, 5, 100) - # solution = solver.solve( - # model, t_eval, inputs={"rate": -1, "ic 1": 0.1, "ic 2": 2} - # ) - # np.testing.assert_array_almost_equal( - # solution.y[0], 0.1 * np.exp(-solution.t), decimal=5 - # ) - # np.testing.assert_array_almost_equal( - # solution.y[-1], 0.1 * np.exp(-solution.t), decimal=5 - # ) - - # # Solve again with different initial conditions - # solution = solver.solve( - # model, t_eval, inputs={"rate": -0.1, "ic 1": 1, "ic 2": 3} - # ) - # np.testing.assert_array_almost_equal( - # solution.y[0], 1 * np.exp(-0.1 * solution.t), decimal=5 - # ) - # np.testing.assert_array_almost_equal( - # solution.y[-1], 1 * np.exp(-0.1 * solution.t), decimal=5 - # ) - - # def test_model_solver_with_external(self): - # # Create model - # model = pybamm.BaseModel() - # domain = ["negative electrode", "separator", "positive electrode"] - # var1 = pybamm.Variable("var1", domain=domain) - # var2 = pybamm.Variable("var2", domain=domain) - # model.rhs = {var1: -var2} - # model.initial_conditions = {var1: 1} - # model.external_variables = [var2] - # model.variables = {"var1": var1, "var2": var2} - # # No need to set parameters; can use base discretisation (no spatial - # # operators) - - # # create discretisation - # mesh = get_mesh_for_testing() - # spatial_methods = {"macroscale": pybamm.FiniteVolume()} - # disc = pybamm.Discretisation(mesh, spatial_methods) - # disc.process_model(model) - # # Solve - # solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) - # t_eval = np.linspace(0, 10, 100) - # solution = solver.solve(model, t_eval, external_variables={"var2": 0.5}) - # np.testing.assert_allclose(solution.y[0], 1 - 0.5 * solution.t, rtol=1e-06) - - # def test_model_solver_with_non_identity_mass(self): - # model = pybamm.BaseModel() - # var1 = pybamm.Variable("var1", domain="negative electrode") - # var2 = pybamm.Variable("var2", domain="negative electrode") - # model.rhs = {var1: var1} - # model.algebraic = {var2: 2 * var1 - var2} - # model.initial_conditions = {var1: 1, var2: 2} - # disc = get_discretisation_for_testing() - # disc.process_model(model) - - # # FV discretisation has identity mass. Manually set the mass matrix to - # # be a diag of 10s here for testing. Note that the algebraic part is all - # # zeros - # mass_matrix = 10 * model.mass_matrix.entries - # model.mass_matrix = pybamm.Matrix(mass_matrix) - - # # Note that mass_matrix_inv is just the inverse of the ode block of the - # # mass matrix - # mass_matrix_inv = 0.1 * eye(int(mass_matrix.shape[0] / 2)) - # model.mass_matrix_inv = pybamm.Matrix(mass_matrix_inv) - - # # Solve - # solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) - # t_eval = np.linspace(0, 1, 100) - # solution = solver.solve(model, t_eval) - # np.testing.assert_array_equal(solution.t, t_eval) - # np.testing.assert_allclose(solution.y[0], np.exp(0.1 * solution.t)) - # np.testing.assert_allclose(solution.y[-1], 2 * np.exp(0.1 * solution.t)) - - # def test_dae_solver_algebraic_model(self): - # model = pybamm.BaseModel() - # var = pybamm.Variable("var") - # model.algebraic = {var: var + 1} - # model.initial_conditions = {var: 0} - - # disc = pybamm.Discretisation() - # disc.process_model(model) - - # solver = pybamm.CasadiSolver() - # t_eval = np.linspace(0, 1) - # with self.assertRaisesRegex( - # pybamm.SolverError, "Cannot use CasadiSolver to solve algebraic model" - # ): - # solver.solve(model, t_eval) + def test_model_solver_python(self): + # Create model + pybamm.set_logging_level("ERROR") + model = pybamm.BaseModel() + model.convert_to_format = "python" + var = pybamm.Variable("var") + model.rhs = {var: 0.1 * var} + model.initial_conditions = {var: 1} + # No need to set parameters; can use base discretisation (no spatial operators) + + # create discretisation + disc = pybamm.Discretisation() + disc.process_model(model) + # Solve + solver = pybamm.CasadiSolver(mode="fast", rtol=1e-8, atol=1e-8) + t_eval = np.linspace(0, 1, 100) + solution = solver.solve(model, t_eval) + np.testing.assert_array_equal(solution.t, t_eval) + np.testing.assert_array_almost_equal( + solution.y[0], np.exp(0.1 * solution.t), decimal=5 + ) + pybamm.set_logging_level("WARNING") + + def test_model_solver_failure(self): + # Create model + model = pybamm.BaseModel() + var = pybamm.Variable("var") + model.rhs = {var: -pybamm.sqrt(var)} + model.initial_conditions = {var: 1} + # add events so that safe mode is used (won't be triggered) + model.events = [pybamm.Event("10", var - 10)] + # No need to set parameters; can use base discretisation (no spatial operators) + + # create discretisation + disc = pybamm.Discretisation() + model_disc = disc.process_model(model, inplace=False) + + solver = pybamm.CasadiSolver(extra_options_call={"regularity_check": False}) + solver_old = pybamm.CasadiSolver( + mode="old safe", extra_options_call={"regularity_check": False} + ) + # Solve with failure at t=2 + t_eval = np.linspace(0, 20, 100) + with self.assertRaises(pybamm.SolverError): + solver.solve(model_disc, t_eval) + with self.assertRaises(pybamm.SolverError): + solver_old.solve(model_disc, t_eval) + # Solve with failure at t=0 + model.initial_conditions = {var: 0} + model_disc = disc.process_model(model, inplace=False) + t_eval = np.linspace(0, 20, 100) + with self.assertRaises(pybamm.SolverError): + solver.solve(model_disc, t_eval) + + def test_model_solver_events(self): + # Create model + model = pybamm.BaseModel() + whole_cell = ["negative electrode", "separator", "positive electrode"] + var1 = pybamm.Variable("var1", domain=whole_cell) + var2 = pybamm.Variable("var2", domain=whole_cell) + model.rhs = {var1: 0.1 * var1} + model.algebraic = {var2: 2 * var1 - var2} + model.initial_conditions = {var1: 1, var2: 2} + model.events = [ + pybamm.Event("var1 = 1.5", pybamm.min(var1 - 1.5)), + pybamm.Event("var2 = 2.5", pybamm.min(var2 - 2.5)), + ] + disc = get_discretisation_for_testing() + disc.process_model(model) + + # Solve using "safe" mode + solver = pybamm.CasadiSolver(mode="safe", rtol=1e-8, atol=1e-8) + t_eval = np.linspace(0, 5, 100) + solution = solver.solve(model, t_eval) + np.testing.assert_array_less(solution.y[0], 1.5) + np.testing.assert_array_less(solution.y[-1], 2.5 + 1e-10) + np.testing.assert_array_almost_equal( + solution.y[0], np.exp(0.1 * solution.t), decimal=5 + ) + np.testing.assert_array_almost_equal( + solution.y[-1], 2 * np.exp(0.1 * solution.t), decimal=5 + ) + + # Solve using "safe" mode with debug off + pybamm.settings.debug_mode = False + solver = pybamm.CasadiSolver(mode="safe", rtol=1e-8, atol=1e-8, dt_max=1) + t_eval = np.linspace(0, 5, 100) + solution = solver.solve(model, t_eval) + np.testing.assert_array_less(solution.y[0], 1.5) + np.testing.assert_array_less(solution.y[-1], 2.5 + 1e-10) + # test the last entry is exactly 2.5 + np.testing.assert_array_equal(solution.y[-1, -1], 2.5) + np.testing.assert_array_almost_equal( + solution.y[0], np.exp(0.1 * solution.t), decimal=5 + ) + np.testing.assert_array_almost_equal( + solution.y[-1], 2 * np.exp(0.1 * solution.t), decimal=5 + ) + pybamm.settings.debug_mode = True + + # Solve using "old safe" mode + solver = pybamm.CasadiSolver(mode="old safe", rtol=1e-8, atol=1e-8) + t_eval = np.linspace(0, 5, 100) + solution = solver.solve(model, t_eval) + np.testing.assert_array_less(solution.y[0], 1.5) + np.testing.assert_array_less(solution.y[-1], 2.5) + np.testing.assert_array_almost_equal( + solution.y[0], np.exp(0.1 * solution.t), decimal=5 + ) + np.testing.assert_array_almost_equal( + solution.y[-1], 2 * np.exp(0.1 * solution.t), decimal=5 + ) + + # Test when an event returns nan + model = pybamm.BaseModel() + var = pybamm.Variable("var") + model.rhs = {var: 0.1 * var} + model.initial_conditions = {var: 1} + model.events = [ + pybamm.Event("event", var - 1.02), + pybamm.Event("sqrt event", pybamm.sqrt(1.0199 - var)), + ] + disc = pybamm.Discretisation() + disc.process_model(model) + solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) + solution = solver.solve(model, t_eval) + np.testing.assert_array_less(solution.y[0], 1.02 + 1e-10) + np.testing.assert_array_equal(solution.y[0, -1], 1.02) + + def test_model_step(self): + # Create model + model = pybamm.BaseModel() + domain = ["negative electrode", "separator", "positive electrode"] + var = pybamm.Variable("var", domain=domain) + model.rhs = {var: 0.1 * var} + model.initial_conditions = {var: 1} + # No need to set parameters; can use base discretisation (no spatial operators) + + # create discretisation + mesh = get_mesh_for_testing() + spatial_methods = {"macroscale": pybamm.FiniteVolume()} + disc = pybamm.Discretisation(mesh, spatial_methods) + disc.process_model(model) + + solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) + + # Step once + dt = 1 + step_sol = solver.step(None, model, dt) + np.testing.assert_array_equal(step_sol.t, [0, dt]) + np.testing.assert_array_almost_equal(step_sol.y[0], np.exp(0.1 * step_sol.t)) + + # Step again (return 5 points) + step_sol_2 = solver.step(step_sol, model, dt, npts=5) + np.testing.assert_array_equal( + step_sol_2.t, np.concatenate([np.array([0]), np.linspace(dt, 2 * dt, 5)]) + ) + np.testing.assert_array_almost_equal( + step_sol_2.y[0], np.exp(0.1 * step_sol_2.t) + ) + + # Check steps give same solution as solve + t_eval = step_sol.t + solution = solver.solve(model, t_eval) + np.testing.assert_array_almost_equal(solution.y[0], step_sol.y[0]) + + def test_model_step_with_input(self): + # Create model + model = pybamm.BaseModel() + var = pybamm.Variable("var") + a = pybamm.InputParameter("a") + model.rhs = {var: a * var} + model.initial_conditions = {var: 1} + model.variables = {"a": a} + # No need to set parameters; can use base discretisation (no spatial operators) + + # create discretisation + disc = pybamm.Discretisation() + disc.process_model(model) + + solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) + + # Step with an input + dt = 0.1 + step_sol = solver.step(None, model, dt, npts=5, inputs={"a": 0.1}) + np.testing.assert_array_equal(step_sol.t, np.linspace(0, dt, 5)) + np.testing.assert_allclose(step_sol.y[0], np.exp(0.1 * step_sol.t)) + + # Step again with different inputs + step_sol_2 = solver.step(step_sol, model, dt, npts=5, inputs={"a": -1}) + np.testing.assert_array_equal(step_sol_2.t, np.linspace(0, 2 * dt, 9)) + np.testing.assert_array_equal( + step_sol_2["a"].entries, np.array([0.1, 0.1, 0.1, 0.1, 0.1, -1, -1, -1, -1]) + ) + np.testing.assert_allclose( + step_sol_2.y[0], + np.concatenate( + [ + np.exp(0.1 * step_sol.t[:5]), + np.exp(0.1 * step_sol.t[4]) * np.exp(-(step_sol.t[5:] - dt)), + ] + ), + ) + + def test_model_step_events(self): + # Create model + model = pybamm.BaseModel() + var1 = pybamm.Variable("var1") + var2 = pybamm.Variable("var2") + model.rhs = {var1: 0.1 * var1} + model.algebraic = {var2: 2 * var1 - var2} + model.initial_conditions = {var1: 1, var2: 2} + model.events = [ + pybamm.Event("var1 = 1.5", pybamm.min(var1 - 1.5)), + pybamm.Event("var2 = 2.5", pybamm.min(var2 - 2.5)), + ] + disc = pybamm.Discretisation() + disc.process_model(model) + + # Solve + step_solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) + dt = 0.05 + time = 0 + end_time = 5 + step_solution = None + while time < end_time: + step_solution = step_solver.step(step_solution, model, dt=dt, npts=10) + time += dt + np.testing.assert_array_less(step_solution.y[0], 1.5) + np.testing.assert_array_less(step_solution.y[-1], 2.5001) + np.testing.assert_array_almost_equal( + step_solution.y[0], np.exp(0.1 * step_solution.t), decimal=5 + ) + np.testing.assert_array_almost_equal( + step_solution.y[-1], 2 * np.exp(0.1 * step_solution.t), decimal=4 + ) + + def test_model_solver_with_inputs(self): + # Create model + model = pybamm.BaseModel() + domain = ["negative electrode", "separator", "positive electrode"] + var = pybamm.Variable("var", domain=domain) + model.rhs = {var: -pybamm.InputParameter("rate") * var} + model.initial_conditions = {var: 1} + model.events = [pybamm.Event("var=0.5", pybamm.min(var - 0.5))] + # No need to set parameters; can use base discretisation (no spatial + # operators) + + # create discretisation + mesh = get_mesh_for_testing() + spatial_methods = {"macroscale": pybamm.FiniteVolume()} + disc = pybamm.Discretisation(mesh, spatial_methods) + disc.process_model(model) + # Solve + solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) + t_eval = np.linspace(0, 10, 100) + solution = solver.solve(model, t_eval, inputs={"rate": 0.1}) + self.assertLess(len(solution.t), len(t_eval)) + np.testing.assert_allclose(solution.y[0], np.exp(-0.1 * solution.t), rtol=1e-04) + + def test_model_solver_dae_inputs_in_initial_conditions(self): + # Create model + model = pybamm.BaseModel() + var1 = pybamm.Variable("var1") + var2 = pybamm.Variable("var2") + model.rhs = {var1: pybamm.InputParameter("rate") * var1} + model.algebraic = {var2: var1 - var2} + model.initial_conditions = { + var1: pybamm.InputParameter("ic 1"), + var2: pybamm.InputParameter("ic 2"), + } + + # Solve + solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) + t_eval = np.linspace(0, 5, 100) + solution = solver.solve( + model, t_eval, inputs={"rate": -1, "ic 1": 0.1, "ic 2": 2} + ) + np.testing.assert_array_almost_equal( + solution.y[0], 0.1 * np.exp(-solution.t), decimal=5 + ) + np.testing.assert_array_almost_equal( + solution.y[-1], 0.1 * np.exp(-solution.t), decimal=5 + ) + + # Solve again with different initial conditions + solution = solver.solve( + model, t_eval, inputs={"rate": -0.1, "ic 1": 1, "ic 2": 3} + ) + np.testing.assert_array_almost_equal( + solution.y[0], 1 * np.exp(-0.1 * solution.t), decimal=5 + ) + np.testing.assert_array_almost_equal( + solution.y[-1], 1 * np.exp(-0.1 * solution.t), decimal=5 + ) + + def test_model_solver_with_external(self): + # Create model + model = pybamm.BaseModel() + domain = ["negative electrode", "separator", "positive electrode"] + var1 = pybamm.Variable("var1", domain=domain) + var2 = pybamm.Variable("var2", domain=domain) + model.rhs = {var1: -var2} + model.initial_conditions = {var1: 1} + model.external_variables = [var2] + model.variables = {"var1": var1, "var2": var2} + # No need to set parameters; can use base discretisation (no spatial + # operators) + + # create discretisation + mesh = get_mesh_for_testing() + spatial_methods = {"macroscale": pybamm.FiniteVolume()} + disc = pybamm.Discretisation(mesh, spatial_methods) + disc.process_model(model) + # Solve + solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) + t_eval = np.linspace(0, 10, 100) + solution = solver.solve(model, t_eval, external_variables={"var2": 0.5}) + np.testing.assert_allclose(solution.y[0], 1 - 0.5 * solution.t, rtol=1e-06) + + def test_model_solver_with_non_identity_mass(self): + model = pybamm.BaseModel() + var1 = pybamm.Variable("var1", domain="negative electrode") + var2 = pybamm.Variable("var2", domain="negative electrode") + model.rhs = {var1: var1} + model.algebraic = {var2: 2 * var1 - var2} + model.initial_conditions = {var1: 1, var2: 2} + disc = get_discretisation_for_testing() + disc.process_model(model) + + # FV discretisation has identity mass. Manually set the mass matrix to + # be a diag of 10s here for testing. Note that the algebraic part is all + # zeros + mass_matrix = 10 * model.mass_matrix.entries + model.mass_matrix = pybamm.Matrix(mass_matrix) + + # Note that mass_matrix_inv is just the inverse of the ode block of the + # mass matrix + mass_matrix_inv = 0.1 * eye(int(mass_matrix.shape[0] / 2)) + model.mass_matrix_inv = pybamm.Matrix(mass_matrix_inv) + + # Solve + solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) + t_eval = np.linspace(0, 1, 100) + solution = solver.solve(model, t_eval) + np.testing.assert_array_equal(solution.t, t_eval) + np.testing.assert_allclose(solution.y[0], np.exp(0.1 * solution.t)) + np.testing.assert_allclose(solution.y[-1], 2 * np.exp(0.1 * solution.t)) + + def test_dae_solver_algebraic_model(self): + model = pybamm.BaseModel() + var = pybamm.Variable("var") + model.algebraic = {var: var + 1} + model.initial_conditions = {var: 0} + + disc = pybamm.Discretisation() + disc.process_model(model) + + solver = pybamm.CasadiSolver() + t_eval = np.linspace(0, 1) + with self.assertRaisesRegex( + pybamm.SolverError, "Cannot use CasadiSolver to solve algebraic model" + ): + solver.solve(model, t_eval) if __name__ == "__main__": From af4062dcfb89887c17d73605fccfdc00c1590ada Mon Sep 17 00:00:00 2001 From: Valentin Sulzer Date: Sun, 28 Jun 2020 17:55:19 -0400 Subject: [PATCH 3/8] #1082 remove test.py --- test.py | 128 -------------------------------------------------------- 1 file changed, 128 deletions(-) delete mode 100644 test.py diff --git a/test.py b/test.py deleted file mode 100644 index 4947954e5c..0000000000 --- a/test.py +++ /dev/null @@ -1,128 +0,0 @@ -from casadi import * -import time - -# Define ode -t = MX.sym("t") -x = MX.sym("x") -p = MX.sym("p") - -x0 = np.ones(x.shape[0]) -t_eval = np.linspace(0, 1, 100) - -t_max = MX.sym("t_min") -t_min = MX.sym("t_max") -tlims = casadi.vertcat(t_min, t_max) - -ode = -(t_max - t_min) * p * x - -# value of the parameter for evaluating -p_eval = 1 - -# First approach: simple integrator without a grid -# fastest but doesn't give the intermediate points -print("no grid") -print("*" * 10) -itg_nogrid = integrator( - "F", "cvodes", {"t": t, "x": x, "ode": ode, "p": casadi.vertcat(p, tlims)} -) - -start = time.time() -itg_nogrid(x0=1, p=[p_eval, 0, 1]) -print("value:", time.time() - start) - -jac_nogrid = Function( - "j", [p], [jacobian(itg_nogrid(x0=x0, p=casadi.vertcat(p, 0, 1))["xf"], p)] -) -start = time.time() -jac_nogrid(1) -print("jacobian:", time.time() - start) - -# Second approach: integrator with a grid -# fast, gives intermediate points, but can't take the jacobian -print("With grid") -print("*" * 10) -itg_grid_auto = integrator( - "F", - "cvodes", - {"t": t, "x": x, "ode": -p * x, "p": p}, - {"grid": t_eval, "output_t0": True}, -) - -start = time.time() -itg_grid_auto(x0=1, p=p_eval) -print("value:", time.time() - start) - -# Fails -rep_p = repmat(p, 1, len(t_eval)) -jac_grid_auto = Function("j", [p], [itg_grid_auto(x0=x0, p=p * DM.ones(100, 1))["xf"]]) -jac_grid_auto(1) -# print("jacobian: fails") - -# Third approach: multiple calls through manual for loop -print("For loop") -print("*" * 10) - - -def itg_grid_manual(x0, p_eval, t_eval): - X = x0 - for i in range(t_eval.shape[0] - 1): - t_min = t_eval[i] - t_max = t_eval[i + 1] - xnew = itg_nogrid(x0=x0, p=casadi.vertcat(p_eval, t_min, t_max))["xf"] - X = casadi.horzcat(X, xnew) - x0 = xnew - return X - - -start = time.time() -itg_grid_manual(x0, p_eval, t_eval) -print("value:", time.time() - start) - -jac_grid_manual = Function("j", [p], [jacobian(itg_grid_manual(x0, p, t_eval), p)],) -start = time.time() -jac_grid_manual(1) -print("jacobian:", time.time() - start) - - -# Fourth approach: multiple calls through mapaccum -print("mapaccum") -print("*" * 10) - -x0_sym = MX.sym("x0", x.shape[0]) -itg_fn = Function( - "itg_fn", - [x0_sym, p, tlims], - [itg_nogrid(x0=x0_sym, p=casadi.vertcat(p, tlims))["xf"]], -) -itg_grid_mapaccum = itg_fn.mapaccum("Fn", len(t_eval) - 1) - -tlims_eval = casadi.horzcat(t_eval[:-1], t_eval[1:]).T - -start = time.time() -itg_grid_mapaccum(x0, p_eval, tlims_eval) -print("value:", time.time() - start) - -# Jacobians -jac_grid_mapaccum = Function( - "j", [p], [jacobian(itg_grid_mapaccum(x0, p, tlims_eval), p)] -) -start = time.time() -jac_grid_mapaccum(1) -print("jacobian:", time.time() - start) - -# -# -# -# Creating vs using integrator -# print("no grid") -# print("*" * 10) -# start = time.time() -# itg_nogrid = integrator( -# "F", "cvodes", {"t": t, "x": x, "ode": ode, "p": casadi.vertcat(p, tlims)} -# ) -# print(time.time() - start) - -# start = time.time() -# itg_nogrid(x0=1, p=[p_eval, 0, 1]) -# print(time.time() - start) - From ed0ac73da700034f9a893482d325e96f4e14de4f Mon Sep 17 00:00:00 2001 From: Valentin Sulzer Date: Mon, 29 Jun 2020 15:38:54 -0400 Subject: [PATCH 4/8] #1082 fix integration tests --- examples/scripts/compare_lithium_ion.py | 2 +- pybamm/solvers/casadi_solver.py | 27 ++++++++----------- tests/unit/test_solvers/test_casadi_solver.py | 4 +-- 3 files changed, 14 insertions(+), 19 deletions(-) diff --git a/examples/scripts/compare_lithium_ion.py b/examples/scripts/compare_lithium_ion.py index 2644511824..c0ef3cd3ba 100644 --- a/examples/scripts/compare_lithium_ion.py +++ b/examples/scripts/compare_lithium_ion.py @@ -3,7 +3,7 @@ # import pybamm -# pybamm.set_logging_level("INFO") +pybamm.set_logging_level("INFO") # load models models = [ diff --git a/pybamm/solvers/casadi_solver.py b/pybamm/solvers/casadi_solver.py index 40c9646f2b..602edf9e58 100644 --- a/pybamm/solvers/casadi_solver.py +++ b/pybamm/solvers/casadi_solver.py @@ -186,12 +186,9 @@ def _integrate(self, model, t_eval, inputs=None): count += 1 if count >= self.max_step_decrease_count: raise pybamm.SolverError( - """ - Maximum number of decreased steps occurred at t={}. Try - solving the model up to this time only or reducing dt_max. - """.format( - t - ) + "Maximum number of decreased steps occurred at t={}. Try " + "solving the model up to this time only or reducing dt_max." + "".format(t) ) # Check most recent y to see if any events have been crossed new_event_signs = np.sign( @@ -246,19 +243,17 @@ def event_fun(t): t_event = np.nanmin(t_events) y_event = y_sol(t_event) - # return truncated solution - t_truncated = current_step_sol.t[current_step_sol.t < t_event] - y_truncated = current_step_sol.y[:, 0 : len(t_truncated)] - # add the event to the truncated solution - t_truncated = np.concatenate([t_truncated, np.array([t_event])]) - y_truncated = np.concatenate( - [y_truncated, y_event[:, np.newaxis]], axis=1 + # solve again until the event time + t_window = np.array([t, t_event]) + integrator = self.get_integrator(model, t_window, inputs) + current_step_sol = self._run_integrator( + integrator, model, y0, inputs, t_window ) - truncated_step_sol = pybamm.Solution(t_truncated, y_truncated) + # assign temporary solve time - truncated_step_sol.solve_time = np.nan + current_step_sol.solve_time = np.nan # append solution from the current step to solution - solution.append(truncated_step_sol) + solution.append(current_step_sol) solution.termination = "event" solution.t_event = t_event solution.y_event = y_event diff --git a/tests/unit/test_solvers/test_casadi_solver.py b/tests/unit/test_solvers/test_casadi_solver.py index 54b63380cd..776182f45b 100644 --- a/tests/unit/test_solvers/test_casadi_solver.py +++ b/tests/unit/test_solvers/test_casadi_solver.py @@ -135,7 +135,7 @@ def test_model_solver_events(self): np.testing.assert_array_less(solution.y[0], 1.5) np.testing.assert_array_less(solution.y[-1], 2.5 + 1e-10) # test the last entry is exactly 2.5 - np.testing.assert_array_equal(solution.y[-1, -1], 2.5) + np.testing.assert_array_almost_equal(solution.y[-1, -1], 2.5, decimal=5) np.testing.assert_array_almost_equal( solution.y[0], np.exp(0.1 * solution.t), decimal=5 ) @@ -171,7 +171,7 @@ def test_model_solver_events(self): solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) solution = solver.solve(model, t_eval) np.testing.assert_array_less(solution.y[0], 1.02 + 1e-10) - np.testing.assert_array_equal(solution.y[0, -1], 1.02) + np.testing.assert_array_almost_equal(solution.y[0, -1], 1.02) def test_model_step(self): # Create model From 92678f35a1d68c19d1e4ff9390b1255406da0df9 Mon Sep 17 00:00:00 2001 From: Valentin Sulzer Date: Mon, 29 Jun 2020 16:14:31 -0400 Subject: [PATCH 5/8] #1082 fix another integration test --- pybamm/solvers/casadi_solver.py | 12 ++++++++++-- .../test_models/standard_output_comparison.py | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/pybamm/solvers/casadi_solver.py b/pybamm/solvers/casadi_solver.py index 602edf9e58..96cb48ee51 100644 --- a/pybamm/solvers/casadi_solver.py +++ b/pybamm/solvers/casadi_solver.py @@ -151,7 +151,9 @@ def _integrate(self, model, t_eval, inputs=None): # Non-dimensionalise provided dt_max dt_max = self.dt_max / model.timescale_eval else: - dt_max = 0.01 * min(model.timescale_eval, t_f) / model.timescale_eval + # t_f is the dimensionless final time (scaled with the timescale) + # Keeping a safe factor of 0.01 but could potentially be bigger + dt_max = 0.01 * min(1, t_f) dt_eval_max = np.max(np.diff(t_eval)) * 1.01 dt_max = np.max([dt_max, dt_eval_max]) while t < t_f: @@ -244,7 +246,13 @@ def event_fun(t): y_event = y_sol(t_event) # solve again until the event time - t_window = np.array([t, t_event]) + # See comments above on creating t_window + t_window = np.concatenate( + ([t], t_eval[(t_eval > t) & (t_eval < t_event)]) + ) + if len(t_window) == 1: + t_window = np.array([t, t_event]) + integrator = self.get_integrator(model, t_window, inputs) current_step_sol = self._run_integrator( integrator, model, y0, inputs, t_window diff --git a/tests/integration/test_models/standard_output_comparison.py b/tests/integration/test_models/standard_output_comparison.py index 0695861cde..52dd172403 100644 --- a/tests/integration/test_models/standard_output_comparison.py +++ b/tests/integration/test_models/standard_output_comparison.py @@ -25,7 +25,7 @@ def get_output_times(self): # Assign common time solution0 = self.solutions[0] - max_index = np.where(solution0.t == max_t)[0][0] + max_index = np.where(solution0.t >= max_t)[0][0] t_common = solution0.t[:max_index] # Check times From 952dc3e357db783f4ab85d14ad61805bc886a7fd Mon Sep 17 00:00:00 2001 From: Valentin Sulzer Date: Mon, 29 Jun 2020 19:23:58 -0400 Subject: [PATCH 6/8] #1082 loosen tols --- tests/unit/test_solvers/test_casadi_solver.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_solvers/test_casadi_solver.py b/tests/unit/test_solvers/test_casadi_solver.py index 776182f45b..ba3376e583 100644 --- a/tests/unit/test_solvers/test_casadi_solver.py +++ b/tests/unit/test_solvers/test_casadi_solver.py @@ -135,7 +135,7 @@ def test_model_solver_events(self): np.testing.assert_array_less(solution.y[0], 1.5) np.testing.assert_array_less(solution.y[-1], 2.5 + 1e-10) # test the last entry is exactly 2.5 - np.testing.assert_array_almost_equal(solution.y[-1, -1], 2.5, decimal=5) + np.testing.assert_array_almost_equal(solution.y[-1, -1], 2.5, decimal=2) np.testing.assert_array_almost_equal( solution.y[0], np.exp(0.1 * solution.t), decimal=5 ) @@ -171,7 +171,7 @@ def test_model_solver_events(self): solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) solution = solver.solve(model, t_eval) np.testing.assert_array_less(solution.y[0], 1.02 + 1e-10) - np.testing.assert_array_almost_equal(solution.y[0, -1], 1.02) + np.testing.assert_array_almost_equal(solution.y[0, -1], 1.02, decimal=2) def test_model_step(self): # Create model From 066b88fcc566c4cfbc77e43172af8a40164bc627 Mon Sep 17 00:00:00 2001 From: Valentin Sulzer Date: Tue, 30 Jun 2020 09:40:19 -0400 Subject: [PATCH 7/8] #1082 changelog --- CHANGELOG.md | 4 +++- pybamm/solvers/casadi_solver.py | 3 +-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76f645cea6..7f210c13cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## Features -- Reformatted Getting Starte notebooks ([#1083](https://github.com/pybamm-team/PyBaMM/pull/1083)) +- Reformatted Getting Started notebooks ([#1083](https://github.com/pybamm-team/PyBaMM/pull/1083)) - Reformatted Landesfeind electrolytes ([#1064](https://github.com/pybamm-team/PyBaMM/pull/1064)) - Adapted examples to be run in Google Colab ([#1061](https://github.com/pybamm-team/PyBaMM/pull/1061)) - Added some new solvers for algebraic models ([#1059](https://github.com/pybamm-team/PyBaMM/pull/1059)) @@ -12,6 +12,8 @@ ## Optimizations +- Reformatted CasADi "safe" mode to deal with events better ([#1089](https://github.com/pybamm-team/PyBaMM/pull/1089)) + ## Bug fixes - 2D processed variables can now be evaluated at the domain boundaries ([#1088](https://github.com/pybamm-team/PyBaMM/pull/1088)) diff --git a/pybamm/solvers/casadi_solver.py b/pybamm/solvers/casadi_solver.py index 96cb48ee51..03b515bb50 100644 --- a/pybamm/solvers/casadi_solver.py +++ b/pybamm/solvers/casadi_solver.py @@ -152,8 +152,7 @@ def _integrate(self, model, t_eval, inputs=None): dt_max = self.dt_max / model.timescale_eval else: # t_f is the dimensionless final time (scaled with the timescale) - # Keeping a safe factor of 0.01 but could potentially be bigger - dt_max = 0.01 * min(1, t_f) + dt_max = 0.1 * min(1, t_f) dt_eval_max = np.max(np.diff(t_eval)) * 1.01 dt_max = np.max([dt_max, dt_eval_max]) while t < t_f: From ce4ca8126b7a162096f8f405afde7f912fdce52e Mon Sep 17 00:00:00 2001 From: Valentin Sulzer Date: Tue, 30 Jun 2020 10:18:50 -0400 Subject: [PATCH 8/8] #1082 print dimensional t and dt_max --- pybamm/solvers/casadi_solver.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pybamm/solvers/casadi_solver.py b/pybamm/solvers/casadi_solver.py index 03b515bb50..9cd654c20a 100644 --- a/pybamm/solvers/casadi_solver.py +++ b/pybamm/solvers/casadi_solver.py @@ -188,8 +188,11 @@ def _integrate(self, model, t_eval, inputs=None): if count >= self.max_step_decrease_count: raise pybamm.SolverError( "Maximum number of decreased steps occurred at t={}. Try " - "solving the model up to this time only or reducing dt_max." - "".format(t) + "solving the model up to this time only or reducing dt_max " + "(currently, dt_max={})." + "".format( + t * model.timescale_eval, dt_max * model.timescale_eval + ) ) # Check most recent y to see if any events have been crossed new_event_signs = np.sign(