def set_voltage_variables(self):

        ocp_n = self.variables["Negative electrode open circuit potential"]
        ocp_p = self.variables["Positive electrode open circuit potential"]
        ocp_n_av = self.variables[
            "X-averaged negative electrode open circuit potential"]
        ocp_p_av = self.variables[
            "X-averaged positive electrode open circuit potential"]

        ocp_n_dim = self.variables[
            "Negative electrode open circuit potential [V]"]
        ocp_p_dim = self.variables[
            "Positive electrode open circuit potential [V]"]
        ocp_n_av_dim = self.variables[
            "X-averaged negative electrode open circuit potential [V]"]
        ocp_p_av_dim = self.variables[
            "X-averaged positive electrode open circuit potential [V]"]

        ocp_n_left = pybamm.boundary_value(ocp_n, "left")
        ocp_n_left_dim = pybamm.boundary_value(ocp_n_dim, "left")
        ocp_p_right = pybamm.boundary_value(ocp_p, "right")
        ocp_p_right_dim = pybamm.boundary_value(ocp_p_dim, "right")

        ocv_av = ocp_p_av - ocp_n_av
        ocv_av_dim = ocp_p_av_dim - ocp_n_av_dim
        ocv = ocp_p_right - ocp_n_left
        ocv_dim = ocp_p_right_dim - ocp_n_left_dim

        # overpotentials
        eta_r_n_av = self.variables[
            "X-averaged negative electrode reaction overpotential"]
        eta_r_n_av_dim = self.variables[
            "X-averaged negative electrode reaction overpotential [V]"]
        eta_r_p_av = self.variables[
            "X-averaged positive electrode reaction overpotential"]
        eta_r_p_av_dim = self.variables[
            "X-averaged positive electrode reaction overpotential [V]"]

        delta_phi_s_n_av = self.variables[
            "X-averaged negative electrode ohmic losses"]
        delta_phi_s_n_av_dim = self.variables[
            "X-averaged negative electrode ohmic losses [V]"]
        delta_phi_s_p_av = self.variables[
            "X-averaged positive electrode ohmic losses"]
        delta_phi_s_p_av_dim = self.variables[
            "X-averaged positive electrode ohmic losses [V]"]

        delta_phi_s_av = delta_phi_s_p_av - delta_phi_s_n_av
        delta_phi_s_av_dim = delta_phi_s_p_av_dim - delta_phi_s_n_av_dim

        eta_r_av = eta_r_p_av - eta_r_n_av
        eta_r_av_dim = eta_r_p_av_dim - eta_r_n_av_dim

        # SEI film overpotential
        eta_sei_n_av = self.variables[
            "X-averaged negative electrode SEI film overpotential"]
        eta_sei_p_av = self.variables[
            "X-averaged positive electrode SEI film overpotential"]
        eta_sei_n_av_dim = self.variables[
            "X-averaged negative electrode SEI film overpotential [V]"]
        eta_sei_p_av_dim = self.variables[
            "X-averaged positive electrode SEI film overpotential [V]"]
        eta_sei_av = eta_sei_n_av + eta_sei_p_av
        eta_sei_av_dim = eta_sei_n_av_dim + eta_sei_p_av_dim

        # TODO: add current collector losses to the voltage in 3D

        self.variables.update({
            "X-averaged open circuit voltage":
            ocv_av,
            "Measured open circuit voltage":
            ocv,
            "X-averaged open circuit voltage [V]":
            ocv_av_dim,
            "Measured open circuit voltage [V]":
            ocv_dim,
            "X-averaged reaction overpotential":
            eta_r_av,
            "X-averaged reaction overpotential [V]":
            eta_r_av_dim,
            "X-averaged SEI film overpotential":
            eta_sei_av,
            "X-averaged SEI film overpotential [V]":
            eta_sei_av_dim,
            "X-averaged solid phase ohmic losses":
            delta_phi_s_av,
            "X-averaged solid phase ohmic losses [V]":
            delta_phi_s_av_dim,
        })

        # Battery-wide variables
        V = self.variables["Terminal voltage"]
        V_dim = self.variables["Terminal voltage [V]"]
        eta_e_av_dim = self.variables[
            "X-averaged electrolyte ohmic losses [V]"]
        eta_c_av_dim = self.variables[
            "X-averaged concentration overpotential [V]"]
        num_cells = pybamm.Parameter(
            "Number of cells connected in series to make a battery")
        self.variables.update({
            "X-averaged battery open circuit voltage [V]":
            ocv_av_dim * num_cells,
            "Measured battery open circuit voltage [V]":
            ocv_dim * num_cells,
            "X-averaged battery reaction overpotential [V]":
            eta_r_av_dim * num_cells,
            "X-averaged battery solid phase ohmic losses [V]":
            delta_phi_s_av_dim * num_cells,
            "X-averaged battery electrolyte ohmic losses [V]":
            eta_e_av_dim * num_cells,
            "X-averaged battery concentration overpotential [V]":
            eta_c_av_dim * num_cells,
            "Battery voltage [V]":
            V_dim * num_cells,
        })
        # Variables for calculating the equivalent circuit model (ECM) resistance
        # Need to compare OCV to initial value to capture this as an overpotential
        ocv_init = self.param.U_p(
            self.param.c_p_init(1), self.param.T_init) - self.param.U_n(
                self.param.c_n_init(0), self.param.T_init)
        ocv_init_dim = (self.param.U_p_ref - self.param.U_n_ref +
                        self.param.potential_scale * ocv_init)
        eta_ocv = ocv - ocv_init
        eta_ocv_dim = ocv_dim - ocv_init_dim
        # Current collector current density for working out euiqvalent resistance
        # based on Ohm's Law
        i_cc = self.variables["Current collector current density"]
        i_cc_dim = self.variables["Current collector current density [A.m-2]"]
        # ECM overvoltage is OCV minus terminal voltage
        v_ecm = ocv - V
        v_ecm_dim = ocv_dim - V_dim
        # Current collector area for turning resistivity into resistance
        A_cc = self.param.A_cc

        # Hack to avoid division by zero if i_cc is exactly zero
        # If i_cc is zero, i_cc_not_zero becomes 1. But multiplying by sign(i_cc) makes
        # the local resistance 'zero' (really, it's not defined when i_cc is zero)
        i_cc_not_zero = ((i_cc > 0) +
                         (i_cc < 0)) * i_cc + (i_cc >= 0) * (i_cc <= 0)
        i_cc_dim_not_zero = (
            (i_cc_dim > 0) +
            (i_cc_dim < 0)) * i_cc_dim + (i_cc_dim >= 0) * (i_cc_dim <= 0)

        self.variables.update({
            "Change in measured open circuit voltage":
            eta_ocv,
            "Change in measured open circuit voltage [V]":
            eta_ocv_dim,
            "Local ECM resistance":
            pybamm.sign(i_cc) * v_ecm / (i_cc_not_zero * A_cc),
            "Local ECM resistance [Ohm]":
            pybamm.sign(i_cc) * v_ecm_dim / (i_cc_dim_not_zero * A_cc),
        })

        # Cut-off voltage
        self.events.append(
            pybamm.Event(
                "Minimum voltage",
                V - self.param.voltage_low_cut,
                pybamm.EventType.TERMINATION,
            ))
        self.events.append(
            pybamm.Event(
                "Maximum voltage",
                V - self.param.voltage_high_cut,
                pybamm.EventType.TERMINATION,
            ))

        # Cut-off open-circuit voltage (for event switch with casadi 'fast with events'
        # mode)
        # A tolerance of 1 is sufficiently small since the dimensionless voltage is
        # scaled with the thermal voltage (0.025V) and hence has a range of around 60
        tol = 1
        self.events.append(
            pybamm.Event(
                "Minimum voltage switch",
                V - (self.param.voltage_low_cut - tol),
                pybamm.EventType.SWITCH,
            ))
        self.events.append(
            pybamm.Event(
                "Maximum voltage switch",
                V - (self.param.voltage_high_cut + tol),
                pybamm.EventType.SWITCH,
            ))

        # Power
        I_dim = self.variables["Current [A]"]
        self.variables.update({"Terminal power [W]": I_dim * V_dim})
示例#2
0
    def process_model(self, model, inplace=True, check_model=True):
        """Discretise a model.
        Currently inplace, could be changed to return a new model.

        Parameters
        ----------
        model : :class:`pybamm.BaseModel`
            Model to dicretise. Must have attributes rhs, initial_conditions and
            boundary_conditions (all dicts of {variable: equation})
        inplace : bool, optional
            If True, discretise the model in place. Otherwise, return a new
            discretised model. Default is True.
        check_model : bool, optional
            If True, model checks are performed after discretisation. For large
            systems these checks can be slow, so can be skipped by setting this
            option to False. When developing, testing or debugging it is recommened
            to leave this option as True as it may help to identify any errors.
            Default is True.

        Returns
        -------
        model_disc : :class:`pybamm.BaseModel`
            The discretised model. Note that if ``inplace`` is True, model will
            have also been discretised in place so model == model_disc. If
            ``inplace`` is False, model != model_disc

        Raises
        ------
        :class:`pybamm.ModelError`
            If an empty model is passed (`model.rhs = {}` and `model.algebraic = {}` and
            `model.variables = {}`)

        """
        if model.is_discretised is True:
            raise pybamm.ModelError(
                "Cannot re-discretise a model. "
                "Set 'inplace=False' when first discretising a model to then be able "
                "to discretise it more times (e.g. for convergence studies).")

        pybamm.logger.info("Start discretising {}".format(model.name))

        # Make sure model isn't empty
        if (len(model.rhs) == 0 and len(model.algebraic) == 0
                and len(model.variables) == 0):
            raise pybamm.ModelError("Cannot discretise empty model")
        # Check well-posedness to avoid obscure errors
        model.check_well_posedness()

        # Prepare discretisation
        # set variables (we require the full variable not just id)
        variables = list(model.rhs.keys()) + list(model.algebraic.keys())
        if self.spatial_methods == {} and any(var.domain != []
                                              for var in variables):
            for var in variables:
                if var.domain != []:
                    raise pybamm.DiscretisationError(
                        "Spatial method has not been given "
                        "for variable {} with domain {}".format(
                            var.name, var.domain))

        # Set the y split for variables
        pybamm.logger.info("Set variable slices for {}".format(model.name))
        self.set_variable_slices(variables)
        # Keep a record of y_slices in the model
        model.y_slices = self.y_slices_explicit

        # now add extrapolated external variables to the boundary conditions
        # if required by the spatial method
        self._preprocess_external_variables(model)
        self.set_external_variables(model)

        # set boundary conditions (only need key ids for boundary_conditions)
        pybamm.logger.info("Discretise boundary conditions for {}".format(
            model.name))
        self.bcs = self.process_boundary_conditions(model)
        pybamm.logger.info("Set internal boundary conditions for {}".format(
            model.name))
        self.set_internal_boundary_conditions(model)

        # set up inplace vs not inplace
        if inplace:
            # any changes to model_disc attributes will change model attributes
            # since they point to the same object
            model_disc = model
        else:
            # create an empty copy of the original model
            model_disc = model.new_copy()

        model_disc.bcs = self.bcs

        pybamm.logger.info("Discretise initial conditions for {}".format(
            model.name))
        ics, concat_ics = self.process_initial_conditions(model)
        model_disc.initial_conditions = ics
        model_disc.concatenated_initial_conditions = concat_ics

        # Discretise variables (applying boundary conditions)
        # Note that we **do not** discretise the keys of model.rhs,
        # model.initial_conditions and model.boundary_conditions
        pybamm.logger.info("Discretise variables for {}".format(model.name))
        model_disc.variables = self.process_dict(model.variables)

        # Process parabolic and elliptic equations
        pybamm.logger.info("Discretise model equations for {}".format(
            model.name))
        rhs, concat_rhs, alg, concat_alg = self.process_rhs_and_algebraic(
            model)
        model_disc.rhs, model_disc.concatenated_rhs = rhs, concat_rhs
        model_disc.algebraic, model_disc.concatenated_algebraic = alg, concat_alg

        # Process events
        processed_events = []
        pybamm.logger.info("Discretise events for {}".format(model.name))
        for event in model.events:
            pybamm.logger.debug("Discretise event '{}'".format(event.name))
            processed_event = pybamm.Event(
                event.name, self.process_symbol(event.expression),
                event.event_type)
            processed_events.append(processed_event)
        model_disc.events = processed_events

        # Create mass matrix
        pybamm.logger.info("Create mass matrix for {}".format(model.name))
        model_disc.mass_matrix, model_disc.mass_matrix_inv = self.create_mass_matrix(
            model_disc)

        # Check that resulting model makes sense
        if check_model:
            pybamm.logger.info("Performing model checks for {}".format(
                model.name))
            self.check_model(model_disc)

        pybamm.logger.info("Finish discretising {}".format(model.name))

        # Record that the model has been discretised
        model_disc.is_discretised = True

        return model_disc
示例#3
0
    def process_model(self, unprocessed_model, inplace=True):
        """Replace all instances of a symbol in a model.

        Parameters
        ----------
        unprocessed_model : :class:`pybamm.BaseModel`
            Model to assign parameter values for
        inplace: bool, optional
            If True, replace the parameters in the model in place. Otherwise, return a
            new model with parameter values set. Default is True.
        """
        pybamm.logger.info("Start replacing symbols in {}".format(
            unprocessed_model.name))

        # set up inplace vs not inplace
        if inplace:
            # any changes to unprocessed_model attributes will change model attributes
            # since they point to the same object
            model = unprocessed_model
        else:
            # create a blank model of the same class
            model = unprocessed_model.new_empty_copy()

        new_rhs = {}
        for variable, equation in unprocessed_model.rhs.items():
            pybamm.logger.debug(
                "Replacing symbols in {!r} (rhs)".format(variable))
            new_rhs[self.process_symbol(variable)] = self.process_symbol(
                equation)
        model.rhs = new_rhs

        new_algebraic = {}
        for variable, equation in unprocessed_model.algebraic.items():
            pybamm.logger.debug(
                "Replacing symbols in {!r} (algebraic)".format(variable))
            new_algebraic[self.process_symbol(variable)] = self.process_symbol(
                equation)
        model.algebraic = new_algebraic

        new_initial_conditions = {}
        for variable, equation in unprocessed_model.initial_conditions.items():
            pybamm.logger.debug(
                "Replacing symbols in {!r} (initial conditions)".format(
                    variable))
            new_initial_conditions[self.process_symbol(
                variable)] = self.process_symbol(equation)
        model.initial_conditions = new_initial_conditions

        model.boundary_conditions = self.process_boundary_conditions(
            unprocessed_model)

        new_variables = {}
        for variable, equation in unprocessed_model.variables.items():
            pybamm.logger.debug(
                "Replacing symbols in {!r} (variables)".format(variable))
            new_variables[variable] = self.process_symbol(equation)
        model.variables = new_variables

        new_events = []
        for event in unprocessed_model.events:
            pybamm.logger.debug("Replacing symbols in event'{}''".format(
                event.name))
            new_events.append(
                pybamm.Event(event.name, self.process_symbol(event.expression),
                             event.event_type))
        model.events = new_events

        # Set external variables
        model.external_variables = [
            self.process_symbol(var)
            for var in unprocessed_model.external_variables
        ]

        # Process timescale
        model.timescale = self.process_symbol(unprocessed_model.timescale)

        # Process length scales
        new_length_scales = {}
        for domain, scale in unprocessed_model.length_scales.items():
            new_length_scales[domain] = self.process_symbol(scale)
        model.length_scales = new_length_scales

        pybamm.logger.info("Finish replacing symbols in {}".format(model.name))

        return model
    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)
    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], 1.5)
                np.testing.assert_array_less(solution.y[-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[:, -1])
                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)
示例#6
0
    def set_up_model_for_experiment_new(self, model):
        """
        Set up self.model to be able to run the experiment (new version).
        In this version, a new model is created for each step.

        This increases set-up time since several models to be processed, but
        reduces simulation time since the model formulation is efficient.
        """
        self.op_conds_to_model_and_param = {}
        self.op_conds_to_built_models = None
        for op_cond, op_inputs in zip(self.experiment.operating_conditions,
                                      self._experiment_inputs):
            # Create model for this operating condition if it has not already been seen
            # before
            if op_cond[:2] not in self.op_conds_to_model_and_param:
                if op_inputs["Current switch"] == 1:
                    # Current control
                    # Make a new copy of the model (we will update events later))
                    new_model = model.new_copy()
                else:
                    # Voltage or power control
                    # Create a new model where the current density is now a variable
                    # To do so, we replace all instances of the current density in the
                    # model with a current density variable, which is obtained from the
                    # FunctionControl submodel
                    # create the FunctionControl submodel and extract variables
                    external_circuit_variables = (
                        pybamm.external_circuit.FunctionControl(
                            model.param, None).get_fundamental_variables())

                    # Perform the replacement
                    symbol_replacement_map = {
                        model.variables[name]: variable
                        for name, variable in
                        external_circuit_variables.items()
                    }
                    replacer = pybamm.SymbolReplacer(symbol_replacement_map)
                    new_model = replacer.process_model(model, inplace=False)

                    # Update the algebraic equation and initial conditions for
                    # FunctionControl
                    # This creates an algebraic equation for the current to allow
                    # current, voltage, or power control, together with the appropriate
                    # guess for the initial condition.
                    # External circuit submodels are always equations on the current
                    # The external circuit function should fix either the current, or
                    # the voltage, or a combination (e.g. I*V for power control)
                    i_cell = new_model.variables["Total current density"]
                    new_model.initial_conditions[
                        i_cell] = new_model.param.current_with_time

                    # add current events to the model
                    # current events both negative and positive to catch specification
                    new_model.events.extend([
                        pybamm.Event(
                            "Current cut-off (positive) [A] [experiment]",
                            new_model.variables["Current [A]"] -
                            abs(pybamm.InputParameter("Current cut-off [A]")),
                        ),
                        pybamm.Event(
                            "Current cut-off (negative) [A] [experiment]",
                            new_model.variables["Current [A]"] +
                            abs(pybamm.InputParameter("Current cut-off [A]")),
                        ),
                    ])
                    if op_inputs["Voltage switch"] == 1:
                        new_model.algebraic[i_cell] = constant_voltage(
                            new_model.variables,
                            pybamm.Parameter("Voltage function [V]"),
                        )
                    elif op_inputs["Power switch"] == 1:
                        new_model.algebraic[i_cell] = constant_power(
                            new_model.variables,
                            pybamm.Parameter("Power function [W]"),
                        )

                # add voltage events to the model
                if op_inputs["Power switch"] == 1 or op_inputs[
                        "Current switch"] == 1:
                    new_model.events.append(
                        pybamm.Event(
                            "Voltage cut-off [V] [experiment]",
                            new_model.variables["Terminal voltage [V]"] -
                            op_inputs["Voltage cut-off [V]"] /
                            model.param.n_cells,
                        ))

                # Keep the min and max voltages as safeguards but add some tolerances
                # so that they are not triggered before the voltage limits in the
                # experiment
                for event in new_model.events:
                    if event.name == "Minimum voltage":
                        event._expression += 1
                    elif event.name == "Maximum voltage":
                        event._expression -= 1

                # Update parameter values
                new_parameter_values = self.parameter_values.copy()
                if op_inputs["Current switch"] == 1:
                    new_parameter_values.update({
                        "Current function [A]":
                        op_inputs["Current input [A]"]
                    })
                elif op_inputs["Voltage switch"] == 1:
                    new_parameter_values.update(
                        {
                            "Voltage function [V]":
                            op_inputs["Voltage input [V]"]
                        },
                        check_already_exists=False,
                    )
                elif op_inputs["Power switch"] == 1:
                    new_parameter_values.update(
                        {"Power function [W]": op_inputs["Power input [W]"]},
                        check_already_exists=False,
                    )

                self.op_conds_to_model_and_param[op_cond[:2]] = (
                    new_model,
                    new_parameter_values,
                )
        self.model = model
示例#7
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[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_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 * (t < discontinuity) + 0.1

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

        rate = nonsmooth_rate(pybamm.t)
        mult = 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], 1.5)
            np.testing.assert_array_less(solution.y[-1, :-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)
示例#9
0
    def process_model(self, unprocessed_model, inplace=True):
        """Assign parameter values to a model.
        Currently inplace, could be changed to return a new model.

        Parameters
        ----------
        unprocessed_model : :class:`pybamm.BaseModel`
            Model to assign parameter values for
        inplace: bool, optional
            If True, replace the parameters in the model in place. Otherwise, return a
            new model with parameter values set. Default is True.

        Raises
        ------
        :class:`pybamm.ModelError`
            If an empty model is passed (`model.rhs = {}` and `model.algebraic = {}` and
            `model.variables = {}`)

        """
        pybamm.logger.info("Start setting parameters for {}".format(
            unprocessed_model.name))

        # set up inplace vs not inplace
        if inplace:
            # any changes to unprocessed_model attributes will change model attributes
            # since they point to the same object
            model = unprocessed_model
        else:
            # create a blank model of the same class
            model = unprocessed_model.new_empty_copy()

        if (len(unprocessed_model.rhs) == 0
                and len(unprocessed_model.algebraic) == 0
                and len(unprocessed_model.variables) == 0):
            raise pybamm.ModelError(
                "Cannot process parameters for empty model")

        new_rhs = {}
        for variable, equation in unprocessed_model.rhs.items():
            pybamm.logger.verbose(
                "Processing parameters for {!r} (rhs)".format(variable))
            new_rhs[variable] = self.process_symbol(equation)
        model.rhs = new_rhs

        new_algebraic = {}
        for variable, equation in unprocessed_model.algebraic.items():
            pybamm.logger.verbose(
                "Processing parameters for {!r} (algebraic)".format(variable))
            new_algebraic[variable] = self.process_symbol(equation)
        model.algebraic = new_algebraic

        new_initial_conditions = {}
        for variable, equation in unprocessed_model.initial_conditions.items():
            pybamm.logger.verbose(
                "Processing parameters for {!r} (initial conditions)".format(
                    variable))
            new_initial_conditions[variable] = self.process_symbol(equation)
        model.initial_conditions = new_initial_conditions

        model.boundary_conditions = self.process_boundary_conditions(
            unprocessed_model)

        new_variables = {}
        for variable, equation in unprocessed_model.variables.items():
            pybamm.logger.verbose(
                "Processing parameters for {!r} (variables)".format(variable))
            new_variables[variable] = self.process_symbol(equation)
        model.variables = new_variables

        new_events = []
        for event in unprocessed_model.events:
            pybamm.logger.verbose(
                "Processing parameters for event '{}''".format(event.name))
            new_events.append(
                pybamm.Event(event.name, self.process_symbol(event.expression),
                             event.event_type))

        for event in self.parameter_events:
            pybamm.logger.verbose(
                "Processing parameters for event '{}''".format(event.name))
            new_events.append(
                pybamm.Event(event.name, self.process_symbol(event.expression),
                             event.event_type))

        model.events = new_events

        # Set external variables
        model.external_variables = [
            self.process_symbol(var)
            for var in unprocessed_model.external_variables
        ]

        # Process timescale
        model.timescale = self.process_symbol(unprocessed_model.timescale)

        # Process length scales
        new_length_scales = {}
        for domain, scale in unprocessed_model.length_scales.items():
            new_length_scales[domain] = self.process_symbol(scale)
        model.length_scales = new_length_scales

        pybamm.logger.info("Finish setting parameters for {}".format(
            model.name))

        return model
示例#10
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)))
示例#11
0
    def set_voltage_variables(self):

        ocp_n = self.variables["Negative electrode open circuit potential"]
        ocp_p = self.variables["Positive electrode open circuit potential"]
        ocp_n_av = self.variables[
            "X-averaged negative electrode open circuit potential"]
        ocp_p_av = self.variables[
            "X-averaged positive electrode open circuit potential"]

        ocp_n_dim = self.variables[
            "Negative electrode open circuit potential [V]"]
        ocp_p_dim = self.variables[
            "Positive electrode open circuit potential [V]"]
        ocp_n_av_dim = self.variables[
            "X-averaged negative electrode open circuit potential [V]"]
        ocp_p_av_dim = self.variables[
            "X-averaged positive electrode open circuit potential [V]"]

        ocp_n_left = pybamm.boundary_value(ocp_n, "left")
        ocp_n_left_dim = pybamm.boundary_value(ocp_n_dim, "left")
        ocp_p_right = pybamm.boundary_value(ocp_p, "right")
        ocp_p_right_dim = pybamm.boundary_value(ocp_p_dim, "right")

        ocv_av = ocp_p_av - ocp_n_av
        ocv_av_dim = ocp_p_av_dim - ocp_n_av_dim
        ocv = ocp_p_right - ocp_n_left
        ocv_dim = ocp_p_right_dim - ocp_n_left_dim

        # overpotentials
        eta_r_n_av = self.variables[
            "X-averaged negative electrode reaction overpotential"]
        eta_r_n_av_dim = self.variables[
            "X-averaged negative electrode reaction overpotential [V]"]
        eta_r_p_av = self.variables[
            "X-averaged positive electrode reaction overpotential"]
        eta_r_p_av_dim = self.variables[
            "X-averaged positive electrode reaction overpotential [V]"]

        delta_phi_s_n_av = self.variables[
            "X-averaged negative electrode ohmic losses"]
        delta_phi_s_n_av_dim = self.variables[
            "X-averaged negative electrode ohmic losses [V]"]
        delta_phi_s_p_av = self.variables[
            "X-averaged positive electrode ohmic losses"]
        delta_phi_s_p_av_dim = self.variables[
            "X-averaged positive electrode ohmic losses [V]"]

        delta_phi_s_av = delta_phi_s_p_av - delta_phi_s_n_av
        delta_phi_s_av_dim = delta_phi_s_p_av_dim - delta_phi_s_n_av_dim

        eta_r_av = eta_r_p_av - eta_r_n_av
        eta_r_av_dim = eta_r_p_av_dim - eta_r_n_av_dim

        # SEI film overpotential
        eta_sei_n_av = self.variables[
            "X-averaged negative electrode sei film overpotential"]
        eta_sei_p_av = self.variables[
            "X-averaged positive electrode sei film overpotential"]
        eta_sei_n_av_dim = self.variables[
            "X-averaged negative electrode sei film overpotential [V]"]
        eta_sei_p_av_dim = self.variables[
            "X-averaged positive electrode sei film overpotential [V]"]
        eta_sei_av = eta_sei_n_av + eta_sei_p_av
        eta_sei_av_dim = eta_sei_n_av_dim + eta_sei_p_av_dim

        # TODO: add current collector losses to the voltage in 3D

        self.variables.update({
            "X-averaged open circuit voltage":
            ocv_av,
            "Measured open circuit voltage":
            ocv,
            "X-averaged open circuit voltage [V]":
            ocv_av_dim,
            "Measured open circuit voltage [V]":
            ocv_dim,
            "X-averaged reaction overpotential":
            eta_r_av,
            "X-averaged reaction overpotential [V]":
            eta_r_av_dim,
            "X-averaged sei film overpotential":
            eta_sei_av,
            "X-averaged sei film overpotential [V]":
            eta_sei_av_dim,
            "X-averaged solid phase ohmic losses":
            delta_phi_s_av,
            "X-averaged solid phase ohmic losses [V]":
            delta_phi_s_av_dim,
        })

        # Battery-wide variables
        V_dim = self.variables["Terminal voltage [V]"]
        eta_e_av = self.variables.get("X-averaged electrolyte ohmic losses", 0)
        eta_c_av = self.variables.get("X-averaged concentration overpotential",
                                      0)
        eta_e_av_dim = self.variables.get(
            "X-averaged electrolyte ohmic losses [V]", 0)
        eta_c_av_dim = self.variables.get(
            "X-averaged concentration overpotential [V]", 0)
        num_cells = pybamm.Parameter(
            "Number of cells connected in series to make a battery")
        self.variables.update({
            "X-averaged battery open circuit voltage [V]":
            ocv_av_dim * num_cells,
            "Measured battery open circuit voltage [V]":
            ocv_dim * num_cells,
            "X-averaged battery reaction overpotential [V]":
            eta_r_av_dim * num_cells,
            "X-averaged battery solid phase ohmic losses [V]":
            delta_phi_s_av_dim * num_cells,
            "X-averaged battery electrolyte ohmic losses [V]":
            eta_e_av_dim * num_cells,
            "X-averaged battery concentration overpotential [V]":
            eta_c_av_dim * num_cells,
            "Battery voltage [V]":
            V_dim * num_cells,
        })
        # Variables for calculating the equivalent circuit model (ECM) resistance
        # Need to compare OCV to initial value to capture this as an overpotential
        ocv_init = self.param.U_p(
            self.param.c_p_init(1), self.param.T_init) - self.param.U_n(
                self.param.c_n_init(0), self.param.T_init)
        ocv_init_dim = (self.param.U_p_ref - self.param.U_n_ref +
                        self.param.potential_scale * ocv_init)
        eta_ocv = ocv - ocv_init
        eta_ocv_dim = ocv_dim - ocv_init_dim
        # Current collector current density for working out euiqvalent resistance
        # based on Ohm's Law
        i_cc = self.variables["Current collector current density"]
        i_cc_dim = self.variables["Current collector current density [A.m-2]"]
        # Gather all overpotentials
        v_ecm = -(eta_ocv + eta_r_av + eta_c_av + eta_e_av + delta_phi_s_av)
        v_ecm_dim = -(eta_ocv_dim + eta_r_av_dim + eta_c_av_dim +
                      eta_e_av_dim + delta_phi_s_av_dim)
        # Current collector area for turning resistivity into resistance
        A_cc = self.param.A_cc
        self.variables.update({
            "Change in measured open circuit voltage":
            eta_ocv,
            "Change in measured open circuit voltage [V]":
            eta_ocv_dim,
            "Local ECM resistance":
            v_ecm / (i_cc * A_cc),
            "Local ECM resistance [Ohm]":
            v_ecm_dim / (i_cc_dim * A_cc),
        })

        # Cut-off voltage
        voltage = self.variables["Terminal voltage"]
        self.events.append(
            pybamm.Event(
                "Minimum voltage",
                voltage - self.param.voltage_low_cut,
                pybamm.EventType.TERMINATION,
            ))
        self.events.append(
            pybamm.Event(
                "Maximum voltage",
                voltage - self.param.voltage_high_cut,
                pybamm.EventType.TERMINATION,
            ))

        # Power
        I_dim = self.variables["Current [A]"]
        self.variables.update({"Terminal power [W]": I_dim * V_dim})
    def __init__(self, name="Doyle-Fuller-Newman half cell model", options=None):
        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
        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"]

        if working_electrode == "negative":
            R_w_typ = param.R_n_typ
        else:
            R_w_typ = param.R_p_typ

        # Set default length scales
        self.length_scales = {
            "working electrode": param.L_x,
            "separator": param.L_x,
            "working particle": R_w_typ,
            "current collector y": param.L_z,
            "current collector z": param.L_z,
        }

        ######################
        # 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.
        c_e_s = pybamm.Variable(
            "Separator electrolyte concentration", domain="separator"
        )
        c_e_w = pybamm.Variable(
            "Working electrolyte concentration", domain="working electrode"
        )
        c_e = pybamm.concatenation(c_e_s, c_e_w)
        c_s_w = pybamm.Variable(
            "Working particle concentration",
            domain="working particle",
            auxiliary_domains={"secondary": "working electrode"},
        )
        phi_s_w = pybamm.Variable(
            "Working electrode potential", domain="working electrode"
        )
        phi_e_s = pybamm.Variable("Separator electrolyte potential", domain="separator")
        phi_e_w = pybamm.Variable(
            "Working electrolyte potential", domain="working electrode"
        )
        phi_e = pybamm.concatenation(phi_e_s, phi_e_w)

        # Constant temperature
        T = param.T_init

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

        # Current density
        i_cell = param.current_with_time

        # Define particle surface concentration
        # 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_w = pybamm.surf(c_s_w)

        # Define parameters. We need to assemble them differently depending on the
        # working electrode

        if working_electrode == "negative":
            # 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_s = pybamm.PrimaryBroadcast(
                pybamm.Parameter("Separator porosity"), "separator"
            )
            eps_w = pybamm.PrimaryBroadcast(
                pybamm.Parameter("Negative electrode porosity"), "working electrode"
            )
            b_e_s = param.b_e_s
            b_e_w = param.b_e_n

            # Interfacial reactions
            j0_w = param.j0_n(c_e_w, c_s_surf_w, T) / param.C_r_n
            U_w = param.U_n
            ne_w = param.ne_n

            # Particle diffusion parameters
            D_w = param.D_n
            C_w = param.C_n
            a_R_w = param.a_R_n
            gamma_w = pybamm.Scalar(1)
            c_w_init = param.c_n_init

            # Electrode equation parameters
            eps_s_w = pybamm.Parameter(
                "Negative electrode active material volume fraction"
            )
            b_s_w = param.b_s_n
            sigma_w = param.sigma_n

            # Other parameters (for outputs)
            c_w_max = param.c_n_max
            U_ref = param.U_n_ref
            phi_s_w_ref = pybamm.Scalar(0)
            L_w = param.L_n

        else:
            # Porosity and Tortuosity
            eps_s = pybamm.PrimaryBroadcast(
                pybamm.Parameter("Separator porosity"), "separator"
            )
            eps_w = pybamm.PrimaryBroadcast(
                pybamm.Parameter("Positive electrode porosity"), "working electrode"
            )
            b_e_s = param.b_e_s
            b_e_w = param.b_e_p

            # Interfacial reactions
            j0_w = param.gamma_p * param.j0_p(c_e_w, c_s_surf_w, T) / param.C_r_p
            U_w = param.U_p
            ne_w = param.ne_p

            # Particle diffusion parameters
            D_w = param.D_p
            C_w = param.C_p
            a_R_w = param.a_R_p
            gamma_w = param.gamma_p
            c_w_init = param.c_p_init

            # Electrode equation parameters
            eps_s_w = pybamm.Parameter(
                "Positive electrode active material volume fraction"
            )
            b_s_w = param.b_s_p
            sigma_w = param.sigma_p

            # Other parameters (for outputs)
            c_w_max = param.c_p_max
            U_ref = param.U_p_ref
            phi_s_w_ref = param.U_p_ref - param.U_n_ref
            L_w = param.L_p

        eps = pybamm.concatenation(eps_s, eps_w)
        tor = pybamm.concatenation(eps_s ** b_e_s, eps_w ** b_e_w)

        j_w = (
            2 * j0_w * pybamm.sinh(ne_w / 2 * (phi_s_w - phi_e_w - U_w(c_s_surf_w, T)))
        )
        j_s = pybamm.PrimaryBroadcast(0, "separator")
        j = pybamm.concatenation(j_s, j_w)

        ######################
        # 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_w = -D_w(c_s_w, T) * pybamm.grad(c_s_w)
        self.rhs[c_s_w] = -(1 / C_w) * pybamm.div(N_s_w)

        # Boundary conditions must be provided for equations with spatial
        # derivatives
        self.boundary_conditions[c_s_w] = {
            "left": (pybamm.Scalar(0), "Neumann"),
            "right": (
                -C_w * j_w / a_R_w / gamma_w / D_w(c_s_surf_w, T),
                "Neumann",
            ),
        }

        # c_w_init can in general be a function of x
        # Note the broadcasting, for domains
        x_w = pybamm.PrimaryBroadcast(half_cell_spatial_vars.x_w, "working particle")
        self.initial_conditions[c_s_w] = c_w_init(x_w)

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

        ######################
        # Current in the solid
        ######################
        sigma_eff_w = sigma_w * eps_s_w ** b_s_w
        i_s_w = -sigma_eff_w * pybamm.grad(phi_s_w)
        self.boundary_conditions[phi_s_w] = {
            "left": (pybamm.Scalar(0), "Neumann"),
            "right": (
                i_cell / pybamm.boundary_value(-sigma_eff_w, "right"),
                "Neumann",
            ),
        }
        self.algebraic[phi_s_w] = pybamm.div(i_s_w) + j_w
        # 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_w] = U_w(c_w_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, T)) * j / param.gamma_e
        )
        dce_dx = (
            -(1 - param.t_plus(c_e, T))
            * i_cell
            * param.C_e
            / (tor * param.gamma_e * param.D_e(c_e, T))
        )

        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

        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]"
        )

        vdrop_cell = pybamm.boundary_value(phi_s_w, "right") - ref_potential
        vdrop_Li = -(
            2 * pybamm.arcsinh(i_cell * i_typ / j_Li)
            + L_Li * i_typ * i_cell / (sigma_Li * pot)
        )
        voltage = vdrop_cell + vdrop_Li

        c_e_total = pybamm.x_average(eps * c_e)
        c_s_surf_w_av = pybamm.x_average(c_s_surf_w)

        c_s_rav = pybamm.r_average(c_s_w)
        c_s_vol_av = pybamm.x_average(eps_s_w * c_s_rav)

        # 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,
            "Working particle surface concentration": c_s_surf_w,
            "X-averaged working particle surface concentration": c_s_surf_w_av,
            "Working particle concentration": c_s_w,
            "Working particle surface concentration [mol.m-3]": c_w_max * c_s_surf_w,
            "X-averaged working particle surface concentration "
            "[mol.m-3]": c_w_max * c_s_surf_w_av,
            "Working particle concentration [mol.m-3]": c_w_max * c_s_w,
            "Total lithium in working electrode": c_s_vol_av,
            "Total lithium in working electrode [mol]": c_s_vol_av
            * c_w_max
            * L_w
            * param.A_cc,
            "Electrolyte concentration": c_e,
            "Electrolyte concentration [mol.m-3]": param.c_e_typ * c_e,
            "Total electrolyte concentration": c_e_total,
            "Total electrolyte concentration [mol]": c_e_total
            * param.c_e_typ
            * L_w
            * param.L_s
            * param.A_cc,
            "Current [A]": I,
            "Working electrode potential": phi_s_w,
            "Working electrode potential [V]": phi_s_w_ref + pot * phi_s_w,
            "Working electrode open circuit potential": U_w(c_s_surf_w, T),
            "Working electrode open circuit potential [V]": U_ref
            + pot * U_w(c_s_surf_w, T),
            "Electrolyte potential": phi_e,
            "Electrolyte potential [V]": -param.U_n_ref + pot * phi_e,
            "Voltage drop in the cell": vdrop_cell,
            "Voltage drop in the cell [V]": phi_s_w_ref
            + param.U_n_ref
            + pot * vdrop_cell,
            "Terminal voltage": voltage,
            "Terminal voltage [V]": phi_s_w_ref + param.U_n_ref + pot * voltage,
        }
示例#13
0
    def set_up(self, model, inputs=None):
        """Unpack model, perform checks, simplify and calculate jacobian.

        Parameters
        ----------
        model : :class:`pybamm.BaseModel`
            The model whose solution to calculate. Must have attributes rhs and
            initial_conditions
        inputs : dict, optional
            Any input parameters to pass to the model when solving

        """
        inputs = inputs or {}
        y0 = model.concatenated_initial_conditions.evaluate(0, None, inputs)

        # Set model timescale
        model.timescale_eval = model.timescale.evaluate(u=inputs)

        # Check model.algebraic for ode solvers
        if self.ode_solver is True and len(model.algebraic) > 0:
            raise pybamm.SolverError(
                "Cannot use ODE solver '{}' to solve DAE model".format(
                    self.name))

        if self.ode_solver is True:
            self.root_method = None
        if (isinstance(self, pybamm.CasadiSolver) or self.root_method
                == "casadi") and model.convert_to_format != "casadi":
            pybamm.logger.warning(
                f"Converting {model.name} to CasADi for solving with CasADi solver"
            )
            model.convert_to_format = "casadi"

        if model.convert_to_format != "casadi":
            simp = pybamm.Simplification()
            # Create Jacobian from concatenated rhs and algebraic
            y = pybamm.StateVector(slice(0, np.size(y0)))
            # set up Jacobian object, for re-use of dict
            jacobian = pybamm.Jacobian()
        else:
            # Convert model attributes to casadi
            t_casadi = casadi.MX.sym("t")
            y_diff = casadi.MX.sym(
                "y_diff", len(model.concatenated_rhs.evaluate(0, y0, inputs)))
            y_alg = casadi.MX.sym(
                "y_alg",
                len(model.concatenated_algebraic.evaluate(0, y0, inputs)))
            y_casadi = casadi.vertcat(y_diff, y_alg)
            u_casadi = {}
            for name, value in inputs.items():
                if isinstance(value, numbers.Number):
                    u_casadi[name] = casadi.MX.sym(name)
                else:
                    u_casadi[name] = casadi.MX.sym(name, value.shape[0])
            u_casadi_stacked = casadi.vertcat(*[u for u in u_casadi.values()])

        def process(func, name, use_jacobian=None):
            def report(string):
                # don't log event conversion
                if "event" not in string:
                    pybamm.logger.info(string)

            if use_jacobian is None:
                use_jacobian = model.use_jacobian
            if model.convert_to_format != "casadi":
                # Process with pybamm functions
                if model.use_simplify:
                    report(f"Simplifying {name}")
                    func = simp.simplify(func)
                if use_jacobian:
                    report(f"Calculating jacobian for {name}")
                    jac = jacobian.jac(func, y)
                    if model.use_simplify:
                        report(f"Simplifying jacobian for {name}")
                        jac = simp.simplify(jac)
                    if model.convert_to_format == "python":
                        report(f"Converting jacobian for {name} to python")
                        jac = pybamm.EvaluatorPython(jac)
                    jac = jac.evaluate
                else:
                    jac = None
                if model.convert_to_format == "python":
                    report(f"Converting {name} to python")
                    func = pybamm.EvaluatorPython(func)
                func = func.evaluate
            else:
                # Process with CasADi
                report(f"Converting {name} to CasADi")
                func = func.to_casadi(t_casadi, y_casadi, u_casadi)
                if use_jacobian:
                    report(f"Calculating jacobian for {name} using CasADi")
                    jac_casadi = casadi.jacobian(func, y_casadi)
                    jac = casadi.Function(
                        name, [t_casadi, y_casadi, u_casadi_stacked],
                        [jac_casadi])
                else:
                    jac = None
                func = casadi.Function(name,
                                       [t_casadi, y_casadi, u_casadi_stacked],
                                       [func])
            if name == "residuals":
                func_call = Residuals(func, name, model)
            else:
                func_call = SolverCallable(func, name, model)
            func_call.set_inputs(inputs)
            if jac is not None:
                jac_call = SolverCallable(jac, name + "_jac", model)
                jac_call.set_inputs(inputs)
            else:
                jac_call = None
            return func, func_call, jac_call

        # Check for heaviside functions in rhs and algebraic and add discontinuity
        # events if these exist.
        # Note: only checks for the case of t < X, t <= X, X < t, or X <= t, but also
        # accounts for the fact that t might be dimensional
        # Only do this for DAE models as ODE models can deal with discontinuities fine
        if len(model.algebraic) > 0:
            for symbol in itertools.chain(
                    model.concatenated_rhs.pre_order(),
                    model.concatenated_algebraic.pre_order(),
            ):
                if isinstance(symbol, pybamm.Heaviside):
                    # Dimensionless
                    if symbol.right.id == pybamm.t.id:
                        expr = symbol.left
                    elif symbol.left.id == pybamm.t.id:
                        expr = symbol.right
                    # Dimensional
                    elif symbol.right.id == (pybamm.t * model.timescale).id:
                        expr = symbol.left.new_copy(
                        ) / symbol.right.right.new_copy()
                    elif symbol.left.id == (pybamm.t * model.timescale).id:
                        expr = symbol.right.new_copy(
                        ) / symbol.left.right.new_copy()

                    model.events.append(
                        pybamm.Event(str(symbol), expr.new_copy(),
                                     pybamm.EventType.DISCONTINUITY))

        # Process rhs, algebraic and event expressions
        rhs, rhs_eval, jac_rhs = process(model.concatenated_rhs, "RHS")
        algebraic, algebraic_eval, jac_algebraic = process(
            model.concatenated_algebraic, "algebraic")
        terminate_events_eval = [
            process(event.expression, "event", use_jacobian=False)[1]
            for event in model.events
            if event.event_type == pybamm.EventType.TERMINATION
        ]

        # discontinuity events are evaluated before the solver is called, so don't need
        # to process them
        discontinuity_events_eval = [
            event for event in model.events
            if event.event_type == pybamm.EventType.DISCONTINUITY
        ]

        # Add the solver attributes
        model.rhs_eval = rhs_eval
        model.algebraic_eval = algebraic_eval
        model.jac_algebraic_eval = jac_algebraic
        model.terminate_events_eval = terminate_events_eval
        model.discontinuity_events_eval = discontinuity_events_eval

        # Save CasADi functions for the CasADi solver
        # Note: when we pass to casadi the ode part of the problem must be in explicit
        # form so we pre-multiply by the inverse of the mass matrix
        if self.root_method == "casadi" or isinstance(self,
                                                      pybamm.CasadiSolver):
            mass_matrix_inv = casadi.MX(model.mass_matrix_inv.entries)
            explicit_rhs = mass_matrix_inv @ rhs(t_casadi, y_casadi,
                                                 u_casadi_stacked)
            model.casadi_rhs = casadi.Function(
                "rhs", [t_casadi, y_casadi, u_casadi_stacked], [explicit_rhs])
            model.casadi_algebraic = algebraic
        # Calculate consistent initial conditions for the algebraic equations
        if len(model.algebraic) > 0:
            all_states = pybamm.NumpyConcatenation(
                model.concatenated_rhs, model.concatenated_algebraic)
            # Process again, uses caching so should be quick
            residuals, residuals_eval, jacobian_eval = process(
                all_states, "residuals")
            model.residuals_eval = residuals_eval
            model.jacobian_eval = jacobian_eval
            y0_guess = y0.flatten()
            model.y0 = self.calculate_consistent_state(model, 0, y0_guess,
                                                       inputs)
        else:
            # can use DAE solver to solve ODE model
            model.residuals_eval = Residuals(rhs, "residuals", model)
            model.jacobian_eval = jac_rhs
            model.y0 = y0.flatten()

        pybamm.logger.info("Finish solver set-up")
示例#14
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),
        ]
示例#15
0
    def set_up(self, model, inputs=None, t_eval=None):
        """Unpack model, perform checks, simplify and calculate jacobian.

        Parameters
        ----------
        model : :class:`pybamm.BaseModel`
            The model whose solution to calculate. Must have attributes rhs and
            initial_conditions
        inputs : dict, optional
            Any input parameters to pass to the model when solving
        t_eval : numeric type, optional
            The times (in seconds) at which to compute the solution

        """

        # Check model.algebraic for ode solvers
        if self.ode_solver is True and len(model.algebraic) > 0:
            raise pybamm.SolverError(
                "Cannot use ODE solver '{}' to solve DAE model".format(
                    self.name))
        # Check model.rhs for algebraic solvers
        if self.algebraic_solver is True and len(model.rhs) > 0:
            raise pybamm.SolverError(
                """Cannot use algebraic solver to solve model with time derivatives"""
            )
        # casadi solver won't allow solving algebraic model so we have to raise an
        # error here
        if isinstance(self, pybamm.CasadiSolver) and len(model.rhs) == 0:
            raise pybamm.SolverError(
                "Cannot use CasadiSolver to solve algebraic model, "
                "use CasadiAlgebraicSolver instead")
        # Discretise model if it isn't already discretised
        # This only works with purely 0D models, as otherwise the mesh and spatial
        # method should be specified by the user
        if model.is_discretised is False:
            try:
                disc = pybamm.Discretisation()
                disc.process_model(model)
            except pybamm.DiscretisationError as e:
                raise pybamm.DiscretisationError(
                    "Cannot automatically discretise model, "
                    "model should be discretised before solving ({})".format(
                        e))

        inputs = inputs or {}

        # Set model timescale
        model.timescale_eval = model.timescale.evaluate(inputs=inputs)
        # Set model lengthscales
        model.length_scales_eval = {
            domain: scale.evaluate(inputs=inputs)
            for domain, scale in model.length_scales.items()
        }
        if (isinstance(self,
                       (pybamm.CasadiSolver, pybamm.CasadiAlgebraicSolver))
            ) and model.convert_to_format != "casadi":
            pybamm.logger.warning(
                "Converting {} to CasADi for solving with CasADi solver".
                format(model.name))
            model.convert_to_format = "casadi"
        if (isinstance(self.root_method, pybamm.CasadiAlgebraicSolver)
                and model.convert_to_format != "casadi"):
            pybamm.logger.warning(
                "Converting {} to CasADi for calculating ICs with CasADi".
                format(model.name))
            model.convert_to_format = "casadi"

        if model.convert_to_format != "casadi":
            simp = pybamm.Simplification()
            # Create Jacobian from concatenated rhs and algebraic
            y = pybamm.StateVector(
                slice(0, model.concatenated_initial_conditions.size))
            # set up Jacobian object, for re-use of dict
            jacobian = pybamm.Jacobian()
        else:
            # Convert model attributes to casadi
            t_casadi = casadi.MX.sym("t")
            y_diff = casadi.MX.sym("y_diff", model.concatenated_rhs.size)
            y_alg = casadi.MX.sym("y_alg", model.concatenated_algebraic.size)
            y_casadi = casadi.vertcat(y_diff, y_alg)
            p_casadi = {}
            for name, value in inputs.items():
                if isinstance(value, numbers.Number):
                    p_casadi[name] = casadi.MX.sym(name)
                else:
                    p_casadi[name] = casadi.MX.sym(name, value.shape[0])
            p_casadi_stacked = casadi.vertcat(*[p for p in p_casadi.values()])

        def process(func, name, use_jacobian=None):
            def report(string):
                # don't log event conversion
                if "event" not in string:
                    pybamm.logger.info(string)

            if use_jacobian is None:
                use_jacobian = model.use_jacobian
            if model.convert_to_format != "casadi":
                # Process with pybamm functions
                if model.use_simplify:
                    report(f"Simplifying {name}")
                    func = simp.simplify(func)

                if model.convert_to_format == "jax":
                    report(f"Converting {name} to jax")
                    jax_func = pybamm.EvaluatorJax(func)

                if use_jacobian:
                    report(f"Calculating jacobian for {name}")
                    jac = jacobian.jac(func, y)
                    if model.use_simplify:
                        report(f"Simplifying jacobian for {name}")
                        jac = simp.simplify(jac)
                    if model.convert_to_format == "python":
                        report(f"Converting jacobian for {name} to python")
                        jac = pybamm.EvaluatorPython(jac)
                    elif model.convert_to_format == "jax":
                        report(f"Converting jacobian for {name} to jax")
                        jac = jax_func.get_jacobian()
                    jac = jac.evaluate
                else:
                    jac = None

                if model.convert_to_format == "python":
                    report(f"Converting {name} to python")
                    func = pybamm.EvaluatorPython(func)
                if model.convert_to_format == "jax":
                    report(f"Converting {name} to jax")
                    func = jax_func

                func = func.evaluate

            else:
                # Process with CasADi
                report(f"Converting {name} to CasADi")
                func = func.to_casadi(t_casadi, y_casadi, inputs=p_casadi)
                if use_jacobian:
                    report(f"Calculating jacobian for {name} using CasADi")
                    jac_casadi = casadi.jacobian(func, y_casadi)
                    jac = casadi.Function(
                        name, [t_casadi, y_casadi, p_casadi_stacked],
                        [jac_casadi])
                else:
                    jac = None
                func = casadi.Function(name,
                                       [t_casadi, y_casadi, p_casadi_stacked],
                                       [func])
            if name == "residuals":
                func_call = Residuals(func, name, model)
            else:
                func_call = SolverCallable(func, name, model)
            if jac is not None:
                jac_call = SolverCallable(jac, name + "_jac", model)
            else:
                jac_call = None
            return func, func_call, jac_call

        # Check for heaviside and modulo functions in rhs and algebraic and add
        # discontinuity events if these exist.
        # Note: only checks for the case of t < X, t <= X, X < t, or X <= t, but also
        # accounts for the fact that t might be dimensional
        # Only do this for DAE models as ODE models can deal with discontinuities fine
        if len(model.algebraic) > 0:
            for symbol in itertools.chain(
                    model.concatenated_rhs.pre_order(),
                    model.concatenated_algebraic.pre_order(),
            ):
                if isinstance(symbol, pybamm.Heaviside):
                    found_t = False
                    # Dimensionless
                    if symbol.right.id == pybamm.t.id:
                        expr = symbol.left
                        found_t = True
                    elif symbol.left.id == pybamm.t.id:
                        expr = symbol.right
                        found_t = True
                    # Dimensional
                    elif symbol.right.id == (pybamm.t * model.timescale).id:
                        expr = symbol.left.new_copy(
                        ) / symbol.right.right.new_copy()
                        found_t = True
                    elif symbol.left.id == (pybamm.t * model.timescale).id:
                        expr = symbol.right.new_copy(
                        ) / symbol.left.right.new_copy()
                        found_t = True

                    # Update the events if the heaviside function depended on t
                    if found_t:
                        model.events.append(
                            pybamm.Event(
                                str(symbol),
                                expr.new_copy(),
                                pybamm.EventType.DISCONTINUITY,
                            ))
                elif isinstance(symbol, pybamm.Modulo):
                    found_t = False
                    # Dimensionless
                    if symbol.left.id == pybamm.t.id:
                        expr = symbol.right
                        found_t = True
                    # Dimensional
                    elif symbol.left.id == (pybamm.t * model.timescale).id:
                        expr = symbol.right.new_copy(
                        ) / symbol.left.right.new_copy()
                        found_t = True

                    # Update the events if the modulo function depended on t
                    if found_t:
                        if t_eval is None:
                            N_events = 200
                        else:
                            N_events = t_eval[-1] // expr.value

                        for i in np.arange(N_events):
                            model.events.append(
                                pybamm.Event(
                                    str(symbol),
                                    expr.new_copy() * pybamm.Scalar(i + 1),
                                    pybamm.EventType.DISCONTINUITY,
                                ))

        # Process initial conditions
        initial_conditions = process(
            model.concatenated_initial_conditions,
            "initial_conditions",
            use_jacobian=False,
        )[0]
        init_eval = InitialConditions(initial_conditions, model)

        # Process rhs, algebraic and event expressions
        rhs, rhs_eval, jac_rhs = process(model.concatenated_rhs, "RHS")
        algebraic, algebraic_eval, jac_algebraic = process(
            model.concatenated_algebraic, "algebraic")
        terminate_events_eval = [
            process(event.expression, "event", use_jacobian=False)[1]
            for event in model.events
            if event.event_type == pybamm.EventType.TERMINATION
        ]

        # discontinuity events are evaluated before the solver is called, so don't need
        # to process them
        discontinuity_events_eval = [
            event for event in model.events
            if event.event_type == pybamm.EventType.DISCONTINUITY
        ]

        # Add the solver attributes
        model.init_eval = init_eval
        model.rhs_eval = rhs_eval
        model.algebraic_eval = algebraic_eval
        model.jac_algebraic_eval = jac_algebraic
        model.terminate_events_eval = terminate_events_eval
        model.discontinuity_events_eval = discontinuity_events_eval

        # Calculate initial conditions
        model.y0 = init_eval(inputs)

        # Save CasADi functions for the CasADi solver
        # Note: when we pass to casadi the ode part of the problem must be in explicit
        # form so we pre-multiply by the inverse of the mass matrix
        if isinstance(
                self.root_method, pybamm.CasadiAlgebraicSolver) or isinstance(
                    self, (pybamm.CasadiSolver, pybamm.CasadiAlgebraicSolver)):
            # can use DAE solver to solve model with algebraic equations only
            if len(model.rhs) > 0:
                mass_matrix_inv = casadi.MX(model.mass_matrix_inv.entries)
                explicit_rhs = mass_matrix_inv @ rhs(t_casadi, y_casadi,
                                                     p_casadi_stacked)
                model.casadi_rhs = casadi.Function(
                    "rhs", [t_casadi, y_casadi, p_casadi_stacked],
                    [explicit_rhs])
            model.casadi_algebraic = algebraic
        if len(model.rhs) == 0:
            # No rhs equations: residuals is algebraic only
            model.residuals_eval = Residuals(algebraic, "residuals", model)
            model.jacobian_eval = jac_algebraic
        elif len(model.algebraic) == 0:
            # No algebraic equations: residuals is rhs only
            model.residuals_eval = Residuals(rhs, "residuals", model)
            model.jacobian_eval = jac_rhs
        # Calculate consistent initial conditions for the algebraic equations
        else:
            all_states = pybamm.NumpyConcatenation(
                model.concatenated_rhs, model.concatenated_algebraic)
            # Process again, uses caching so should be quick
            residuals_eval, jacobian_eval = process(all_states,
                                                    "residuals")[1:]
            model.residuals_eval = residuals_eval
            model.jacobian_eval = jacobian_eval

        pybamm.logger.info("Finish solver set-up")
示例#16
0
    def set_voltage_variables(self):

        ocp_n = self.variables["Negative electrode open circuit potential"]
        ocp_p = self.variables["Positive electrode open circuit potential"]
        ocp_n_av = self.variables[
            "X-averaged negative electrode open circuit potential"]
        ocp_p_av = self.variables[
            "X-averaged positive electrode open circuit potential"]

        ocp_n_dim = self.variables[
            "Negative electrode open circuit potential [V]"]
        ocp_p_dim = self.variables[
            "Positive electrode open circuit potential [V]"]
        ocp_n_av_dim = self.variables[
            "X-averaged negative electrode open circuit potential [V]"]
        ocp_p_av_dim = self.variables[
            "X-averaged positive electrode open circuit potential [V]"]

        ocp_n_left = pybamm.boundary_value(ocp_n, "left")
        ocp_n_left_dim = pybamm.boundary_value(ocp_n_dim, "left")
        ocp_p_right = pybamm.boundary_value(ocp_p, "right")
        ocp_p_right_dim = pybamm.boundary_value(ocp_p_dim, "right")

        ocv_av = ocp_p_av - ocp_n_av
        ocv_av_dim = ocp_p_av_dim - ocp_n_av_dim
        ocv = ocp_p_right - ocp_n_left
        ocv_dim = ocp_p_right_dim - ocp_n_left_dim

        # overpotentials
        eta_r_n_av = self.variables[
            "X-averaged negative electrode reaction overpotential"]
        eta_r_n_av_dim = self.variables[
            "X-averaged negative electrode reaction overpotential [V]"]
        eta_r_p_av = self.variables[
            "X-averaged positive electrode reaction overpotential"]
        eta_r_p_av_dim = self.variables[
            "X-averaged positive electrode reaction overpotential [V]"]

        delta_phi_s_n_av = self.variables[
            "X-averaged negative electrode ohmic losses"]
        delta_phi_s_n_av_dim = self.variables[
            "X-averaged negative electrode ohmic losses [V]"]
        delta_phi_s_p_av = self.variables[
            "X-averaged positive electrode ohmic losses"]
        delta_phi_s_p_av_dim = self.variables[
            "X-averaged positive electrode ohmic losses [V]"]

        delta_phi_s_av = delta_phi_s_p_av - delta_phi_s_n_av
        delta_phi_s_av_dim = delta_phi_s_p_av_dim - delta_phi_s_n_av_dim

        eta_r_av = eta_r_p_av - eta_r_n_av
        eta_r_av_dim = eta_r_p_av_dim - eta_r_n_av_dim

        # TODO: add current collector losses to the voltage in 3D

        self.variables.update({
            "X-averaged open circuit voltage":
            ocv_av,
            "Measured open circuit voltage":
            ocv,
            "X-averaged open circuit voltage [V]":
            ocv_av_dim,
            "Measured open circuit voltage [V]":
            ocv_dim,
            "X-averaged reaction overpotential":
            eta_r_av,
            "X-averaged reaction overpotential [V]":
            eta_r_av_dim,
            "X-averaged solid phase ohmic losses":
            delta_phi_s_av,
            "X-averaged solid phase ohmic losses [V]":
            delta_phi_s_av_dim,
        })

        # Battery-wide variables
        V_dim = self.variables["Terminal voltage [V]"]
        eta_e_av_dim = self.variables.get(
            "X-averaged electrolyte ohmic losses [V]", 0)
        eta_c_av_dim = self.variables.get(
            "X-averaged concentration overpotential [V]", 0)
        num_cells = pybamm.Parameter(
            "Number of cells connected in series to make a battery")

        self.variables.update({
            "X-averaged battery open circuit voltage [V]":
            ocv_av_dim * num_cells,
            "Measured battery open circuit voltage [V]":
            ocv_dim * num_cells,
            "X-averaged battery reaction overpotential [V]":
            eta_r_av_dim * num_cells,
            "X-averaged battery solid phase ohmic losses [V]":
            delta_phi_s_av_dim * num_cells,
            "X-averaged battery electrolyte ohmic losses [V]":
            eta_e_av_dim * num_cells,
            "X-averaged battery concentration overpotential [V]":
            eta_c_av_dim * num_cells,
            "Battery voltage [V]":
            V_dim * num_cells,
        })

        # Cut-off voltage
        voltage = self.variables["Terminal voltage"]
        self.events.append(
            pybamm.Event(
                "Minimum voltage",
                voltage - self.param.voltage_low_cut,
                pybamm.EventType.TERMINATION,
            ))
        self.events.append(
            pybamm.Event(
                "Maximum voltage",
                voltage - self.param.voltage_high_cut,
                pybamm.EventType.TERMINATION,
            ))

        # Power
        I_dim = self.variables["Current [A]"]
        self.variables.update({"Terminal power [W]": I_dim * V_dim})
示例#17
0
    def __init__(self, name="Basic full 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",
        )

        # Porosity
        eps_n = pybamm.Variable(
            "Negative electrode porosity",
            domain="negative electrode",
        )
        eps_s = pybamm.Variable("Separator porosity", domain="separator")
        eps_p = pybamm.Variable(
            "Positive electrode porosity",
            domain="positive electrode",
        )
        eps = pybamm.Concatenation(eps_n, eps_s, eps_p)

        # Pressure (for convection)
        pressure_n = pybamm.Variable(
            "Negative electrolyte pressure",
            domain="negative electrode",
        )
        pressure_p = pybamm.Variable(
            "Positive electrolyte pressure",
            domain="positive electrode",
        )

        # Constant temperature
        T = param.T_init

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

        # Current density
        i_cell = param.current_with_time

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

        # Interfacial reactions
        j0_n = param.j0_n(c_e_n, T)
        j_n = (2 * j0_n *
               pybamm.sinh(param.ne_n / 2 *
                           (phi_s_n - phi_e_n - param.U_n(c_e_n, T))))
        j0_p = param.j0_p(c_e_p, T)
        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_e_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)

        ######################
        # Convection
        ######################
        v_n = -pybamm.grad(pressure_n)
        v_p = -pybamm.grad(pressure_p)
        l_s = pybamm.geometric_parameters.l_s
        l_n = pybamm.geometric_parameters.l_n
        x_s = pybamm.SpatialVariable("x_s", domain="separator")

        # Difference in negative and positive electrode velocities determines the
        # velocity in the separator
        v_n_right = param.beta_n * i_cell
        v_p_left = param.beta_p * i_cell
        d_v_s__dx = (v_p_left - v_n_right) / l_s

        # Simple formula for velocity in the separator
        div_V_s = -d_v_s__dx
        v_s = d_v_s__dx * (x_s - l_n) + v_n_right

        # v is the velocity in the x-direction
        # div_V is the divergence of the velocity in the yz-directions
        v = pybamm.Concatenation(v_n, v_s, v_p)
        div_V = pybamm.Concatenation(
            pybamm.PrimaryBroadcast(0, "negative electrode"),
            pybamm.PrimaryBroadcast(div_V_s, "separator"),
            pybamm.PrimaryBroadcast(0, "positive electrode"),
        )
        # Simple formula for velocity in the separator
        self.algebraic[pressure_n] = pybamm.div(v_n) - param.beta_n * j_n
        self.algebraic[pressure_p] = pybamm.div(v_p) - param.beta_p * j_p
        self.boundary_conditions[pressure_n] = {
            "left": (pybamm.Scalar(0), "Neumann"),
            "right": (pybamm.Scalar(0), "Dirichlet"),
        }
        self.boundary_conditions[pressure_p] = {
            "left": (pybamm.Scalar(0), "Dirichlet"),
            "right": (pybamm.Scalar(0), "Neumann"),
        }
        self.initial_conditions[pressure_n] = pybamm.Scalar(0)
        self.initial_conditions[pressure_p] = pybamm.Scalar(0)

        ######################
        # 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_e_init,
                                                    param.T_init)

        ######################
        # 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
        self.initial_conditions[phi_s_n] = pybamm.Scalar(0)
        self.initial_conditions[phi_s_p] = param.U_p(
            param.c_e_init, param.T_init) - param.U_n(param.c_e_init,
                                                      param.T_init)

        ######################
        # Porosity
        ######################
        beta_surf = pybamm.Concatenation(
            pybamm.PrimaryBroadcast(param.beta_surf_n, "negative electrode"),
            pybamm.PrimaryBroadcast(0, "separator"),
            pybamm.PrimaryBroadcast(param.beta_surf_p, "positive electrode"),
        )
        deps_dt = -beta_surf * j
        self.rhs[eps] = deps_dt
        self.initial_conditions[eps] = param.epsilon_init
        self.events.extend([
            pybamm.Event("Zero negative electrode porosity cut-off",
                         pybamm.min(eps_n)),
            pybamm.Event("Max negative electrode porosity cut-off",
                         pybamm.max(eps_n) - 1),
            pybamm.Event("Zero positive electrode porosity cut-off",
                         pybamm.min(eps_p)),
            pybamm.Event("Max positive electrode porosity cut-off",
                         pybamm.max(eps_p) - 1),
        ])

        ######################
        # Electrolyte concentration
        ######################
        N_e = (-tor * param.D_e(c_e, T) * pybamm.grad(c_e) +
               param.C_e * param.t_plus(c_e) * i_e / param.gamma_e +
               param.C_e * c_e * v)
        s = pybamm.Concatenation(
            pybamm.PrimaryBroadcast(param.s_plus_n_S, "negative electrode"),
            pybamm.PrimaryBroadcast(0, "separator"),
            pybamm.PrimaryBroadcast(param.s_plus_p_S, "positive electrode"),
        )
        self.rhs[c_e] = (1 / eps) * (-pybamm.div(N_e) / param.C_e +
                                     s * j / param.gamma_e - c_e * deps_dt -
                                     c_e * div_V)
        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
        pot = param.potential_scale

        self.variables = {
            "Electrolyte concentration":
            c_e,
            "Current [A]":
            I,
            "Negative electrode potential [V]":
            pot * phi_s_n,
            "Electrolyte potential [V]":
            -param.U_n_ref + pot * phi_e,
            "Positive electrode potential [V]":
            param.U_p_ref - param.U_n_ref + pot * phi_s_p,
            "Terminal voltage [V]":
            param.U_p_ref - param.U_n_ref + pot * voltage,
            "x [m]":
            pybamm.standard_spatial_vars.x * param.L_x,
            "x":
            pybamm.standard_spatial_vars.x,
            "Porosity":
            eps,
            "Volume-averaged velocity":
            v,
            "X-averaged separator transverse volume-averaged velocity":
            div_V_s,
        }
        self.events.extend([
            pybamm.Event("Minimum voltage", voltage - param.voltage_low_cut),
            pybamm.Event("Maximum voltage", voltage - param.voltage_high_cut),
        ])
    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)
示例#19
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) * 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),
        ]
示例#20
0
    def set_up_model_for_experiment_old(self, model):
        """
        Set up self.model to be able to run the experiment (old version).
        In this version, a single model is created which can then be called with
        different inputs for current-control, voltage-control, or power-control.

        This reduces set-up time since only one model needs to be processed, but
        increases simulation time since the model formulation is inefficient
        """
        # Create a new model where the current density is now a variable
        # To do so, we replace all instances of the current density in the
        # model with a current density variable, which is obtained from the
        # FunctionControl submodel
        # create the FunctionControl submodel and extract variables
        external_circuit_variables = pybamm.external_circuit.FunctionControl(
            model.param, None).get_fundamental_variables()

        # Perform the replacement
        symbol_replacement_map = {
            model.variables[name]: variable
            for name, variable in external_circuit_variables.items()
        }
        replacer = pybamm.SymbolReplacer(symbol_replacement_map)
        new_model = replacer.process_model(model, inplace=False)

        # Update the algebraic equation and initial conditions for FunctionControl
        # This creates an algebraic equation for the current to allow current, voltage,
        # or power control, together with the appropriate guess for the
        # initial condition.
        # External circuit submodels are always equations on the current
        # The external circuit function should fix either the current, or the voltage,
        # or a combination (e.g. I*V for power control)
        i_cell = new_model.variables["Total current density"]
        new_model.initial_conditions[
            i_cell] = new_model.param.current_with_time
        new_model.algebraic[
            i_cell] = constant_current_constant_voltage_constant_power(
                new_model.variables)

        # Remove upper and lower voltage cut-offs that are *not* part of the experiment
        new_model.events = [
            event for event in model.events
            if event.name not in ["Minimum voltage", "Maximum voltage"]
        ]
        # add current and voltage events to the model
        # current events both negative and positive to catch specification
        new_model.events.extend([
            pybamm.Event(
                "Current cut-off (positive) [A] [experiment]",
                new_model.variables["Current [A]"] -
                abs(pybamm.InputParameter("Current cut-off [A]")),
            ),
            pybamm.Event(
                "Current cut-off (negative) [A] [experiment]",
                new_model.variables["Current [A]"] +
                abs(pybamm.InputParameter("Current cut-off [A]")),
            ),
            pybamm.Event(
                "Voltage cut-off [V] [experiment]",
                new_model.variables["Terminal voltage [V]"] -
                pybamm.InputParameter("Voltage cut-off [V]") /
                model.param.n_cells,
            ),
        ])

        self.model = new_model

        self.op_conds_to_model_and_param = {
            op_cond[:2]: (new_model, self.parameter_values)
            for op_cond in set(self.experiment.operating_conditions)
        }
        self.op_conds_to_built_models = None
 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))
示例#22
0
    def set_up_experiment(self, model, experiment):
        """
        Set up a simulation to run with an experiment. This creates a dictionary of
        inputs (current/voltage/power, running time, stopping condition) for each
        operating condition in the experiment. The model will then be solved by
        integrating the model successively with each group of inputs, one group at a
        time.
        """
        self.operating_mode = "with experiment"
        self.model = model.new_copy(
            options={
                **model.options,
                "operating mode":
                constant_current_constant_voltage_constant_power,
            })
        if not isinstance(experiment, pybamm.Experiment):
            raise TypeError(
                "experiment must be a pybamm `Experiment` instance")
        # Save the experiment
        self.experiment = experiment
        # Update parameter values with experiment parameters
        self._parameter_values.update(experiment.parameters)
        # Create a new submodel for each set of operating conditions and update
        # parameters and events accordingly
        self._experiment_inputs = []
        self._experiment_times = []
        for op, events in zip(experiment.operating_conditions,
                              experiment.events):
            if op[1] in ["A", "C"]:
                # Update inputs for constant current
                if op[1] == "A":
                    I = op[0]
                else:
                    # Scale C-rate with capacity to obtain current
                    capacity = self._parameter_values["Cell capacity [A.h]"]
                    I = op[0] * capacity
                operating_inputs = {
                    "Current switch": 1,
                    "Voltage switch": 0,
                    "Power switch": 0,
                    "Current input [A]": I,
                    "Voltage input [V]": 0,  # doesn't matter
                    "Power input [W]": 0,  # doesn't matter
                }
            elif op[1] == "V":
                # Update inputs for constant voltage
                V = op[0]
                operating_inputs = {
                    "Current switch": 0,
                    "Voltage switch": 1,
                    "Power switch": 0,
                    "Current input [A]": 0,  # doesn't matter
                    "Voltage input [V]": V,
                    "Power input [W]": 0,  # doesn't matter
                }
            elif op[1] == "W":
                # Update inputs for constant power
                P = op[0]
                operating_inputs = {
                    "Current switch": 0,
                    "Voltage switch": 0,
                    "Power switch": 1,
                    "Current input [A]": 0,  # doesn't matter
                    "Voltage input [V]": 0,  # doesn't matter
                    "Power input [W]": P,
                }
            # Update period
            operating_inputs["period"] = op[3]
            # Update events
            if events is None:
                # make current and voltage values that won't be hit
                operating_inputs.update({
                    "Current cut-off [A]": -1e10,
                    "Voltage cut-off [V]": -1e10
                })
            elif events[1] in ["A", "C"]:
                # update current cut-off, make voltage a value that won't be hit
                if events[1] == "A":
                    I = events[0]
                else:
                    # Scale C-rate with capacity to obtain current
                    capacity = self._parameter_values["Cell capacity [A.h]"]
                    I = events[0] * capacity
                operating_inputs.update({
                    "Current cut-off [A]": I,
                    "Voltage cut-off [V]": -1e10
                })
            elif events[1] == "V":
                # update voltage cut-off, make current a value that won't be hit
                V = events[0]
                operating_inputs.update({
                    "Current cut-off [A]": -1e10,
                    "Voltage cut-off [V]": V
                })

            self._experiment_inputs.append(operating_inputs)
            # Add time to the experiment times
            dt = op[2]
            if dt is None:
                # max simulation time: 1 week
                dt = 7 * 24 * 3600
            self._experiment_times.append(dt)

        # add current and voltage events to the model
        # current events both negative and positive to catch specification
        n_cells = pybamm.electrical_parameters.n_cells
        self.model.events.extend([
            pybamm.Event(
                "Current cut-off (positive) [A] [experiment]",
                self.model.variables["Current [A]"] -
                abs(pybamm.InputParameter("Current cut-off [A]")),
            ),
            pybamm.Event(
                "Current cut-off (negative) [A] [experiment]",
                self.model.variables["Current [A]"] +
                abs(pybamm.InputParameter("Current cut-off [A]")),
            ),
            pybamm.Event(
                "Voltage cut-off [V] [experiment]",
                self.model.variables["Terminal voltage [V]"] -
                pybamm.InputParameter("Voltage cut-off [V]") / n_cells,
            ),
        ])
示例#23
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_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_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) * 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,
        }