예제 #1
0
    def test_model_solver_dae_events_casadi(self):
        # Create model
        model = pybamm.BaseModel()
        for use_jacobian in [True, False]:
            model.use_jacobian = use_jacobian
            model.convert_to_format = "casadi"
            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()
            model_disc = disc.process_model(model, inplace=False)

            # Solve
            solver = pybamm.ScikitsDaeSolver(rtol=1e-8, atol=1e-8)
            t_eval = np.linspace(0, 5, 100)
            solution = solver.solve(model_disc, 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_allclose(solution.y[0], np.exp(0.1 * solution.t))
            np.testing.assert_allclose(solution.y[-1],
                                       2 * np.exp(0.1 * solution.t))
예제 #2
0
    def test_model_solver_dae_events_python(self):
        model = pybamm.BaseModel()
        model.convert_to_format = "python"
        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
        solver = pybamm.ScikitsDaeSolver(rtol=1e-8,
                                         atol=1e-8,
                                         root_method="lm")
        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_allclose(solution.y[0], np.exp(0.1 * solution.t))
        np.testing.assert_allclose(solution.y[-1],
                                   2 * np.exp(0.1 * solution.t))
예제 #3
0
    def test_model_solver_ode_events_casadi(self):
        # Create model
        model = pybamm.BaseModel()
        model.convert_to_format = "casadi"
        whole_cell = ["negative electrode", "separator", "positive electrode"]
        var = pybamm.Variable("var", domain=whole_cell)
        model.rhs = {var: 0.1 * var}
        model.initial_conditions = {var: 1}
        model.events = [
            pybamm.Event("2 * var = 2.5", pybamm.min(2 * var - 2.5)),
            pybamm.Event("var = 1.5", pybamm.min(var - 1.5)),
        ]
        disc = get_discretisation_for_testing()
        disc.process_model(model)

        # Solve
        solver = pybamm.ScikitsOdeSolver(rtol=1e-9, atol=1e-9)
        t_eval = np.linspace(0, 10, 100)
        solution = solver.solve(model, t_eval)
        np.testing.assert_allclose(solution.y[0], np.exp(0.1 * solution.t))
        np.testing.assert_array_less(solution.y[0:, -1], 1.5)
        np.testing.assert_array_less(solution.y[0:, -1], 1.25 + 1e-6)
        np.testing.assert_equal(solution.t_event[0], solution.t[-1])
        np.testing.assert_array_equal(solution.y_event[:, 0], solution.y[:,
                                                                         -1])
예제 #4
0
    def test_model_solver_with_event_python(self):
        # Create model
        model = pybamm.BaseModel()
        model.convert_to_format = "python"
        domain = ["negative electrode", "separator", "positive electrode"]
        var = pybamm.Variable("var", domain=domain)
        model.rhs = {var: -0.1 * var}
        model.initial_conditions = {var: 1}
        # needs to work with multiple events (to avoid bug where only last event is
        # used)
        model.events = [
            pybamm.Event("var=0.5", pybamm.min(var - 0.5)),
            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.ScipySolver(rtol=1e-8, atol=1e-8, method="RK45")
        t_eval = np.linspace(0, 10, 100)
        solution = solver.solve(model, t_eval)
        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))
예제 #5
0
    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.ScikitsDaeSolver(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=5)
예제 #6
0
    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
        solver = pybamm.CasadiSolver(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)
예제 #7
0
    def test_solver_doesnt_support_events(self):
        # Create model
        model = pybamm.BaseModel()
        model.convert_to_format = "jax"
        domain = ["negative electrode", "separator", "positive electrode"]
        var = pybamm.Variable("var", domain=domain)
        model.rhs = {var: -0.1 * var}
        model.initial_conditions = {var: 1}
        # needs to work with multiple events (to avoid bug where only last event is
        # used)
        model.events = [
            pybamm.Event("var=0.5", pybamm.min(var - 0.5)),
            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.JaxSolver()
        t_eval = np.linspace(0, 10, 100)
        with self.assertRaisesRegex(RuntimeError,
                                    "Terminate events not supported"):
            solver.solve(model, t_eval)
예제 #8
0
    def test_model_solver_dae_inputs_events(self):
        # Create model
        for form in ["python", "casadi"]:
            model = pybamm.BaseModel()
            model.convert_to_format = form
            whole_cell = ["negative electrode", "separator", "positive electrode"]
            var1 = pybamm.Variable("var1", domain=whole_cell)
            var2 = pybamm.Variable("var2", domain=whole_cell)
            model.rhs = {var1: pybamm.InputParameter("rate 1") * var1}
            model.algebraic = {var2: pybamm.InputParameter("rate 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
            if form == "python":
                solver = pybamm.ScikitsDaeSolver(rtol=1e-8, atol=1e-8, root_method="lm")
            else:
                solver = pybamm.ScikitsDaeSolver(rtol=1e-8, atol=1e-8)
            t_eval = np.linspace(0, 5, 100)
            solution = solver.solve(model, t_eval, inputs={"rate 1": 0.1, "rate 2": 2})
            np.testing.assert_array_less(solution.y[0], 1.5)
            np.testing.assert_array_less(solution.y[-1], 2.5)
            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))
예제 #9
0
    def set_events(self, variables):
        eps_n = variables["Negative electrode porosity"]
        eps_p = variables["Positive electrode porosity"]
        self.events.append(
            pybamm.Event(
                "Zero negative electrode porosity cut-off",
                pybamm.min(eps_n),
                pybamm.EventType.TERMINATION,
            ))
        self.events.append(
            pybamm.Event(
                "Max negative electrode porosity cut-off",
                pybamm.max(eps_n) - 1,
                pybamm.EventType.TERMINATION,
            ))

        self.events.append(
            pybamm.Event(
                "Zero positive electrode porosity cut-off",
                pybamm.min(eps_p),
                pybamm.EventType.TERMINATION,
            ))

        self.events.append(
            pybamm.Event(
                "Max positive electrode porosity cut-off",
                pybamm.max(eps_p) - 1,
                pybamm.EventType.TERMINATION,
            ))
예제 #10
0
    def test_model_step_nonsmooth_events(self):
        # Create model
        model = pybamm.BaseModel()
        model.timescale = pybamm.Scalar(1)
        var1 = pybamm.Variable("var1")
        var2 = pybamm.Variable("var2")
        a = 0.6
        discontinuities = (np.arange(3) + 1) * a

        model.rhs = {var1: pybamm.Modulo(pybamm.t * model.timescale, a)}
        model.algebraic = {var2: 2 * var1 - var2}
        model.initial_conditions = {var1: 0, var2: 0}
        model.events = [
            pybamm.Event("var1 = 0.55", pybamm.min(var1 - 0.55)),
            pybamm.Event("var2 = 1.2", pybamm.min(var2 - 1.2)),
        ]
        for discontinuity in discontinuities:
            model.events.append(
                pybamm.Event("nonsmooth rate", pybamm.Scalar(discontinuity)))
        disc = get_discretisation_for_testing()
        disc.process_model(model)

        # Solve
        step_solver = pybamm.ScikitsDaeSolver(rtol=1e-8, atol=1e-8)
        dt = 0.05
        time = 0
        end_time = 3
        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], 0.55)
        np.testing.assert_array_less(step_solution.y[-1, :-1], 1.2)
        np.testing.assert_equal(step_solution.t_event[0], step_solution.t[-1])
        np.testing.assert_array_equal(step_solution.y_event[:, 0],
                                      step_solution.y[:, -1])
        var1_soln = (step_solution.t %
                     a)**2 / 2 + a**2 / 2 * (step_solution.t // a)
        var2_soln = 2 * var1_soln
        np.testing.assert_array_almost_equal(step_solution.y[0],
                                             var1_soln,
                                             decimal=5)
        np.testing.assert_array_almost_equal(step_solution.y[-1],
                                             var2_soln,
                                             decimal=5)
예제 #11
0
    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)

        # Without grid
        solver = pybamm.CasadiSolver(mode="safe without grid", 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)
        solution = solver.solve(model, t_eval, inputs={"rate": 1.1})
        self.assertLess(len(solution.t), len(t_eval))
        np.testing.assert_allclose(solution.y[0], np.exp(-1.1 * solution.t), rtol=1e-04)
예제 #12
0
    def test_model_solver_with_event_with_casadi(self):
        # Create model
        model = pybamm.BaseModel()
        for use_jacobian in [True, False]:
            model.use_jacobian = use_jacobian
            model.convert_to_format = "casadi"
            domain = ["negative electrode", "separator", "positive electrode"]
            var = pybamm.Variable("var", domain=domain)
            model.rhs = {var: -0.1 * var}
            model.initial_conditions = {var: 1}
            model.events = {"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.ScipySolver(rtol=1e-8, atol=1e-8, method="RK45")
            t_eval = np.linspace(0, 10, 100)
            solution = solver.solve(model, t_eval)
            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))
예제 #13
0
 def test_special_functions(self):
     a = pybamm.Array(np.array([1, 2, 3, 4, 5]))
     self.assert_casadi_equal(pybamm.max(a).to_casadi(),
                              casadi.MX(5),
                              evalf=True)
     self.assert_casadi_equal(pybamm.min(a).to_casadi(),
                              casadi.MX(1),
                              evalf=True)
     b = pybamm.Array(np.array([-2]))
     c = pybamm.Array(np.array([3]))
     self.assert_casadi_equal(pybamm.Function(np.abs, b).to_casadi(),
                              casadi.MX(2),
                              evalf=True)
     self.assert_casadi_equal(pybamm.Function(np.abs, c).to_casadi(),
                              casadi.MX(3),
                              evalf=True)
     for np_fun in [
             np.sqrt,
             np.tanh,
             np.cosh,
             np.sinh,
             np.exp,
             np.log,
             np.sign,
             np.sin,
             np.cos,
             np.arccosh,
             np.arcsinh,
     ]:
         self.assert_casadi_equal(pybamm.Function(np_fun, c).to_casadi(),
                                  casadi.MX(np_fun(3)),
                                  evalf=True)
예제 #14
0
 def test_special_functions(self):
     a = pybamm.Array(np.array([1, 2, 3, 4, 5]))
     self.assertEqual(pybamm.max(a).to_casadi(), casadi.SX(5))
     self.assertEqual(pybamm.min(a).to_casadi(), casadi.SX(1))
     b = pybamm.Array(np.array([-2]))
     c = pybamm.Array(np.array([3]))
     self.assertEqual(pybamm.Function(np.abs, b).to_casadi(), casadi.SX(2))
     self.assertEqual(pybamm.Function(np.abs, c).to_casadi(), casadi.SX(3))
예제 #15
0
 def set_events(self, variables):
     c_e = variables["Electrolyte concentration"]
     self.events.append(
         pybamm.Event(
             "Zero electrolyte concentration cut-off",
             pybamm.min(c_e) - 0.002,
             pybamm.EventType.TERMINATION,
         ))
예제 #16
0
    def set_events(self, variables):
        c_s_surf = variables[self.domain + " particle surface concentration"]
        tol = 0.01

        self.events["Minumum " + self.domain.lower() +
                    " particle surface concentration"] = (
                        pybamm.min(c_s_surf) - tol)

        self.events["Maximum " + self.domain.lower() +
                    " particle surface concentration"] = (
                        1 - tol) - pybamm.max(c_s_surf)
예제 #17
0
    def test_model_solver_ode_events(self):
        # Create model
        model = pybamm.BaseModel()
        whole_cell = ["negative electrode", "separator", "positive electrode"]
        var = pybamm.Variable("var", domain=whole_cell)
        model.rhs = {var: 0.1 * var}
        model.initial_conditions = {var: 1}
        model.events = {
            "2 * var = 2.5": pybamm.min(2 * var - 2.5),
            "var = 1.5": pybamm.min(var - 1.5),
        }
        disc = get_discretisation_for_testing()
        disc.process_model(model)

        # Solve
        solver = pybamm.ScikitsOdeSolver(rtol=1e-9, atol=1e-9)
        t_eval = np.linspace(0, 10, 100)
        solution = solver.solve(model, t_eval)
        np.testing.assert_allclose(solution.y[0], np.exp(0.1 * solution.t))
        np.testing.assert_array_less(solution.y[0], 1.5)
        np.testing.assert_array_less(solution.y[0], 1.25)
예제 #18
0
 def test_special_functions(self):
     a = pybamm.Array(np.array([1, 2, 3, 4, 5]))
     self.assert_casadi_equal(pybamm.max(a).to_casadi(),
                              casadi.MX(5),
                              evalf=True)
     self.assert_casadi_equal(pybamm.min(a).to_casadi(),
                              casadi.MX(1),
                              evalf=True)
     b = pybamm.Array(np.array([-2]))
     c = pybamm.Array(np.array([3]))
     self.assert_casadi_equal(pybamm.Function(np.abs, b).to_casadi(),
                              casadi.MX(2),
                              evalf=True)
     self.assert_casadi_equal(pybamm.Function(np.abs, c).to_casadi(),
                              casadi.MX(3),
                              evalf=True)
예제 #19
0
    def set_events(self, variables):
        c_s_surf = variables[self.domain + " particle surface concentration"]
        tol = 0.01

        self.events.append(
            pybamm.Event(
                "Minumum " + self.domain.lower() +
                " particle surface concentration",
                pybamm.min(c_s_surf) - tol,
                pybamm.EventType.TERMINATION,
            ))

        self.events.append(
            pybamm.Event(
                "Maximum " + self.domain.lower() +
                " particle surface concentration",
                (1 - tol) - pybamm.max(c_s_surf),
                pybamm.EventType.TERMINATION,
            ))
예제 #20
0
 def test_min(self):
     a = pybamm.Vector(np.array([1, 2, 3]))
     fun = pybamm.min(a)
     self.assertIsInstance(fun, pybamm.Function)
     self.assertEqual(fun.evaluate(), 1)
예제 #21
0
    def __init__(self, name="Doyle-Fuller-Newman half cell model", options=None):
        super().__init__({}, name)
        pybamm.citations.register("marquis2019asymptotic")
        # `param` is a class containing all the relevant parameters and functions for
        # this model. These are purely symbolic at this stage, and will be set by the
        # `ParameterValues` class when the model is processed.
        param = self.param
        options = options or {"working electrode": None}

        if options["working electrode"] not in ["negative", "positive"]:
            raise ValueError(
                "The option 'working electrode' should be either 'positive'"
                " or 'negative'"
            )

        self.options.update(options)
        working_electrode = options["working electrode"]

        ######################
        # Variables
        ######################
        # Variables that depend on time only are created without a domain
        Q = pybamm.Variable("Discharge capacity [A.h]")

        # Define some useful scalings
        pot = param.potential_scale
        i_typ = param.current_scale

        # Variables that vary spatially are created with a domain. Depending on
        # which is the working electrode we need to define a set variables or another
        if working_electrode == "negative":
            # Electrolyte concentration
            c_e_n = pybamm.Variable(
                "Negative electrolyte concentration", domain="negative electrode"
            )
            c_e_s = pybamm.Variable(
                "Separator electrolyte concentration", domain="separator"
            )
            # Concatenations combine several variables into a single variable, to
            # simplify implementing equations that hold over several domains
            c_e = pybamm.Concatenation(c_e_n, c_e_s)

            # Electrolyte potential
            phi_e_n = pybamm.Variable(
                "Negative electrolyte potential", domain="negative electrode"
            )
            phi_e_s = pybamm.Variable(
                "Separator electrolyte potential", domain="separator"
            )
            phi_e = pybamm.Concatenation(phi_e_n, phi_e_s)

            # Particle concentrations are variables on the particle domain, but also
            # vary in the x-direction (electrode domain) and so must be provided with
            # auxiliary domains
            c_s_n = pybamm.Variable(
                "Negative particle concentration",
                domain="negative particle",
                auxiliary_domains={"secondary": "negative electrode"},
            )
            # Set concentration in positive particle to be equal to the initial
            # concentration as it is not the working electrode
            x_p = pybamm.PrimaryBroadcast(
                pybamm.standard_spatial_vars.x_p, "positive particle"
            )
            c_s_p = param.c_n_init(x_p)

            # Electrode potential
            phi_s_n = pybamm.Variable(
                "Negative electrode potential", domain="negative electrode"
            )
            # Set potential in positive electrode to be equal to the initial OCV
            phi_s_p = param.U_p(pybamm.surf(param.c_p_init(x_p)), param.T_init)
        else:
            c_e_p = pybamm.Variable(
                "Positive electrolyte concentration", domain="positive electrode"
            )
            c_e_s = pybamm.Variable(
                "Separator electrolyte concentration", domain="separator"
            )
            # Concatenations combine several variables into a single variable, to
            # simplify implementing equations that hold over several domains
            c_e = pybamm.Concatenation(c_e_s, c_e_p)

            # Electrolyte potential
            phi_e_s = pybamm.Variable(
                "Separator electrolyte potential", domain="separator"
            )
            phi_e_p = pybamm.Variable(
                "Positive electrolyte potential", domain="positive electrode"
            )
            phi_e = pybamm.Concatenation(phi_e_s, phi_e_p)

            # Particle concentrations are variables on the particle domain, but also
            # vary in the x-direction (electrode domain) and so must be provided with
            # auxiliary domains
            c_s_p = pybamm.Variable(
                "Positive particle concentration",
                domain="positive particle",
                auxiliary_domains={"secondary": "positive electrode"},
            )
            # Set concentration in negative particle to be equal to the initial
            # concentration as it is not the working electrode
            x_n = pybamm.PrimaryBroadcast(
                pybamm.standard_spatial_vars.x_n, "negative particle"
            )
            c_s_n = param.c_n_init(x_n)

            # Electrode potential
            phi_s_p = pybamm.Variable(
                "Positive electrode potential", domain="positive electrode"
            )
            # Set potential in negative electrode to be equal to the initial OCV
            phi_s_n = param.U_n(pybamm.surf(param.c_n_init(x_n)), param.T_init)

        # Constant temperature
        T = param.T_init

        ######################
        # Other set-up
        ######################

        # Current density
        i_cell = param.current_with_time

        # Porosity and Tortuosity
        # Primary broadcasts are used to broadcast scalar quantities across a domain
        # into a vector of the right shape, for multiplying with other vectors
        eps_n = pybamm.PrimaryBroadcast(
            pybamm.Parameter("Negative electrode porosity"), "negative electrode"
        )
        eps_s = pybamm.PrimaryBroadcast(
            pybamm.Parameter("Separator porosity"), "separator"
        )
        eps_p = pybamm.PrimaryBroadcast(
            pybamm.Parameter("Positive electrode porosity"), "positive electrode"
        )

        if working_electrode == "negative":
            eps = pybamm.Concatenation(eps_n, eps_s)
            tor = pybamm.Concatenation(eps_n ** param.b_e_n, eps_s ** param.b_e_s)
        else:
            eps = pybamm.Concatenation(eps_s, eps_p)
            tor = pybamm.Concatenation(eps_s ** param.b_e_s, eps_p ** param.b_e_p)

        # Interfacial reactions
        # Surf takes the surface value of a variable, i.e. its boundary value on the
        # right side. This is also accessible via `boundary_value(x, "right")`, with
        # "left" providing the boundary value of the left side
        c_s_surf_n = pybamm.surf(c_s_n)
        c_s_surf_p = pybamm.surf(c_s_p)

        if working_electrode == "negative":
            j0_n = param.j0_n(c_e_n, c_s_surf_n, T) / param.C_r_n
            j_n = (
                2
                * j0_n
                * pybamm.sinh(
                    param.ne_n / 2 * (phi_s_n - phi_e_n - param.U_n(c_s_surf_n, T))
                )
            )
            j_s = pybamm.PrimaryBroadcast(0, "separator")
            j_p = pybamm.PrimaryBroadcast(0, "positive electrode")
            j = pybamm.Concatenation(j_n, j_s)
        else:
            j0_p = param.gamma_p * param.j0_p(c_e_p, c_s_surf_p, T) / param.C_r_p
            j_p = (
                2
                * j0_p
                * pybamm.sinh(
                    param.ne_p / 2 * (phi_s_p - phi_e_p - param.U_p(c_s_surf_p, T))
                )
            )
            j_s = pybamm.PrimaryBroadcast(0, "separator")
            j_n = pybamm.PrimaryBroadcast(0, "negative electrode")
            j = pybamm.Concatenation(j_s, j_p)

        ######################
        # State of Charge
        ######################
        I = param.dimensional_current_with_time
        # The `rhs` dictionary contains differential equations, with the key being the
        # variable in the d/dt
        self.rhs[Q] = I * param.timescale / 3600
        # Initial conditions must be provided for the ODEs
        self.initial_conditions[Q] = pybamm.Scalar(0)

        ######################
        # Particles
        ######################

        if working_electrode == "negative":
            # The div and grad operators will be converted to the appropriate matrix
            # multiplication at the discretisation stage
            N_s_n = -param.D_n(c_s_n, T) * pybamm.grad(c_s_n)
            self.rhs[c_s_n] = -(1 / param.C_n) * pybamm.div(N_s_n)

            # Boundary conditions must be provided for equations with spatial
            # derivatives
            self.boundary_conditions[c_s_n] = {
                "left": (pybamm.Scalar(0), "Neumann"),
                "right": (
                    -param.C_n * j_n / param.a_R_n / param.D_n(c_s_surf_n, T),
                    "Neumann",
                ),
            }

            # c_n_init can in general be a function of x
            # Note the broadcasting, for domains
            x_n = pybamm.PrimaryBroadcast(
                pybamm.standard_spatial_vars.x_n, "negative particle"
            )
            self.initial_conditions[c_s_n] = param.c_n_init(x_n)

            # Events specify points at which a solution should terminate
            self.events += [
                pybamm.Event(
                    "Minimum negative particle surface concentration",
                    pybamm.min(c_s_surf_n) - 0.01,
                ),
                pybamm.Event(
                    "Maximum negative particle surface concentration",
                    (1 - 0.01) - pybamm.max(c_s_surf_n),
                ),
            ]
        else:
            # The div and grad operators will be converted to the appropriate matrix
            # multiplication at the discretisation stage
            N_s_p = -param.D_p(c_s_p, T) * pybamm.grad(c_s_p)
            self.rhs[c_s_p] = -(1 / param.C_p) * pybamm.div(N_s_p)

            # Boundary conditions must be provided for equations with spatial
            # derivatives
            self.boundary_conditions[c_s_p] = {
                "left": (pybamm.Scalar(0), "Neumann"),
                "right": (
                    -param.C_p
                    * j_p
                    / param.a_R_p
                    / param.gamma_p
                    / param.D_p(c_s_surf_p, T),
                    "Neumann",
                ),
            }

            # c_p_init can in general be a function of x
            # Note the broadcasting, for domains
            x_p = pybamm.PrimaryBroadcast(
                pybamm.standard_spatial_vars.x_p, "positive particle"
            )
            self.initial_conditions[c_s_p] = param.c_p_init(x_p)

            # Events specify points at which a solution should terminate
            self.events += [
                pybamm.Event(
                    "Minimum positive particle surface concentration",
                    pybamm.min(c_s_surf_p) - 0.01,
                ),
                pybamm.Event(
                    "Maximum positive particle surface concentration",
                    (1 - 0.01) - pybamm.max(c_s_surf_p),
                ),
            ]

        ######################
        # Current in the solid
        ######################
        eps_s_n = pybamm.Parameter("Negative electrode active material volume fraction")
        eps_s_p = pybamm.Parameter("Positive electrode active material volume fraction")

        if working_electrode == "negative":
            sigma_eff_n = param.sigma_n * eps_s_n ** param.b_s_n
            i_s_n = -sigma_eff_n * pybamm.grad(phi_s_n)
            self.boundary_conditions[phi_s_n] = {
                "left": (
                    i_cell / pybamm.boundary_value(-sigma_eff_n, "left"),
                    "Neumann",
                ),
                "right": (pybamm.Scalar(0), "Neumann"),
            }
            # The `algebraic` dictionary contains differential equations, with the key
            # being the main scalar variable of interest in the equation
            self.algebraic[phi_s_n] = pybamm.div(i_s_n) + j_n

            # Initial conditions must also be provided for algebraic equations, as an
            # initial guess for a root-finding algorithm which calculates consistent
            # initial conditions
            self.initial_conditions[phi_s_n] = param.U_n(
                param.c_n_init(0), param.T_init
            )
        else:
            sigma_eff_p = param.sigma_p * eps_s_p ** param.b_s_p
            i_s_p = -sigma_eff_p * pybamm.grad(phi_s_p)
            self.boundary_conditions[phi_s_p] = {
                "left": (pybamm.Scalar(0), "Neumann"),
                "right": (
                    i_cell / pybamm.boundary_value(-sigma_eff_p, "right"),
                    "Neumann",
                ),
            }
            self.algebraic[phi_s_p] = pybamm.div(i_s_p) + j_p
            # Initial conditions must also be provided for algebraic equations, as an
            # initial guess for a root-finding algorithm which calculates consistent
            # initial conditions
            self.initial_conditions[phi_s_p] = param.U_p(
                param.c_p_init(1), param.T_init
            )

        ######################
        # Electrolyte concentration
        ######################
        N_e = -tor * param.D_e(c_e, T) * pybamm.grad(c_e)
        self.rhs[c_e] = (1 / eps) * (
            -pybamm.div(N_e) / param.C_e + (1 - param.t_plus(c_e)) * j / param.gamma_e
        )
        dce_dx = (
            -(1 - param.t_plus(c_e))
            * i_cell
            * param.C_e
            / (tor * param.gamma_e * param.D_e(c_e, T))
        )

        if working_electrode == "negative":
            self.boundary_conditions[c_e] = {
                "left": (pybamm.Scalar(0), "Neumann"),
                "right": (pybamm.boundary_value(dce_dx, "right"), "Neumann"),
            }
        else:
            self.boundary_conditions[c_e] = {
                "left": (pybamm.boundary_value(dce_dx, "left"), "Neumann"),
                "right": (pybamm.Scalar(0), "Neumann"),
            }

        self.initial_conditions[c_e] = param.c_e_init
        self.events.append(
            pybamm.Event(
                "Zero electrolyte concentration cut-off", pybamm.min(c_e) - 0.002
            )
        )

        ######################
        # Current in the electrolyte
        ######################
        i_e = (param.kappa_e(c_e, T) * tor * param.gamma_e / param.C_e) * (
            param.chi(c_e, T) * pybamm.grad(c_e) / c_e - pybamm.grad(phi_e)
        )
        self.algebraic[phi_e] = pybamm.div(i_e) - j

        ref_potential = param.U_n_ref / pot

        if working_electrode == "negative":
            self.boundary_conditions[phi_e] = {
                "left": (pybamm.Scalar(0), "Neumann"),
                "right": (ref_potential, "Dirichlet"),
            }
        else:
            self.boundary_conditions[phi_e] = {
                "left": (ref_potential, "Dirichlet"),
                "right": (pybamm.Scalar(0), "Neumann"),
            }

        self.initial_conditions[phi_e] = ref_potential
        ######################
        # (Some) variables
        ######################
        L_Li = pybamm.Parameter("Lithium counter electrode thickness [m]")
        sigma_Li = pybamm.Parameter("Lithium counter electrode conductivity [S.m-1]")
        j_Li = pybamm.Parameter(
            "Lithium counter electrode exchange-current density [A.m-2]"
        )

        if working_electrode == "negative":
            voltage = pybamm.boundary_value(phi_s_n, "left") - ref_potential
            voltage_dim = pot * pybamm.boundary_value(phi_s_n, "left")
            vdrop_Li = 2 * pybamm.arcsinh(
                i_cell * i_typ / j_Li
            ) + L_Li * i_typ * i_cell / (sigma_Li * pot)
            vdrop_Li_dim = (
                2 * pot * pybamm.arcsinh(i_cell * i_typ / j_Li)
                + L_Li * i_typ * i_cell / sigma_Li
            )
        else:
            voltage = pybamm.boundary_value(phi_s_p, "right") - ref_potential
            voltage_dim = param.U_p_ref + pot * voltage
            vdrop_Li = -(
                2 * pybamm.arcsinh(i_cell * i_typ / j_Li)
                + L_Li * i_typ * i_cell / (sigma_Li * pot)
            )
            vdrop_Li_dim = -(
                2 * pot * pybamm.arcsinh(i_cell * i_typ / j_Li)
                + L_Li * i_typ * i_cell / sigma_Li
            )

        c_s_surf_p_av = pybamm.x_average(c_s_surf_p)
        c_s_surf_n_av = pybamm.x_average(c_s_surf_n)

        # The `variables` dictionary contains all variables that might be useful for
        # visualising the solution of the model
        self.variables = {
            "Time [s]": param.timescale * pybamm.t,
            "Negative particle surface concentration": c_s_surf_n,
            "X-averaged negative particle surface concentration": c_s_surf_n_av,
            "Negative particle concentration": c_s_n,
            "Negative particle surface concentration [mol.m-3]": param.c_n_max
            * c_s_surf_n,
            "X-averaged negative particle surface concentration "
            "[mol.m-3]": param.c_n_max * c_s_surf_n_av,
            "Negative particle concentration [mol.m-3]": param.c_n_max * c_s_n,
            "Electrolyte concentration": c_e,
            "Electrolyte concentration [mol.m-3]": param.c_e_typ * c_e,
            "Positive particle surface concentration": c_s_surf_p,
            "X-averaged positive particle surface concentration": c_s_surf_p_av,
            "Positive particle concentration": c_s_p,
            "Positive particle surface concentration [mol.m-3]": param.c_p_max
            * c_s_surf_p,
            "X-averaged positive particle surface concentration "
            "[mol.m-3]": param.c_p_max * c_s_surf_p_av,
            "Positive particle concentration [mol.m-3]": param.c_p_max * c_s_p,
            "Current [A]": I,
            "Negative electrode potential": phi_s_n,
            "Negative electrode potential [V]": pot * phi_s_n,
            "Negative electrode open circuit potential": param.U_n(c_s_surf_n, T),
            "Electrolyte potential": phi_e,
            "Electrolyte potential [V]": -param.U_n_ref + pot * phi_e,
            "Positive electrode potential": phi_s_p,
            "Positive electrode potential [V]": (param.U_p_ref - param.U_n_ref)
            + pot * phi_s_p,
            "Positive electrode open circuit potential": param.U_p(c_s_surf_p, T),
            "Voltage drop": voltage,
            "Voltage drop [V]": voltage_dim,
            "Terminal voltage": voltage + vdrop_Li,
            "Terminal voltage [V]": voltage_dim + vdrop_Li_dim,
        }
예제 #22
0
    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.full()[0, :-1], 1.5)
        np.testing.assert_array_less(solution.y.full()[-1, :-1], 2.5)
        np.testing.assert_equal(solution.t_event[0], solution.t[-1])
        np.testing.assert_array_equal(solution.y_event[:, 0],
                                      solution.y.full()[:, -1])
        np.testing.assert_array_almost_equal(solution.y.full()[0],
                                             np.exp(0.1 * solution.t),
                                             decimal=5)
        np.testing.assert_array_almost_equal(solution.y.full()[-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.full()[0], 1.5)
        np.testing.assert_array_less(solution.y.full()[-1], 2.5 + 1e-10)
        # test the last entry is exactly 2.5
        np.testing.assert_array_almost_equal(solution.y[-1, -1],
                                             2.5,
                                             decimal=2)
        np.testing.assert_array_almost_equal(solution.y.full()[0],
                                             np.exp(0.1 * solution.t),
                                             decimal=5)
        np.testing.assert_array_almost_equal(solution.y.full()[-1],
                                             2 * np.exp(0.1 * solution.t),
                                             decimal=5)
        pybamm.settings.debug_mode = True

        # Try dt_max=0 to enforce using all timesteps
        solver = pybamm.CasadiSolver(dt_max=0, rtol=1e-8, atol=1e-8)
        t_eval = np.linspace(0, 5, 100)
        solution = solver.solve(model, t_eval)
        np.testing.assert_array_less(solution.y.full()[0], 1.5)
        np.testing.assert_array_less(solution.y.full()[-1], 2.5 + 1e-10)
        np.testing.assert_array_almost_equal(solution.y.full()[0],
                                             np.exp(0.1 * solution.t),
                                             decimal=5)
        np.testing.assert_array_almost_equal(solution.y.full()[-1],
                                             2 * np.exp(0.1 * solution.t),
                                             decimal=5)

        # Solve using "fast with events" mode
        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", var1 - 1.5),
            pybamm.Event("var2 = 2.5", var2 - 2.5),
            pybamm.Event("var1 = 1.5 switch", var1 - 2,
                         pybamm.EventType.SWITCH),
            pybamm.Event("var2 = 2.5 switch", var2 - 3,
                         pybamm.EventType.SWITCH),
        ]

        solver = pybamm.CasadiSolver(mode="fast with events",
                                     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.full()[0, :-1], 1.5)
        np.testing.assert_array_less(solution.y.full()[-1, :-1], 2.5)
        np.testing.assert_equal(solution.t_event[0], solution.t[-1])
        np.testing.assert_array_almost_equal(solution.y_event[:, 0].flatten(),
                                             [1.25, 2.5],
                                             decimal=5)
        np.testing.assert_array_almost_equal(solution.y.full()[0],
                                             np.exp(0.1 * solution.t),
                                             decimal=5)
        np.testing.assert_array_almost_equal(solution.y.full()[-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.full()[0], 1.02 + 1e-10)
        np.testing.assert_array_almost_equal(solution.y[0, -1],
                                             1.02,
                                             decimal=2)
예제 #23
0
    def __init__(self, name="Doyle-Fuller-Newman model"):
        super().__init__({}, name)
        # `param` is a class containing all the relevant parameters and functions for
        # this model. These are purely symbolic at this stage, and will be set by the
        # `ParameterValues` class when the model is processed.
        param = self.param

        ######################
        # Variables
        ######################
        # Variables that depend on time only are created without a domain
        Q = pybamm.Variable("Discharge capacity [A.h]")
        # Variables that vary spatially are created with a domain
        c_e_n = pybamm.Variable(
            "Negative electrolyte concentration",
            domain="negative electrode",
        )
        c_e_s = pybamm.Variable(
            "Separator electrolyte concentration",
            domain="separator",
        )
        c_e_p = pybamm.Variable(
            "Positive electrolyte concentration",
            domain="positive electrode",
        )
        # Concatenations combine several variables into a single variable, to simplify
        # implementing equations that hold over several domains
        c_e = pybamm.Concatenation(c_e_n, c_e_s, c_e_p)

        # Electrolyte potential
        phi_e_n = pybamm.Variable(
            "Negative electrolyte potential",
            domain="negative electrode",
        )
        phi_e_s = pybamm.Variable(
            "Separator electrolyte potential",
            domain="separator",
        )
        phi_e_p = pybamm.Variable(
            "Positive electrolyte potential",
            domain="positive electrode",
        )
        phi_e = pybamm.Concatenation(phi_e_n, phi_e_s, phi_e_p)

        # Electrode potential
        phi_s_n = pybamm.Variable(
            "Negative electrode potential",
            domain="negative electrode",
        )
        phi_s_p = pybamm.Variable(
            "Positive electrode potential",
            domain="positive electrode",
        )
        # Particle concentrations are variables on the particle domain, but also vary in
        # the x-direction (electrode domain) and so must be provided with auxiliary
        # domains
        c_s_n = pybamm.Variable(
            "Negative particle concentration",
            domain="negative particle",
            auxiliary_domains={"secondary": "negative electrode"},
        )
        c_s_p = pybamm.Variable(
            "Positive particle concentration",
            domain="positive particle",
            auxiliary_domains={"secondary": "positive electrode"},
        )

        # Constant temperature
        T = param.T_init

        ######################
        # Other set-up
        ######################

        # Current density
        i_cell = param.current_with_time

        # Porosity
        # Primary broadcasts are used to broadcast scalar quantities across a domain
        # into a vector of the right shape, for multiplying with other vectors
        eps_n = pybamm.PrimaryBroadcast(
            pybamm.Parameter("Negative electrode porosity"),
            "negative electrode")
        eps_s = pybamm.PrimaryBroadcast(pybamm.Parameter("Separator porosity"),
                                        "separator")
        eps_p = pybamm.PrimaryBroadcast(
            pybamm.Parameter("Positive electrode porosity"),
            "positive electrode")
        eps = pybamm.Concatenation(eps_n, eps_s, eps_p)

        # Tortuosity
        tor = pybamm.Concatenation(eps_n**param.b_e_n, eps_s**param.b_e_s,
                                   eps_p**param.b_e_p)

        # Interfacial reactions
        # Surf takes the surface value of a variable, i.e. its boundary value on the
        # right side. This is also accessible via `boundary_value(x, "right")`, with
        # "left" providing the boundary value of the left side
        c_s_surf_n = pybamm.surf(c_s_n)
        j0_n = (param.m_n(T) / param.C_r_n * c_e_n**(1 / 2) *
                c_s_surf_n**(1 / 2) * (1 - c_s_surf_n)**(1 / 2))
        j_n = (2 * j0_n *
               pybamm.sinh(param.ne_n / 2 *
                           (phi_s_n - phi_e_n - param.U_n(c_s_surf_n, T))))
        c_s_surf_p = pybamm.surf(c_s_p)
        j0_p = (param.gamma_p * param.m_p(T) / param.C_r_p * c_e_p**(1 / 2) *
                c_s_surf_p**(1 / 2) * (1 - c_s_surf_p)**(1 / 2))
        j_s = pybamm.PrimaryBroadcast(0, "separator")
        j_p = (2 * j0_p *
               pybamm.sinh(param.ne_p / 2 *
                           (phi_s_p - phi_e_p - param.U_p(c_s_surf_p, T))))
        j = pybamm.Concatenation(j_n, j_s, j_p)

        ######################
        # State of Charge
        ######################
        I = param.dimensional_current_with_time
        # The `rhs` dictionary contains differential equations, with the key being the
        # variable in the d/dt
        self.rhs[Q] = I * param.timescale / 3600
        # Initial conditions must be provided for the ODEs
        self.initial_conditions[Q] = pybamm.Scalar(0)

        ######################
        # Particles
        ######################

        # The div and grad operators will be converted to the appropriate matrix
        # multiplication at the discretisation stage
        N_s_n = -param.D_n(c_s_n, T) * pybamm.grad(c_s_n)
        N_s_p = -param.D_p(c_s_p, T) * pybamm.grad(c_s_p)
        self.rhs[c_s_n] = -(1 / param.C_n) * pybamm.div(N_s_n)
        self.rhs[c_s_p] = -(1 / param.C_p) * pybamm.div(N_s_p)
        # Boundary conditions must be provided for equations with spatial derivatives
        self.boundary_conditions[c_s_n] = {
            "left": (pybamm.Scalar(0), "Neumann"),
            "right": (
                -param.C_n * j_n / param.a_n / param.D_n(c_s_surf_n, T),
                "Neumann",
            ),
        }
        self.boundary_conditions[c_s_p] = {
            "left": (pybamm.Scalar(0), "Neumann"),
            "right": (
                -param.C_p * j_p / param.a_p / param.gamma_p /
                param.D_p(c_s_surf_p, T),
                "Neumann",
            ),
        }
        # c_n_init and c_p_init can in general be functions of x
        # Note the broadcasting, for domains
        x_n = pybamm.PrimaryBroadcast(pybamm.standard_spatial_vars.x_n,
                                      "negative particle")
        self.initial_conditions[c_s_n] = param.c_n_init(x_n)
        x_p = pybamm.PrimaryBroadcast(pybamm.standard_spatial_vars.x_p,
                                      "positive particle")
        self.initial_conditions[c_s_p] = param.c_p_init(x_p)
        # Events specify points at which a solution should terminate
        self.events += [
            pybamm.Event(
                "Minimum negative particle surface concentration",
                pybamm.min(c_s_surf_n) - 0.01,
            ),
            pybamm.Event(
                "Maximum negative particle surface concentration",
                (1 - 0.01) - pybamm.max(c_s_surf_n),
            ),
            pybamm.Event(
                "Minimum positive particle surface concentration",
                pybamm.min(c_s_surf_p) - 0.01,
            ),
            pybamm.Event(
                "Maximum positive particle surface concentration",
                (1 - 0.01) - pybamm.max(c_s_surf_p),
            ),
        ]
        ######################
        # Current in the solid
        ######################
        i_s_n = -param.sigma_n * (1 -
                                  eps_n)**param.b_s_n * pybamm.grad(phi_s_n)
        sigma_eff_p = param.sigma_p * (1 - eps_p)**param.b_s_p
        i_s_p = -sigma_eff_p * pybamm.grad(phi_s_p)
        # The `algebraic` dictionary contains differential equations, with the key being
        # the main scalar variable of interest in the equation
        self.algebraic[phi_s_n] = pybamm.div(i_s_n) + j_n
        self.algebraic[phi_s_p] = pybamm.div(i_s_p) + j_p
        self.boundary_conditions[phi_s_n] = {
            "left": (pybamm.Scalar(0), "Dirichlet"),
            "right": (pybamm.Scalar(0), "Neumann"),
        }
        self.boundary_conditions[phi_s_p] = {
            "left": (pybamm.Scalar(0), "Neumann"),
            "right":
            (i_cell / pybamm.boundary_value(-sigma_eff_p, "right"), "Neumann"),
        }
        # Initial conditions must also be provided for algebraic equations, as an
        # initial guess for a root-finding algorithm which calculates consistent initial
        # conditions
        # We evaluate c_n_init at x=0 and c_p_init at x=1 (this is just an initial
        # guess so actual value is not too important)
        self.initial_conditions[phi_s_n] = pybamm.Scalar(0)
        self.initial_conditions[phi_s_p] = param.U_p(
            param.c_p_init(1), param.T_init) - param.U_n(
                param.c_n_init(0), param.T_init)

        ######################
        # Current in the electrolyte
        ######################
        i_e = (param.kappa_e(c_e, T) * tor * param.gamma_e / param.C_e) * (
            param.chi(c_e) * pybamm.grad(c_e) / c_e - pybamm.grad(phi_e))
        self.algebraic[phi_e] = pybamm.div(i_e) - j
        self.boundary_conditions[phi_e] = {
            "left": (pybamm.Scalar(0), "Neumann"),
            "right": (pybamm.Scalar(0), "Neumann"),
        }
        self.initial_conditions[phi_e] = -param.U_n(param.c_n_init(0),
                                                    param.T_init)

        ######################
        # Electrolyte concentration
        ######################
        N_e = -tor * param.D_e(c_e, T) * pybamm.grad(c_e)
        self.rhs[c_e] = (1 /
                         eps) * (-pybamm.div(N_e) / param.C_e +
                                 (1 - param.t_plus(c_e)) * j / param.gamma_e)
        self.boundary_conditions[c_e] = {
            "left": (pybamm.Scalar(0), "Neumann"),
            "right": (pybamm.Scalar(0), "Neumann"),
        }
        self.initial_conditions[c_e] = param.c_e_init
        self.events.append(
            pybamm.Event("Zero electrolyte concentration cut-off",
                         pybamm.min(c_e) - 0.002))

        ######################
        # (Some) variables
        ######################
        voltage = pybamm.boundary_value(phi_s_p, "right")
        # The `variables` dictionary contains all variables that might be useful for
        # visualising the solution of the model
        self.variables = {
            "Negative particle surface concentration": c_s_surf_n,
            "Electrolyte concentration": c_e,
            "Positive particle surface concentration": c_s_surf_p,
            "Current [A]": I,
            "Negative electrode potential": phi_s_n,
            "Electrolyte potential": phi_e,
            "Positive electrode potential": phi_s_p,
            "Terminal voltage": voltage,
        }
        self.events += [
            pybamm.Event("Minimum voltage", voltage - param.voltage_low_cut),
            pybamm.Event("Maximum voltage", voltage - param.voltage_high_cut),
        ]
예제 #24
0
    def _process_symbol(self, symbol):
        """ See :meth:`ParameterValues.process_symbol()`. """

        if isinstance(symbol, pybamm.Parameter):
            value = self[symbol.name]
            if isinstance(value, numbers.Number):
                # Scalar inherits name (for updating parameters) and domain (for
                # Broadcast)
                return pybamm.Scalar(value,
                                     name=symbol.name,
                                     domain=symbol.domain)
            elif isinstance(value, pybamm.Symbol):
                new_value = self.process_symbol(value)
                new_value.domain = symbol.domain
                return new_value
            else:
                raise TypeError("Cannot process parameter '{}'".format(value))

        elif isinstance(symbol, pybamm.FunctionParameter):
            new_children = []
            for child in symbol.children:
                if symbol.diff_variable is not None and any(
                        x.id == symbol.diff_variable.id
                        for x in child.pre_order()):
                    # Wrap with NotConstant to avoid simplification,
                    # which would stop symbolic diff from working properly
                    new_child = pybamm.NotConstant(child.new_copy())
                    new_children.append(self.process_symbol(new_child))
                else:
                    new_children.append(self.process_symbol(child))
            function_name = self[symbol.name]

            # Create Function or Interpolant or Scalar object
            if isinstance(function_name, tuple):
                # If function_name is a tuple then it should be (name, data) and we need
                # to create an Interpolant
                name, data = function_name
                function = pybamm.Interpolant(data[:, 0],
                                              data[:, 1],
                                              *new_children,
                                              name=name)
                # Define event to catch extrapolation. In these events the sign is
                # important: it should be positive inside of the range and negative
                # outside of it
                self.parameter_events.append(
                    pybamm.Event(
                        "Interpolant {} lower bound".format(name),
                        pybamm.min(new_children[0] - min(data[:, 0])),
                        pybamm.EventType.INTERPOLANT_EXTRAPOLATION,
                    ))
                self.parameter_events.append(
                    pybamm.Event(
                        "Interpolant {} upper bound".format(name),
                        pybamm.min(max(data[:, 0]) - new_children[0]),
                        pybamm.EventType.INTERPOLANT_EXTRAPOLATION,
                    ))
            elif isinstance(function_name, numbers.Number):
                # If the "function" is provided is actually a scalar, return a Scalar
                # object instead of throwing an error.
                # Also use ones_like so that we get the right shapes
                function = pybamm.Scalar(
                    function_name,
                    name=symbol.name) * pybamm.ones_like(*new_children)
            elif (isinstance(function_name, pybamm.Symbol)
                  and function_name.evaluates_to_number()):
                # If the "function" provided is a pybamm scalar-like, use ones_like to
                # get the right shape
                # This also catches input parameters
                function = function_name * pybamm.ones_like(*new_children)
            elif callable(function_name):
                # otherwise evaluate the function to create a new PyBaMM object
                function = function_name(*new_children)
            elif isinstance(function_name, pybamm.Interpolant):
                function = function_name
            else:
                raise TypeError(
                    "Parameter provided for '{}' ".format(symbol.name) +
                    "is of the wrong type (should either be scalar-like or callable)"
                )
            # Differentiate if necessary
            if symbol.diff_variable is None:
                function_out = function
            else:
                # return differentiated function
                new_diff_variable = self.process_symbol(symbol.diff_variable)
                function_out = function.diff(new_diff_variable)
            # Convert possible float output to a pybamm scalar
            if isinstance(function_out, numbers.Number):
                return pybamm.Scalar(function_out)
            # Process again just to be sure
            return self.process_symbol(function_out)

        elif isinstance(symbol, pybamm.BinaryOperator):
            # process children
            new_left = self.process_symbol(symbol.left)
            new_right = self.process_symbol(symbol.right)
            # Special case for averages, which can appear as "integral of a broadcast"
            # divided by "integral of a broadcast"
            # this construction seems very specific but can appear often when averaging
            if (isinstance(symbol, pybamm.Division)
                    # right is integral(Broadcast(1))
                    and (isinstance(new_right, pybamm.Integral)
                         and isinstance(new_right.child, pybamm.Broadcast)
                         and new_right.child.child.id == pybamm.Scalar(1).id)
                    # left is integral
                    and isinstance(new_left, pybamm.Integral)):
                # left is integral(Broadcast)
                if (isinstance(new_left.child, pybamm.Broadcast)
                        and new_left.child.child.domain == []):
                    integrand = new_left.child
                    if integrand.auxiliary_domains == {}:
                        return integrand.orphans[0]
                    else:
                        domain = integrand.auxiliary_domains["secondary"]
                        if "tertiary" not in integrand.auxiliary_domains:
                            return pybamm.PrimaryBroadcast(
                                integrand.orphans[0], domain)
                        else:
                            auxiliary_domains = {
                                "secondary":
                                integrand.auxiliary_domains["tertiary"]
                            }
                            return pybamm.FullBroadcast(
                                integrand.orphans[0], domain,
                                auxiliary_domains)
                # left is "integral of concatenation of broadcasts"
                elif isinstance(new_left.child, pybamm.Concatenation) and all(
                        isinstance(child, pybamm.Broadcast)
                        for child in new_left.child.children):
                    return self.process_symbol(pybamm.x_average(
                        new_left.child))
            # make new symbol, ensure domain remains the same
            new_symbol = symbol._binary_new_copy(new_left, new_right)
            new_symbol.domain = symbol.domain
            return new_symbol

        # Unary operators
        elif isinstance(symbol, pybamm.UnaryOperator):
            new_child = self.process_symbol(symbol.child)
            new_symbol = symbol._unary_new_copy(new_child)
            # ensure domain remains the same
            new_symbol.domain = symbol.domain
            return new_symbol

        # Functions
        elif isinstance(symbol, pybamm.Function):
            new_children = [
                self.process_symbol(child) for child in symbol.children
            ]
            return symbol._function_new_copy(new_children)

        # Concatenations
        elif isinstance(symbol, pybamm.Concatenation):
            new_children = [
                self.process_symbol(child) for child in symbol.children
            ]
            return symbol._concatenation_new_copy(new_children)

        else:
            # Backup option: return new copy of the object
            try:
                return symbol.new_copy()
            except NotImplementedError:
                raise NotImplementedError(
                    "Cannot process parameters for symbol of type '{}'".format(
                        type(symbol)))
예제 #25
0
 def test_min(self):
     a = pybamm.StateVector(slice(0, 3))
     y_test = np.array([1, 2, 3])
     fun = pybamm.min(a)
     self.assertIsInstance(fun, pybamm.Function)
     self.assertEqual(fun.evaluate(y=y_test), 1)
예제 #26
0
    def test_model_solver_dae_nonsmooth(self):
        whole_cell = ["negative electrode", "separator", "positive electrode"]
        var1 = pybamm.Variable("var1", domain=whole_cell)
        var2 = pybamm.Variable("var2")
        discontinuity = 0.6

        # Create three different models with the same solution, each expressing the
        # discontinuity in a different way

        # first model explicitly adds a discontinuity event
        def nonsmooth_rate(t):
            return 0.1 * (t < discontinuity) + 0.1

        rate = pybamm.Function(nonsmooth_rate, pybamm.t)
        model1 = pybamm.BaseModel()
        model1.rhs = {var1: rate * var1}
        model1.algebraic = {var2: var2}
        model1.initial_conditions = {var1: 1, var2: 0}
        model1.events = [
            pybamm.Event("var1 = 1.5", pybamm.min(var1 - 1.5)),
            pybamm.Event(
                "nonsmooth rate",
                pybamm.Scalar(discontinuity),
                pybamm.EventType.DISCONTINUITY,
            ),
        ]

        # second model implicitly adds a discontinuity event via a heaviside function
        model2 = pybamm.BaseModel()
        model2.rhs = {var1: (0.1 * (pybamm.t < discontinuity) + 0.1) * var1}
        model2.algebraic = {var2: var2}
        model2.initial_conditions = {var1: 1, var2: 0}
        model2.events = [pybamm.Event("var1 = 1.5", pybamm.min(var1 - 1.5))]

        # third model implicitly adds a discontinuity event via another heaviside
        # function
        model3 = pybamm.BaseModel()
        model3.rhs = {var1: (-0.1 * (discontinuity < pybamm.t) + 0.2) * var1}
        model3.algebraic = {var2: var2}
        model3.initial_conditions = {var1: 1, var2: 0}
        model3.events = [pybamm.Event("var1 = 1.5", pybamm.min(var1 - 1.5))]

        for model in [model1, model2, model3]:

            disc = get_discretisation_for_testing()
            disc.process_model(model)

            # Solve
            solver = pybamm.ScikitsDaeSolver(rtol=1e-8, atol=1e-8)

            # create two time series, one without a time point on the discontinuity,
            # and one with
            t_eval1 = np.linspace(0, 5, 10)
            t_eval2 = np.insert(t_eval1,
                                np.searchsorted(t_eval1,
                                                discontinuity), discontinuity)
            solution1 = solver.solve(model, t_eval1)
            solution2 = solver.solve(model, t_eval2)

            # check time vectors
            for solution in [solution1, solution2]:
                # time vectors are ordered
                self.assertTrue(np.all(solution.t[:-1] <= solution.t[1:]))

                # time value before and after discontinuity is an epsilon away
                dindex = np.searchsorted(solution.t, discontinuity)
                value_before = solution.t[dindex - 1]
                value_after = solution.t[dindex]
                self.assertEqual(value_before + sys.float_info.epsilon,
                                 discontinuity)
                self.assertEqual(value_after - sys.float_info.epsilon,
                                 discontinuity)

            # both solution time vectors should have same number of points
            self.assertEqual(len(solution1.t), len(solution2.t))

            # check solution
            for solution in [solution1, solution2]:
                np.testing.assert_array_less(solution.y[0], 1.5)
                np.testing.assert_array_less(solution.y[-1], 2.5)
                var1_soln = np.exp(0.2 * solution.t)
                y0 = np.exp(0.2 * discontinuity)
                var1_soln[solution.t > discontinuity] = y0 * np.exp(
                    0.1 *
                    (solution.t[solution.t > discontinuity] - discontinuity))
                np.testing.assert_allclose(solution.y[0],
                                           var1_soln,
                                           rtol=1e-06)
예제 #27
0
    def __init__(self, name="Single Particle Model"):
        super().__init__({}, name)
        pybamm.citations.register("Marquis2019")
        # `param` is a class containing all the relevant parameters and functions for
        # this model. These are purely symbolic at this stage, and will be set by the
        # `ParameterValues` class when the model is processed.
        param = self.param

        ######################
        # Variables
        ######################
        # Variables that depend on time only are created without a domain
        Q = pybamm.Variable("Discharge capacity [A.h]")
        # Variables that vary spatially are created with a domain
        c_s_n = pybamm.Variable(
            "X-averaged negative particle concentration", domain="negative particle"
        )
        c_s_p = pybamm.Variable(
            "X-averaged positive particle concentration", domain="positive particle"
        )

        # Constant temperature
        T = param.T_init

        ######################
        # Other set-up
        ######################

        # Current density
        i_cell = param.current_with_time
        j_n = i_cell / param.l_n
        j_p = -i_cell / param.l_p

        ######################
        # State of Charge
        ######################
        I = param.dimensional_current_with_time
        # The `rhs` dictionary contains differential equations, with the key being the
        # variable in the d/dt
        self.rhs[Q] = I * param.timescale / 3600
        # Initial conditions must be provided for the ODEs
        self.initial_conditions[Q] = pybamm.Scalar(0)

        ######################
        # Particles
        ######################

        # The div and grad operators will be converted to the appropriate matrix
        # multiplication at the discretisation stage
        N_s_n = -param.D_n(c_s_n, T) * pybamm.grad(c_s_n)
        N_s_p = -param.D_p(c_s_p, T) * pybamm.grad(c_s_p)
        self.rhs[c_s_n] = -(1 / param.C_n) * pybamm.div(N_s_n)
        self.rhs[c_s_p] = -(1 / param.C_p) * pybamm.div(N_s_p)
        # Surf takes the surface value of a variable, i.e. its boundary value on the
        # right side. This is also accessible via `boundary_value(x, "right")`, with
        # "left" providing the boundary value of the left side
        c_s_surf_n = pybamm.surf(c_s_n)
        c_s_surf_p = pybamm.surf(c_s_p)
        # Boundary conditions must be provided for equations with spatial derivatives
        self.boundary_conditions[c_s_n] = {
            "left": (pybamm.Scalar(0), "Neumann"),
            "right": (
                -param.C_n * j_n / param.a_R_n / param.D_n(c_s_surf_n, T),
                "Neumann",
            ),
        }
        self.boundary_conditions[c_s_p] = {
            "left": (pybamm.Scalar(0), "Neumann"),
            "right": (
                -param.C_p
                * j_p
                / param.a_R_p
                / param.gamma_p
                / param.D_p(c_s_surf_p, T),
                "Neumann",
            ),
        }
        # c_n_init and c_p_init are functions, but for the SPM we evaluate them at x=0
        # and x=1 since there is no x-dependence in the particles
        self.initial_conditions[c_s_n] = param.c_n_init(0)
        self.initial_conditions[c_s_p] = param.c_p_init(1)
        # Events specify points at which a solution should terminate
        self.events += [
            pybamm.Event(
                "Minimum negative particle surface concentration",
                pybamm.min(c_s_surf_n) - 0.01,
            ),
            pybamm.Event(
                "Maximum negative particle surface concentration",
                (1 - 0.01) - pybamm.max(c_s_surf_n),
            ),
            pybamm.Event(
                "Minimum positive particle surface concentration",
                pybamm.min(c_s_surf_p) - 0.01,
            ),
            pybamm.Event(
                "Maximum positive particle surface concentration",
                (1 - 0.01) - pybamm.max(c_s_surf_p),
            ),
        ]

        # Note that the SPM does not have any algebraic equations, so the `algebraic`
        # dictionary remains empty

        ######################
        # (Some) variables
        ######################
        # Interfacial reactions
        j0_n = param.j0_n(1, c_s_surf_n, T) / param.C_r_n
        j0_p = param.gamma_p * param.j0_p(1, c_s_surf_p, T) / param.C_r_p
        eta_n = (2 / param.ne_n) * pybamm.arcsinh(j_n / (2 * j0_n))
        eta_p = (2 / param.ne_p) * pybamm.arcsinh(j_p / (2 * j0_p))
        phi_s_n = 0
        phi_e = -eta_n - param.U_n(c_s_surf_n, T)
        phi_s_p = eta_p + phi_e + param.U_p(c_s_surf_p, T)
        V = phi_s_p

        whole_cell = ["negative electrode", "separator", "positive electrode"]
        # The `variables` dictionary contains all variables that might be useful for
        # visualising the solution of the model
        # Primary broadcasts are used to broadcast scalar quantities across a domain
        # into a vector of the right shape, for multiplying with other vectors
        self.variables = {
            "Negative particle surface concentration": pybamm.PrimaryBroadcast(
                c_s_surf_n, "negative electrode"
            ),
            "Electrolyte concentration": pybamm.PrimaryBroadcast(1, whole_cell),
            "Positive particle surface concentration": pybamm.PrimaryBroadcast(
                c_s_surf_p, "positive electrode"
            ),
            "Current [A]": I,
            "Negative electrode potential": pybamm.PrimaryBroadcast(
                phi_s_n, "negative electrode"
            ),
            "Electrolyte potential": pybamm.PrimaryBroadcast(phi_e, whole_cell),
            "Positive electrode potential": pybamm.PrimaryBroadcast(
                phi_s_p, "positive electrode"
            ),
            "Terminal voltage": V,
        }
        self.events += [
            pybamm.Event("Minimum voltage", V - param.voltage_low_cut),
            pybamm.Event("Maximum voltage", V - param.voltage_high_cut),
        ]
예제 #28
0
    def test_model_solver_dae_multiple_nonsmooth_python(self):
        model = pybamm.BaseModel()
        model.convert_to_format = "python"
        whole_cell = ["negative electrode", "separator", "positive electrode"]
        var1 = pybamm.Variable("var1", domain=whole_cell)
        var2 = pybamm.Variable("var2", domain=whole_cell)
        a = 0.6
        discontinuities = (np.arange(3) + 1) * a

        model.rhs = {var1: pybamm.Modulo(pybamm.t, a)}
        model.algebraic = {var2: 2 * var1 - var2}
        model.initial_conditions = {var1: 0, var2: 0}
        model.events = [
            pybamm.Event("var1 = 0.55", pybamm.min(var1 - 0.55)),
            pybamm.Event("var2 = 1.2", pybamm.min(var2 - 1.2)),
        ]
        for discontinuity in discontinuities:
            model.events.append(
                pybamm.Event("nonsmooth rate", pybamm.Scalar(discontinuity)))
        disc = get_discretisation_for_testing()
        disc.process_model(model)

        # Solve
        solver = pybamm.ScikitsDaeSolver(rtol=1e-8,
                                         atol=1e-8,
                                         root_method="lm")

        # create two time series, one without a time point on the discontinuity,
        # and one with
        t_eval1 = np.linspace(0, 2, 10)
        t_eval2 = np.insert(t_eval1, np.searchsorted(t_eval1, discontinuities),
                            discontinuities)
        solution1 = solver.solve(model, t_eval1)
        solution2 = solver.solve(model, t_eval2)

        # check time vectors
        for solution in [solution1, solution2]:
            # time vectors are ordered
            self.assertTrue(np.all(solution.t[:-1] <= solution.t[1:]))

            # time value before and after discontinuity is an epsilon away
            for discontinuity in discontinuities:
                dindex = np.searchsorted(solution.t, discontinuity)
                value_before = solution.t[dindex - 1]
                value_after = solution.t[dindex]
                self.assertEqual(value_before + sys.float_info.epsilon,
                                 discontinuity)
                self.assertEqual(value_after - sys.float_info.epsilon,
                                 discontinuity)

        # both solution time vectors should have same number of points
        self.assertEqual(len(solution1.t), len(solution2.t))

        # check solution
        for solution in [solution1, solution2]:
            np.testing.assert_array_less(solution.y[0, :-1], 0.55)
            np.testing.assert_array_less(solution.y[-1, :-1], 1.2)
            var1_soln = (solution.t % a)**2 / 2 + a**2 / 2 * (solution.t // a)
            var2_soln = 2 * var1_soln
            np.testing.assert_allclose(solution.y[0], var1_soln, rtol=1e-06)
            np.testing.assert_allclose(solution.y[-1], var2_soln, rtol=1e-06)
예제 #29
0
    def test_model_solver_dae_nonsmooth_python(self):
        model = pybamm.BaseModel()
        model.convert_to_format = "python"
        whole_cell = ["negative electrode", "separator", "positive electrode"]
        var1 = pybamm.Variable("var1", domain=whole_cell)
        var2 = pybamm.Variable("var2", domain=whole_cell)
        discontinuity = 0.6

        def nonsmooth_rate(t):
            return 0.1 * int(t < discontinuity) + 0.1

        def nonsmooth_mult(t):
            return int(t < discontinuity) + 1.0

        rate = pybamm.Function(nonsmooth_rate, pybamm.t)
        mult = pybamm.Function(nonsmooth_mult, pybamm.t)
        # put in an extra heaviside with no time dependence, this should be ignored by
        # the solver i.e. no extra discontinuities added
        model.rhs = {var1: rate * var1 + (var1 < 0)}
        model.algebraic = {var2: mult * 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)),
            pybamm.Event(
                "nonsmooth rate",
                pybamm.Scalar(discontinuity),
                pybamm.EventType.DISCONTINUITY,
            ),
            pybamm.Event(
                "nonsmooth mult",
                pybamm.Scalar(discontinuity),
                pybamm.EventType.DISCONTINUITY,
            ),
        ]
        disc = get_discretisation_for_testing()
        disc.process_model(model)

        # Solve
        solver = pybamm.ScikitsDaeSolver(rtol=1e-8,
                                         atol=1e-8,
                                         root_method="lm")

        # create two time series, one without a time point on the discontinuity,
        # and one with
        t_eval1 = np.linspace(0, 5, 10)
        t_eval2 = np.insert(t_eval1, np.searchsorted(t_eval1, discontinuity),
                            discontinuity)
        solution1 = solver.solve(model, t_eval1)
        solution2 = solver.solve(model, t_eval2)

        # check time vectors
        for solution in [solution1, solution2]:
            # time vectors are ordered
            self.assertTrue(np.all(solution.t[:-1] <= solution.t[1:]))

            # time value before and after discontinuity is an epsilon away
            dindex = np.searchsorted(solution.t, discontinuity)
            value_before = solution.t[dindex - 1]
            value_after = solution.t[dindex]
            self.assertEqual(value_before + sys.float_info.epsilon,
                             discontinuity)
            self.assertEqual(value_after - sys.float_info.epsilon,
                             discontinuity)

        # both solution time vectors should have same number of points
        self.assertEqual(len(solution1.t), len(solution2.t))

        # check solution
        for solution in [solution1, solution2]:
            np.testing.assert_array_less(solution.y[0], 1.5)
            np.testing.assert_array_less(solution.y[-1], 2.5)
            var1_soln = np.exp(0.2 * solution.t)
            y0 = np.exp(0.2 * discontinuity)
            var1_soln[solution.t > discontinuity] = y0 * np.exp(
                0.1 * (solution.t[solution.t > discontinuity] - discontinuity))
            var2_soln = 2 * var1_soln
            var2_soln[solution.t > discontinuity] = var1_soln[
                solution.t > discontinuity]
            np.testing.assert_allclose(solution.y[0], var1_soln, rtol=1e-06)
            np.testing.assert_allclose(solution.y[-1], var2_soln, rtol=1e-06)
예제 #30
0
    def _get_standard_concentration_variables(
        self, c_s, c_s_xav=None, c_s_rav=None, c_s_av=None, c_s_surf=None
    ):
        """
        All particle submodels must provide the particle concentration as an argument
        to this method. Some submodels solve for quantities other than the concentration
        itself, for example the 'FickianSingleParticle' models solves for the x-averaged
        concentration. In such cases the variables being solved for (set in
        'get_fundamental_variables') must also be passed as keyword arguments. If not
        passed as keyword arguments, the various average concentrations and surface
        concentration are computed automatically from the particle concentration.
        """

        # Get surface concentration if not provided as fundamental variable to
        # solve for
        c_s_surf = c_s_surf or pybamm.surf(c_s)
        c_s_surf_av = pybamm.x_average(c_s_surf)

        if self.domain == "Negative":
            c_scale = self.param.c_n_max
        elif self.domain == "Positive":
            c_scale = self.param.c_p_max

        # Get average concentration(s) if not provided as fundamental variable to
        # solve for
        c_s_xav = c_s_xav or pybamm.x_average(c_s)
        c_s_rav = c_s_rav or pybamm.r_average(c_s)
        c_s_av = c_s_av or pybamm.r_average(c_s_xav)

        variables = {
            self.domain + " particle concentration": c_s,
            self.domain + " particle concentration [mol.m-3]": c_s * c_scale,
            self.domain + " particle concentration [mol.m-3]": c_s * c_scale,
            "X-averaged " + self.domain.lower() + " particle concentration": c_s_xav,
            "X-averaged "
            + self.domain.lower()
            + " particle concentration [mol.m-3]": c_s_xav * c_scale,
            "R-averaged " + self.domain.lower() + " particle concentration": c_s_rav,
            "R-averaged "
            + self.domain.lower()
            + " particle concentration [mol.m-3]": c_s_rav * c_scale,
            "Average " + self.domain.lower() + " particle concentration": c_s_av,
            "Average "
            + self.domain.lower()
            + " particle concentration [mol.m-3]": c_s_av * c_scale,
            self.domain + " particle surface concentration": c_s_surf,
            self.domain
            + " particle surface concentration [mol.m-3]": c_scale * c_s_surf,
            "X-averaged "
            + self.domain.lower()
            + " particle surface concentration": c_s_surf_av,
            "X-averaged "
            + self.domain.lower()
            + " particle surface concentration [mol.m-3]": c_scale * c_s_surf_av,
            self.domain + " electrode extent of lithiation": c_s_rav,
            "X-averaged "
            + self.domain.lower()
            + " electrode extent of lithiation": c_s_av,
            "Minimum "
            + self.domain.lower()
            + " particle concentration": pybamm.min(c_s),
            "Maximum "
            + self.domain.lower()
            + " particle concentration": pybamm.max(c_s),
            "Minimum "
            + self.domain.lower()
            + " particle concentration [mol.m-3]": pybamm.min(c_s) * c_scale,
            "Maximum "
            + self.domain.lower()
            + " particle concentration [mol.m-3]": pybamm.max(c_s) * c_scale,
            "Minimum "
            + self.domain.lower()
            + " particle surface concentration": pybamm.min(c_s_surf),
            "Maximum "
            + self.domain.lower()
            + " particle surface concentration": pybamm.max(c_s_surf),
            "Minimum "
            + self.domain.lower()
            + " particle surface concentration [mol.m-3]": pybamm.min(c_s_surf)
            * c_scale,
            "Maximum "
            + self.domain.lower()
            + " particle surface concentration [mol.m-3]": pybamm.max(c_s_surf)
            * c_scale,
        }

        return variables