Esempio n. 1
0
    def solve(
        self,
        model,
        t_eval=None,
        external_variables=None,
        inputs=None,
        initial_conditions=None,
        nproc=None,
    ):
        """
        Execute the solver setup and calculate the solution of the model at
        specified times.

        Parameters
        ----------
        model : :class:`pybamm.BaseModel`
            The model whose solution to calculate. Must have attributes rhs and
            initial_conditions
        t_eval : numeric type
            The times (in seconds) at which to compute the solution
        external_variables : dict
            A dictionary of external variables and their corresponding
            values at the current time
        inputs : dict or list, optional
            A dictionary or list of dictionaries describing any input parameters to
            pass to the model when solving
        initial_conditions : :class:`pybamm.Symbol`, optional
            Initial conditions to use when solving the model. If None (default),
            `model.concatenated_initial_conditions` is used. Otherwise, must be a symbol
            of size `len(model.rhs) + len(model.algebraic)`.
        nproc : int, optional
            Number of processes to use when solving for more than one set of input
            parameters. Defaults to value returned by "os.cpu_count()".

        Returns
        -------
        :class:`pybamm.Solution` or list of :class:`pybamm.Solution` objects.
             If type of `inputs` is `list`, return a list of corresponding
             :class:`pybamm.Solution` objects.

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

        """
        pybamm.logger.info("Start solving {} with {}".format(model.name, self.name))

        # Make sure model isn't empty
        if len(model.rhs) == 0 and len(model.algebraic) == 0:
            if not isinstance(self, pybamm.DummySolver):
                raise pybamm.ModelError(
                    "Cannot solve empty model, use `pybamm.DummySolver` instead"
                )

        # t_eval can only be None if the solver is an algebraic solver. In that case
        # set it to 0
        if t_eval is None:
            if self.algebraic_solver is True:
                t_eval = np.array([0])
            else:
                raise ValueError("t_eval cannot be None")

        # If t_eval is provided as [t0, tf] return the solution at 100 points
        elif isinstance(t_eval, list):
            if len(t_eval) == 1 and self.algebraic_solver is True:
                pass
            elif len(t_eval) != 2:
                raise pybamm.SolverError(
                    "'t_eval' can be provided as an array of times at which to "
                    "return the solution, or as a list [t0, tf] where t0 is the "
                    "initial time and tf is the final time, but has been provided "
                    "as a list of length {}.".format(len(t_eval))
                )
            else:
                t_eval = np.linspace(t_eval[0], t_eval[-1], 100)

        # Make sure t_eval is monotonic
        if (np.diff(t_eval) < 0).any():
            raise pybamm.SolverError("t_eval must increase monotonically")

        # Set up external variables and inputs
        #
        # Argument "inputs" can be either a list of input dicts or
        # a single dict. The remaining of this function is only working
        # with variable "input_list", which is a list of dictionaries.
        # If "inputs" is a single dict, "inputs_list" is a list of only one dict.
        inputs_list = inputs if isinstance(inputs, list) else [inputs]
        ext_and_inputs_list = [
            self._set_up_ext_and_inputs(model, external_variables, inputs)
            for inputs in inputs_list
        ]

        # Cannot use multiprocessing with model in "jax" format
        if (len(inputs_list) > 1) and model.convert_to_format == "jax":
            raise pybamm.SolverError(
                "Cannot solve list of inputs with multiprocessing "
                'when model in format "jax".'
            )

        # Set up (if not done already)
        timer = pybamm.Timer()
        if model not in self.models_set_up:
            # It is assumed that when len(inputs_list) > 1, model set
            # up (initial condition, time-scale and length-scale) does
            # not depend on input parameters. Thefore only `ext_and_inputs[0]`
            # is passed to `set_up`.
            # See https://github.com/pybamm-team/PyBaMM/pull/1261
            self.set_up(model, ext_and_inputs_list[0], t_eval)
            self.models_set_up.update(
                {model: {"initial conditions": model.concatenated_initial_conditions}}
            )
        else:
            ics_set_up = self.models_set_up[model]["initial conditions"]
            # Check that initial conditions have not been updated
            if ics_set_up.id != model.concatenated_initial_conditions.id:
                # If the new initial conditions are different, set up again
                # Doing the whole setup again might be slow, but no need to prematurely
                # optimize this
                self.set_up(model, ext_and_inputs_list[0], t_eval)
                self.models_set_up[model][
                    "initial conditions"
                ] = model.concatenated_initial_conditions
        set_up_time = timer.time()
        timer.reset()

        # (Re-)calculate consistent initial conditions
        # Assuming initial conditions do not depend on input parameters
        # when len(inputs_list) > 1, only `ext_and_inputs_list[0]`
        # is passed to `_set_initial_conditions`.
        # See https://github.com/pybamm-team/PyBaMM/pull/1261
        if len(inputs_list) > 1:
            all_inputs_names = set(
                itertools.chain.from_iterable(
                    [ext_and_inputs.keys() for ext_and_inputs in ext_and_inputs_list]
                )
            )
            initial_conditions_node_names = set(
                [it.name for it in model.concatenated_initial_conditions.pre_order()]
            )
            if all_inputs_names.issubset(initial_conditions_node_names):
                raise pybamm.SolverError(
                    "Input parameters cannot appear in expression "
                    "for initial conditions."
                )

        self._set_initial_conditions(model, ext_and_inputs_list[0], update_rhs=True)

        # Non-dimensionalise time
        t_eval_dimensionless = t_eval / model.timescale_eval

        # Calculate discontinuities
        discontinuities = [
            # Assuming that discontinuities do not depend on
            # input parameters when len(input_list) > 1, only
            # `input_list[0]` is passed to `evaluate`.
            # See https://github.com/pybamm-team/PyBaMM/pull/1261
            event.expression.evaluate(inputs=inputs_list[0])
            for event in model.discontinuity_events_eval
        ]

        # make sure they are increasing in time
        discontinuities = sorted(discontinuities)

        # remove any identical discontinuities
        discontinuities = [
            v
            for i, v in enumerate(discontinuities)
            if (
                i == len(discontinuities) - 1
                or discontinuities[i] < discontinuities[i + 1]
            )
            and v > 0
        ]

        # remove any discontinuities after end of t_eval
        discontinuities = [v for v in discontinuities if v < t_eval_dimensionless[-1]]

        if len(discontinuities) > 0:
            pybamm.logger.verbose(
                "Discontinuity events found at t = {}".format(discontinuities)
            )
            if isinstance(inputs, list):
                raise pybamm.SolverError(
                    "Cannot solve for a list of input parameters"
                    " sets with discontinuities"
                )
        else:
            pybamm.logger.verbose("No discontinuity events found")

        # insert time points around discontinuities in t_eval
        # keep track of sub sections to integrate by storing start and end indices
        start_indices = [0]
        end_indices = []
        eps = sys.float_info.epsilon
        for dtime in discontinuities:
            dindex = np.searchsorted(t_eval_dimensionless, dtime, side="left")
            end_indices.append(dindex + 1)
            start_indices.append(dindex + 1)
            if dtime - eps < t_eval_dimensionless[dindex] < dtime + eps:
                t_eval_dimensionless[dindex] += eps
                t_eval_dimensionless = np.insert(
                    t_eval_dimensionless, dindex, dtime - eps
                )
            else:
                t_eval_dimensionless = np.insert(
                    t_eval_dimensionless, dindex, [dtime - eps, dtime + eps]
                )
        end_indices.append(len(t_eval_dimensionless))

        # Integrate separately over each time segment and accumulate into the solution
        # object, restarting the solver at each discontinuity (and recalculating a
        # consistent state afterwards if a DAE)
        old_y0 = model.y0
        solutions = None
        for start_index, end_index in zip(start_indices, end_indices):
            pybamm.logger.verbose(
                "Calling solver for {} < t < {}".format(
                    t_eval_dimensionless[start_index] * model.timescale_eval,
                    t_eval_dimensionless[end_index - 1] * model.timescale_eval,
                )
            )
            ninputs = len(ext_and_inputs_list)
            if ninputs == 1:
                new_solution = self._integrate(
                    model,
                    t_eval_dimensionless[start_index:end_index],
                    ext_and_inputs_list[0],
                )
                new_solutions = [new_solution]
            else:
                with mp.Pool(processes=nproc) as p:
                    new_solutions = p.starmap(
                        self._integrate,
                        zip(
                            [model] * ninputs,
                            [t_eval_dimensionless[start_index:end_index]] * ninputs,
                            ext_and_inputs_list,
                        ),
                    )
                    p.close()
                    p.join()
            # Setting the solve time for each segment.
            # pybamm.Solution.__add__ assumes attribute solve_time.
            solve_time = timer.time()
            for sol in new_solutions:
                sol.solve_time = solve_time
            if start_index == start_indices[0]:
                solutions = [sol for sol in new_solutions]
            else:
                for i, new_solution in enumerate(new_solutions):
                    solutions[i] = solutions[i] + new_solution

            if solutions[0].termination != "final time":
                break

            if end_index != len(t_eval_dimensionless):
                # setup for next integration subsection
                last_state = solutions[0].y[:, -1]
                # update y0 (for DAE solvers, this updates the initial guess for the
                # rootfinder)
                model.y0 = last_state
                if len(model.algebraic) > 0:
                    model.y0 = self.calculate_consistent_state(
                        model, t_eval_dimensionless[end_index], ext_and_inputs_list[0]
                    )
        solve_time = timer.time()

        for i, solution in enumerate(solutions):
            # Check if extrapolation occurred
            extrapolation = self.check_extrapolation(solution, model.events)
            if extrapolation:
                warnings.warn(
                    "While solving {} extrapolation occurred for {}".format(
                        model.name, extrapolation
                    ),
                    pybamm.SolverWarning,
                )
            # Identify the event that caused termination and update the solution to
            # include the event time and state
            solutions[i], termination = self.get_termination_reason(
                solution, model.events
            )
            # Assign times
            solutions[i].set_up_time = set_up_time
            # all solutions get the same solve time, but their integration time
            # will be different (see https://github.com/pybamm-team/PyBaMM/pull/1261)
            solutions[i].solve_time = solve_time

        # Restore old y0
        model.y0 = old_y0

        # Report times
        if len(solutions) == 1:
            pybamm.logger.info("Finish solving {} ({})".format(model.name, termination))
            pybamm.logger.info(
                (
                    "Set-up time: {}, Solve time: {} (of which integration time: {}), "
                    "Total time: {}"
                ).format(
                    solutions[0].set_up_time,
                    solutions[0].solve_time,
                    solutions[0].integration_time,
                    solutions[0].total_time,
                )
            )
        else:
            pybamm.logger.info("Finish solving {} for all inputs".format(model.name))
            pybamm.logger.info(
                ("Set-up time: {}, Solve time: {}, Total time: {}").format(
                    solutions[0].set_up_time,
                    solutions[0].solve_time,
                    solutions[0].total_time,
                )
            )

        # Raise error if solutions[0] only contains one timestep (except for algebraic
        # solvers, where we may only expect one time in the solution)
        if (
            self.algebraic_solver is False
            and len(solution.all_ts) == 1
            and len(solution.all_ts[0]) == 1
        ):
            raise pybamm.SolverError(
                "Solution time vector has length 1. "
                "Check whether simulation terminated too early."
            )

        # Return solution(s)
        if ninputs == 1:
            return solutions[0]
        else:
            return solutions
Esempio n. 2
0
    def set_up(self, model):
        """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

        Raises
        ------
        :class:`pybamm.SolverError`
            If the model contains any algebraic equations (in which case a DAE solver
            should be used instead)

        """
        # Check for algebraic equations
        if len(model.algebraic) > 0:
            raise pybamm.SolverError(
                """Cannot use ODE solver to solve model with DAEs""")

        # create simplified rhs and event expressions
        concatenated_rhs = model.concatenated_rhs
        events = model.events

        if model.use_simplify:
            # set up simplification object, for re-use of dict
            simp = pybamm.Simplification()
            # create simplified rhs and event expressions
            pybamm.logger.info("Simplifying RHS")
            concatenated_rhs = simp.simplify(concatenated_rhs)

            pybamm.logger.info("Simplifying events")
            events = {
                name: simp.simplify(event)
                for name, event in events.items()
            }

        y0 = model.concatenated_initial_conditions[:, 0]

        if model.use_jacobian:
            # Create Jacobian from concatenated rhs
            y = pybamm.StateVector(slice(0, np.size(y0)))
            # set up Jacobian object, for re-use of dict
            jacobian = pybamm.Jacobian()
            pybamm.logger.info("Calculating jacobian")
            jac_rhs = jacobian.jac(concatenated_rhs, y)
            model.jacobian = jac_rhs
            model.jacobian_rhs = jac_rhs

            if model.use_simplify:
                pybamm.logger.info("Simplifying jacobian")
                jac_rhs = simp.simplify(jac_rhs)

            if model.use_to_python:
                pybamm.logger.info("Converting jacobian to python")
                jac_rhs = pybamm.EvaluatorPython(jac_rhs)
        else:
            jac_rhs = None

        if model.use_to_python:
            pybamm.logger.info("Converting RHS to python")
            concatenated_rhs = pybamm.EvaluatorPython(concatenated_rhs)
            pybamm.logger.info("Converting events to python")
            events = {
                name: pybamm.EvaluatorPython(event)
                for name, event in events.items()
            }

        # Create function to evaluate rhs
        def dydt(t, y):
            pybamm.logger.debug("Evaluating RHS for {} at t={}".format(
                model.name, t))
            y = y[:, np.newaxis]
            dy = concatenated_rhs.evaluate(t, y, known_evals={})[0]
            return dy[:, 0]

        # Create event-dependent function to evaluate events
        def event_fun(event):
            def eval_event(t, y):
                return event.evaluate(t, y)

            return eval_event

        event_funs = [event_fun(event) for event in events.values()]

        # Create function to evaluate jacobian
        if jac_rhs is not None:

            def jacobian(t, y):
                return jac_rhs.evaluate(t, y, known_evals={})[0]

        else:
            jacobian = None

        # Add the solver attributes
        # Note: these are the (possibly) converted to python version rhs, algebraic
        # etc. The expression tree versions of these are attributes of the model
        self.y0 = y0
        self.dydt = dydt
        self.events = events
        self.event_funs = event_funs
        self.jacobian = jacobian
Esempio n. 3
0
    def solve(
        self,
        t_eval=None,
        solver=None,
        external_variables=None,
        inputs=None,
        check_model=True,
    ):
        """
        A method to solve the model. This method will automatically build
        and set the model parameters if not already done so.

        Parameters
        ----------
        t_eval : numeric type, optional
            The times (in seconds) at which to compute the solution. Can be
            provided as an array of times at which to return the solution, or as a
            list `[t0, tf]` where `t0` is the initial time and `tf` is the final time.
            If provided as a list the solution is returned at 100 points within the
            interval `[t0, tf]`.

            If not using an experiment or running a drive cycle simulation (current
            provided as data) `t_eval` *must* be provided.

            If running an experiment the values in `t_eval` are ignored, and the
            solution times are specified by the experiment.

            If None and the parameter "Current function [A]" is read from data
            (i.e. drive cycle simulation) the model will be solved at the times
            provided in the data.
        solver : :class:`pybamm.BaseSolver`
            The solver to use to solve the model.
        external_variables : dict
            A dictionary of external variables and their corresponding
            values at the current time. The variables must correspond to
            the variables that would normally be found by solving the
            submodels that have been made external.
        inputs : dict, optional
            Any input parameters to pass to the model when solving
        check_model : bool, optional
            If True, model checks are performed after discretisation (see
            :meth:`pybamm.Discretisation.process_model`). Default is True.
        """
        # Setup
        self.build(check_model=check_model)
        if solver is None:
            solver = self.solver

        if self.operating_mode in ["without experiment", "drive cycle"]:

            if self.operating_mode == "without experiment":
                if t_eval is None:
                    raise pybamm.SolverError(
                        "'t_eval' must be provided if not using an experiment or "
                        "simulating a drive cycle. 't_eval' can be provided as an "
                        "array of times at which to return the solution, or as a "
                        "list [t0, tf] where t0 is the initial time and tf is the "
                        "final time. "
                        "For a constant current (dis)charge the suggested 't_eval'  "
                        "is [0, 3700/C] where C is the C-rate.")

            elif self.operating_mode == "drive cycle":
                # For drive cycles (current provided as data) we perform additional
                # tests on t_eval (if provided) to ensure the returned solution
                # captures the input.
                time_data = self._parameter_values[
                    "Current function [A]"].data[:, 0]
                # If no t_eval is provided, we use the times provided in the data.
                if t_eval is None:
                    pybamm.logger.info(
                        "Setting t_eval as specified by the data")
                    t_eval = time_data
                # If t_eval is provided we first check if it contains all of the
                # times in the data to within 10-12. If it doesn't, we then check
                # that the largest gap in t_eval is smaller than the smallest gap in
                # the time data (to ensure the resolution of t_eval is fine enough).
                # We only raise a warning here as users may genuinely only want
                # the solution returned at some specified points.
                elif (set(np.round(time_data, 12)).issubset(
                        set(np.round(t_eval, 12)))) is False:
                    warnings.warn(
                        """
                        t_eval does not contain all of the time points in the data
                        set. Note: passing t_eval = None automatically sets t_eval
                        to be the points in the data.
                        """,
                        pybamm.SolverWarning,
                    )
                    dt_data_min = np.min(np.diff(time_data))
                    dt_eval_max = np.max(np.diff(t_eval))
                    if dt_eval_max > dt_data_min + sys.float_info.epsilon:
                        warnings.warn(
                            """
                            The largest timestep in t_eval ({}) is larger than
                            the smallest timestep in the data ({}). The returned
                            solution may not have the correct resolution to accurately
                            capture the input. Try refining t_eval. Alternatively,
                            passing t_eval = None automatically sets t_eval to be the
                            points in the data.
                            """.format(dt_eval_max, dt_data_min),
                            pybamm.SolverWarning,
                        )

            self._solution = solver.solve(
                self.built_model,
                t_eval,
                external_variables=external_variables,
                inputs=inputs,
            )
            self.t_eval = self._solution.t * self.model.timescale.evaluate()

        elif self.operating_mode == "with experiment":
            if t_eval is not None:
                pybamm.logger.warning(
                    "Ignoring t_eval as solution times are specified by the experiment"
                )
            # Re-initialize solution, e.g. for solving multiple times with different
            # inputs without having to build the simulation again
            self._solution = None
            # Step through all experimental conditions
            inputs = inputs or {}
            pybamm.logger.info("Start running experiment")
            timer = pybamm.Timer()
            for idx, (exp_inputs, dt) in enumerate(
                    zip(self._experiment_inputs, self._experiment_times)):
                pybamm.logger.info(
                    self.experiment.operating_conditions_strings[idx])
                inputs.update(exp_inputs)
                # Make sure we take at least 2 timesteps
                npts = max(int(round(dt / exp_inputs["period"])) + 1, 2)
                self.step(
                    dt,
                    solver=solver,
                    npts=npts,
                    external_variables=external_variables,
                    inputs=inputs,
                )
                # Only allow events specified by experiment
                if not (self._solution.termination == "final time"
                        or "[experiment]" in self._solution.termination):
                    pybamm.logger.warning(
                        "\n\n\tExperiment is infeasible: '{}' ".format(
                            self._solution.termination) +
                        "was triggered during '{}'. ".format(
                            self.experiment.operating_conditions_strings[idx])
                        +
                        "Try reducing current, shortening the time interval, "
                        "or reducing the period.\n\n")
                    break
            pybamm.logger.info("Finish experiment simulation, took {}".format(
                timer.format(timer.time())))

        return self.solution
Esempio n. 4
0
    def set_up(self, model):
        """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

        Returns
        -------
        concatenated_algebraic : :class:`pybamm.Concatenation`
            Algebraic equations, which should evaluate to zero
        jac : :class:`pybamm.SparseStack`
            Jacobian matrix for the differential and algebraic equations

        Raises
        ------
        :class:`pybamm.SolverError`
            If the model contains any time derivatives, i.e. rhs equations (in
            which case an ODE or DAE solver should be used instead)
        """
        if len(model.rhs) > 0:
            raise pybamm.SolverError(
                """Cannot use algebraic solver to solve model with time derivatives"""
            )

        # create simplified algebraic expressions
        concatenated_algebraic = model.concatenated_algebraic

        if model.use_simplify:
            # set up simplification object, for re-use of dict
            simp = pybamm.Simplification()
            pybamm.logger.info("Simplifying algebraic")
            concatenated_algebraic = simp.simplify(concatenated_algebraic)

        if model.use_jacobian:
            # Create Jacobian from concatenated algebraic
            y = pybamm.StateVector(
                slice(0, np.size(model.concatenated_initial_conditions)))
            # set up Jacobian object, for re-use of dict
            jacobian = pybamm.Jacobian()
            pybamm.logger.info("Calculating jacobian")
            jac = jacobian.jac(concatenated_algebraic, y)
            model.jacobian = jac
            model.jacobian_algebraic = jac

            if model.use_simplify:
                pybamm.logger.info("Simplifying jacobian")
                jac = simp.simplify(jac)

            if model.convert_to_format == "python":
                pybamm.logger.info("Converting jacobian to python")
                jac = pybamm.EvaluatorPython(jac)

        else:
            jac = None

        if model.convert_to_format == "python":
            pybamm.logger.info("Converting algebraic to python")
            concatenated_algebraic = pybamm.EvaluatorPython(
                concatenated_algebraic)

        return concatenated_algebraic, jac
Esempio n. 5
0
    def calculate_consistent_state(self,
                                   model,
                                   time=0,
                                   y0_guess=None,
                                   inputs=None):
        """
        Calculate consistent state for the algebraic equations through
        root-finding

        Parameters
        ----------
        model : :class:`pybamm.BaseModel`
            The model for which to calculate initial conditions.
        time : float
            The time at which to calculate the states
        y0_guess : :class:`np.array`
            Guess for the rootfinding
        inputs : dict, optional
            Any input parameters to pass to the model when solving

        Returns
        -------
        y0_consistent : array-like, same shape as y0_guess
            Initial conditions that are consistent with the algebraic equations (roots
            of the algebraic equations)
        """
        pybamm.logger.info("Start calculating consistent states")
        if y0_guess is None:
            y0_guess = model.concatenated_initial_conditions.flatten()

        # Split y0_guess into differential and algebraic
        len_rhs = model.rhs_eval(time, y0_guess).shape[0]
        y0_diff, y0_alg_guess = np.split(y0_guess, [len_rhs])
        inputs = inputs or {}

        # Solve using casadi or scipy
        if self.root_method == "casadi":
            # Set up
            u_stacked = casadi.vertcat(*[x for x in inputs.values()])
            u = casadi.MX.sym("u", u_stacked.shape[0])
            y_alg = casadi.MX.sym("y_alg", y0_alg_guess.shape[0])
            y = casadi.vertcat(y0_diff, y_alg)
            alg_root = model.casadi_algebraic(time, y, u)
            # Solve
            # set error_on_fail to False and just check the final output is small
            # enough
            roots = casadi.rootfinder(
                "roots",
                "newton",
                dict(x=y_alg, p=u, g=alg_root),
                {"abstol": self.root_tol},
            )
            try:
                y0_alg = roots(y0_alg_guess, u_stacked).full().flatten()
                success = True
                message = None
                # Check final output
                fun = model.casadi_algebraic(time,
                                             casadi.vertcat(y0_diff, y0_alg),
                                             u_stacked)
            except RuntimeError as err:
                success = False
                message = err.args[0]
                fun = None
        else:
            algebraic = model.algebraic_eval
            jac = model.jac_algebraic_eval

            def root_fun(y0_alg):
                "Evaluates algebraic using y0_diff (fixed) and y0_alg (changed by algo)"
                y0 = np.concatenate([y0_diff, y0_alg])
                out = algebraic(time, y0)
                pybamm.logger.debug(
                    "Evaluating algebraic equations at t={}, L2-norm is {}".
                    format(time * model.timescale, np.linalg.norm(out)))
                return out

            if jac:
                if issparse(jac(0, y0_guess)):

                    def jac_fn(y0_alg):
                        """
                        Evaluates jacobian using y0_diff (fixed) and y0_alg (varying)
                        """
                        y0 = np.concatenate([y0_diff, y0_alg])
                        return jac(0, y0)[:, len_rhs:].toarray()

                else:

                    def jac_fn(y0_alg):
                        """
                        Evaluates jacobian using y0_diff (fixed) and y0_alg (varying)
                        """
                        y0 = np.concatenate([y0_diff, y0_alg])
                        return jac(0, y0)[:, len_rhs:]

            else:
                jac_fn = None
            # Find the values of y0_alg that are roots of the algebraic equations
            sol = optimize.root(
                root_fun,
                y0_alg_guess,
                jac=jac_fn,
                method=self.root_method,
                tol=self.root_tol,
            )
            pybamm.citations.register("virtanen2020scipy")

            # Set outputs
            y0_alg = sol.x
            success = sol.success
            fun = sol.fun
            message = sol.message

        if success and np.all(fun < self.root_tol * len(y0_alg)):
            # Return full set of consistent initial conditions (y0_diff unchanged)
            y0_consistent = np.concatenate([y0_diff, y0_alg])
            pybamm.logger.info(
                "Finish calculating consistent initial conditions")
            return y0_consistent
        elif not success:
            raise pybamm.SolverError(
                "Could not find consistent initial conditions: {}".format(
                    message))
        else:
            raise pybamm.SolverError("""
                Could not find consistent initial conditions: solver terminated
                successfully, but maximum solution error ({}) above tolerance ({})
                """.format(np.max(fun), self.root_tol * len(y0_alg)))
Esempio n. 6
0
    def _integrate(self, model, t_eval, inputs=None):
        """
        Calculate the solution of the algebraic equations through root-finding

        Parameters
        ----------
        model : :class:`pybamm.BaseModel`
            The model whose solution to calculate.
        t_eval : :class:`numpy.array`, size (k,)
            The times at which to compute the solution
        inputs : dict, optional
            Any input parameters to pass to the model when solving
        """
        inputs = inputs or {}
        if model.convert_to_format == "casadi":
            inputs = casadi.vertcat(*[x for x in inputs.values()])

        y0 = model.y0
        if isinstance(y0, casadi.DM):
            y0 = y0.full().flatten()

        # The casadi algebraic solver can read rhs equations, but leaves them unchanged
        # i.e. the part of the solution vector that corresponds to the differential
        # equations will be equal to the initial condition provided. This allows this
        # solver to be used for initialising the DAE solvers
        # Split y0 into differential and algebraic
        if model.rhs == {}:
            len_rhs = 0
        else:
            len_rhs = model.rhs_eval(t_eval[0], y0, inputs).shape[0]
        y0_diff, y0_alg = np.split(y0, [len_rhs])

        algebraic = model.algebraic_eval

        y_alg = np.empty((len(y0_alg), len(t_eval)))

        timer = pybamm.Timer()
        integration_time = 0
        for idx, t in enumerate(t_eval):

            def root_fun(y_alg):
                "Evaluates algebraic using y"
                y = np.concatenate([y0_diff, y_alg])
                out = algebraic(t, y, inputs)
                pybamm.logger.debug(
                    "Evaluating algebraic equations at t={}, L2-norm is {}".
                    format(t * model.timescale_eval, np.linalg.norm(out)))
                return out

            jac = model.jac_algebraic_eval
            if jac:
                if issparse(jac(t_eval[0], y0, inputs)):

                    def jac_fn(y_alg):
                        """
                        Evaluates jacobian using y0_diff (fixed) and y_alg (varying)
                        """
                        y = np.concatenate([y0_diff, y_alg])
                        return jac(0, y, inputs)[:, len_rhs:].toarray()

                else:

                    def jac_fn(y_alg):
                        """
                        Evaluates jacobian using y0_diff (fixed) and y_alg (varying)
                        """
                        y = np.concatenate([y0_diff, y_alg])
                        return jac(0, y, inputs)[:, len_rhs:]

            else:
                jac_fn = None

            # Evaluate algebraic with new t and previous y0, if it's already close
            # enough then keep it
            if np.all(abs(algebraic(t, y0, inputs)) < self.tol):
                pybamm.logger.debug("Keeping same solution at t={}".format(t))
                y_alg[:, idx] = y0_alg
            # Otherwise calculate new y0
            else:
                # Methods which use least-squares are specified as either "lsq", which
                # uses the default method, or with "lsq__methodname"
                if self.method.startswith("lsq"):

                    if self.method == "lsq":
                        method = "trf"
                    else:
                        method = self.method[5:]
                    if jac_fn is None:
                        jac_fn = "2-point"
                    timer.reset()
                    sol = optimize.least_squares(
                        root_fun,
                        y0_alg,
                        method=method,
                        ftol=self.tol,
                        jac=jac_fn,
                        bounds=model.bounds,
                        **self.extra_options,
                    )
                    integration_time += timer.time()
                # Methods which use minimize are specified as either "minimize", which
                # uses the default method, or with "minimize__methodname"
                elif self.method.startswith("minimize"):
                    # Adapt the root function for minimize
                    def root_norm(y):
                        return np.sum(root_fun(y)**2)

                    if jac_fn is None:
                        jac_norm = None
                    else:

                        def jac_norm(y):
                            return np.sum(2 * root_fun(y) * jac_fn(y), 0)

                    if self.method == "minimize":
                        method = None
                    else:
                        method = self.method[10:]
                    extra_options = self.extra_options
                    if np.any(model.bounds[0] != -np.inf) or np.any(
                            model.bounds[1] != np.inf):
                        bounds = [
                            (lb, ub)
                            for lb, ub in zip(model.bounds[0], model.bounds[1])
                        ]
                        extra_options["bounds"] = bounds
                    timer.reset()
                    sol = optimize.minimize(
                        root_norm,
                        y0_alg,
                        method=method,
                        tol=self.tol,
                        jac=jac_norm,
                        **extra_options,
                    )
                    integration_time += timer.time()
                else:
                    timer.reset()
                    sol = optimize.root(
                        root_fun,
                        y0_alg,
                        method=self.method,
                        tol=self.tol,
                        jac=jac_fn,
                        options=self.extra_options,
                    )
                    integration_time += timer.time()

                if sol.success and np.all(abs(sol.fun) < self.tol):
                    # update initial guess for the next iteration
                    y0_alg = sol.x
                    # update solution array
                    y_alg[:, idx] = y0_alg
                elif not sol.success:
                    raise pybamm.SolverError(
                        "Could not find acceptable solution: {}".format(
                            sol.message))
                else:
                    raise pybamm.SolverError(
                        "Could not find acceptable solution: solver terminated "
                        "successfully, but maximum solution error "
                        "({}) above tolerance ({})".format(
                            np.max(abs(sol.fun)), self.tol))

        # Concatenate differential part
        y_diff = np.r_[[y0_diff] * len(t_eval)].T
        y_sol = np.r_[y_diff, y_alg]
        # Return solution object (no events, so pass None to t_event, y_event)
        sol = pybamm.Solution(t_eval, y_sol, termination="success")
        sol.integration_time = integration_time
        return sol
Esempio n. 7
0
    def _integrate(self, model, t_eval, inputs=None):
        """
        Solve a model defined by dydt with initial conditions y0.

        Parameters
        ----------
        model : :class:`pybamm.BaseModel`
            The model whose solution to calculate.
        t_eval : :class:`numpy.array`, size (k,)
            The times at which to compute the solution
        inputs : dict, optional
            Any input parameters to pass to the model when solving

        Returns
        -------
        object
            An object containing the times and values of the solution, as well as
            various diagnostic messages.

        """
        if model.convert_to_format == "casadi":
            inputs = casadi.vertcat(*[x for x in inputs.values()])

        extra_options = {"rtol": self.rtol, "atol": self.atol}

        # check for user-supplied Jacobian
        implicit_methods = ["Radau", "BDF", "LSODA"]
        if np.any([self.method in implicit_methods]):
            if model.jacobian_eval:
                extra_options.update({
                    "jac":
                    lambda t, y: model.jacobian_eval(t, y, inputs)
                })

        # make events terminal so that the solver stops when they are reached
        if model.terminate_events_eval:

            def event_wrapper(event):
                def event_fn(t, y):
                    return event(t, y, inputs)

                event_fn.terminal = True
                return event_fn

            events = [
                event_wrapper(event) for event in model.terminate_events_eval
            ]
            extra_options.update({"events": events})

        sol = it.solve_ivp(lambda t, y: model.rhs_eval(t, y, inputs),
                           (t_eval[0], t_eval[-1]),
                           model.y0,
                           t_eval=t_eval,
                           method=self.method,
                           dense_output=True,
                           **extra_options)

        if sol.success:
            # Set the reason for termination
            if sol.message == "A termination event occurred.":
                termination = "event"
                t_event = []
                for time in sol.t_events:
                    if time.size > 0:
                        t_event = np.append(t_event, np.max(time))
                t_event = np.array([np.max(t_event)])
                y_event = sol.sol(t_event)
            elif sol.message.startswith(
                    "The solver successfully reached the end"):
                termination = "final time"
                t_event = None
                y_event = np.array(None)
            return pybamm.Solution(sol.t, sol.y, t_event, y_event, termination)
        else:
            raise pybamm.SolverError(sol.message)
Esempio n. 8
0
    def _integrate(self, model, t_eval, inputs_dict=None):
        """
        Calculate the solution of the algebraic equations through root-finding

        Parameters
        ----------
        model : :class:`pybamm.BaseModel`
            The model whose solution to calculate.
        t_eval : :class:`numpy.array`, size (k,)
            The times at which to compute the solution
        inputs_dict : dict, optional
            Any input parameters to pass to the model when solving. If any input
            parameters that are present in the model are missing from "inputs", then
            the solution will consist of `ProcessedSymbolicVariable` objects, which must
            be provided with inputs to obtain their value.
        """
        # Record whether there are any symbolic inputs
        inputs_dict = inputs_dict or {}
        has_symbolic_inputs = any(
            isinstance(v, casadi.MX) for v in inputs_dict.values())
        symbolic_inputs = casadi.vertcat(
            *[v for v in inputs_dict.values() if isinstance(v, casadi.MX)])

        # Create casadi objects for the root-finder
        inputs = casadi.vertcat(*[v for v in inputs_dict.values()])

        y0 = model.y0

        # If y0 already satisfies the tolerance for all t then keep it
        if has_symbolic_inputs is False and all(
                np.all(
                    abs(model.casadi_algebraic(t, y0, inputs).full()) <
                    self.tol) for t in t_eval):
            pybamm.logger.debug("Keeping same solution at all times")
            return pybamm.Solution(t_eval,
                                   y0,
                                   model,
                                   inputs_dict,
                                   termination="success")

        # The casadi algebraic solver can read rhs equations, but leaves them unchanged
        # i.e. the part of the solution vector that corresponds to the differential
        # equations will be equal to the initial condition provided. This allows this
        # solver to be used for initialising the DAE solvers
        if model.rhs == {}:
            len_rhs = 0
            y0_diff = casadi.DM()
            y0_alg = y0
        else:
            len_rhs = model.concatenated_rhs.size
            y0_diff = y0[:len_rhs]
            y0_alg = y0[len_rhs:]

        y_alg = None

        # Set up
        t_sym = casadi.MX.sym("t")
        y_alg_sym = casadi.MX.sym("y_alg", y0_alg.shape[0])
        y_sym = casadi.vertcat(y0_diff, y_alg_sym)

        t_and_inputs_sym = casadi.vertcat(t_sym, symbolic_inputs)
        alg = model.casadi_algebraic(t_sym, y_sym, inputs)

        # Check interpolant extrapolation
        if model.interpolant_extrapolation_events_eval:
            extrap_event = [
                event(0, y0, inputs)
                for event in model.interpolant_extrapolation_events_eval
            ]
            if extrap_event:
                if (np.concatenate(extrap_event) < self.extrap_tol).any():
                    extrap_event_names = []
                    for event in model.events:
                        if (event.event_type
                                == pybamm.EventType.INTERPOLANT_EXTRAPOLATION
                                and (event.expression.evaluate(
                                    0, y0.full(), inputs=inputs) <
                                     self.extrap_tol)):
                            extrap_event_names.append(event.name[12:])

                    raise pybamm.SolverError(
                        "CasADi solver failed because the following interpolation "
                        "bounds were exceeded at the initial conditions: {}. "
                        "You may need to provide additional interpolation points "
                        "outside these bounds.".format(extrap_event_names))

        # Set constraints vector in the casadi format
        # Constrain the unknowns. 0 (default): no constraint on ui, 1: ui >= 0.0,
        # -1: ui <= 0.0, 2: ui > 0.0, -2: ui < 0.0.
        constraints = np.zeros_like(model.bounds[0], dtype=int)
        # If the lower bound is positive then the variable must always be positive
        constraints[model.bounds[0] >= 0] = 1
        # If the upper bound is negative then the variable must always be negative
        constraints[model.bounds[1] <= 0] = -1

        # Set up rootfinder
        roots = casadi.rootfinder(
            "roots",
            "newton",
            dict(x=y_alg_sym, p=t_and_inputs_sym, g=alg),
            {
                **self.extra_options,
                "abstol": self.tol,
                "constraints": list(constraints[len_rhs:]),
            },
        )
        timer = pybamm.Timer()
        integration_time = 0
        for idx, t in enumerate(t_eval):
            # Evaluate algebraic with new t and previous y0, if it's already close
            # enough then keep it
            # We can't do this if there are symbolic inputs
            if has_symbolic_inputs is False and np.all(
                    abs(model.casadi_algebraic(t, y0, inputs).full()) <
                    self.tol):
                pybamm.logger.debug("Keeping same solution at t={}".format(
                    t * model.timescale_eval))
                if y_alg is None:
                    y_alg = y0_alg
                else:
                    y_alg = casadi.horzcat(y_alg, y0_alg)
            # Otherwise calculate new y_sol
            else:
                t_eval_inputs_sym = casadi.vertcat(t, symbolic_inputs)
                # Solve
                try:
                    timer.reset()
                    y_alg_sol = roots(y0_alg, t_eval_inputs_sym)
                    integration_time += timer.time()
                    success = True
                    message = None
                    # Check final output
                    y_sol = casadi.vertcat(y0_diff, y_alg_sol)
                    fun = model.casadi_algebraic(t, y_sol, inputs)
                except RuntimeError as err:
                    success = False
                    message = err.args[0]
                    fun = None

                # If there are no symbolic inputs, check the function is below the tol
                # Skip this check if there are symbolic inputs
                if success and (has_symbolic_inputs is True or
                                (not any(np.isnan(fun))
                                 and np.all(casadi.fabs(fun) < self.tol))):
                    # update initial guess for the next iteration
                    y0_alg = y_alg_sol
                    y0 = casadi.vertcat(y0_diff, y0_alg)
                    # update solution array
                    if y_alg is None:
                        y_alg = y_alg_sol
                    else:
                        y_alg = casadi.horzcat(y_alg, y_alg_sol)
                elif not success:
                    raise pybamm.SolverError(
                        "Could not find acceptable solution: {}".format(
                            message))
                elif any(np.isnan(fun)):
                    raise pybamm.SolverError(
                        "Could not find acceptable solution: solver returned NaNs"
                    )
                else:
                    raise pybamm.SolverError("""
                        Could not find acceptable solution: solver terminated
                        successfully, but maximum solution error ({})
                        above tolerance ({})
                        """.format(casadi.mmax(casadi.fabs(fun)), self.tol))

        # Concatenate differential part
        y_diff = casadi.horzcat(*[y0_diff] * len(t_eval))
        y_sol = casadi.vertcat(y_diff, y_alg)
        # Return solution object (no events, so pass None to t_event, y_event)
        sol = pybamm.Solution([t_eval],
                              y_sol,
                              model,
                              inputs_dict,
                              termination="success")
        sol.integration_time = integration_time
        return sol
Esempio n. 9
0
    def __init__(self, base_variable, solution, known_evals=None):
        self.base_variable = base_variable
        self.t_sol = solution.t
        self.u_sol = solution.y
        self.mesh = base_variable.mesh
        self.inputs = solution.inputs
        self.domain = base_variable.domain
        self.auxiliary_domains = base_variable.auxiliary_domains
        self.known_evals = known_evals

        if self.known_evals:
            self.base_eval, self.known_evals[
                solution.t[0]] = base_variable.evaluate(
                    solution.t[0],
                    solution.y[:, 0],
                    inputs={
                        name: inp[0]
                        for name, inp in solution.inputs.items()
                    },
                    known_evals=self.known_evals[solution.t[0]],
                )
        else:
            self.base_eval = base_variable.evaluate(
                solution.t[0],
                solution.y[:, 0],
                inputs={name: inp[0]
                        for name, inp in solution.inputs.items()},
            )

        # handle 2D (in space) finite element variables differently
        if (self.mesh and "current collector" in self.domain
                and isinstance(self.mesh[0], pybamm.ScikitSubMesh2D)):
            if len(solution.t) == 1:
                # space only (steady solution)
                self.initialise_2D_fixed_t_scikit_fem()
            else:
                self.initialise_2D_scikit_fem()

        # check variable shape
        else:
            if len(solution.t) == 1:
                raise pybamm.SolverError(
                    "Solution time vector must have length > 1. Check whether "
                    "simulation terminated too early.")
            elif (isinstance(self.base_eval, numbers.Number)
                  or len(self.base_eval.shape) == 0
                  or self.base_eval.shape[0] == 1):
                self.initialise_0D()
            else:
                n = self.mesh[0].npts
                base_shape = self.base_eval.shape[0]
                # Try some shapes that could make the variable a 1D variable
                if base_shape in [n, n + 1]:
                    self.initialise_1D()
                else:
                    # Try some shapes that could make the variable a 2D variable
                    first_dim_nodes = self.mesh[0].nodes
                    first_dim_edges = self.mesh[0].edges
                    second_dim_pts = self.base_variable.secondary_mesh[0].nodes
                    if self.base_eval.size // len(second_dim_pts) in [
                            len(first_dim_nodes),
                            len(first_dim_edges),
                    ]:
                        self.initialise_2D()
                    else:
                        # Raise error for 3D variable
                        raise NotImplementedError(
                            "Shape not recognized for {} ".format(
                                base_variable) +
                            "(note processing of 3D variables is not yet implemented)"
                        )
Esempio n. 10
0
    def calculate_consistent_initial_conditions(self,
                                                rhs,
                                                algebraic,
                                                y0_guess,
                                                jac=None):
        """
        Calculate consistent initial conditions for the algebraic equations through
        root-finding

        Parameters
        ----------
        rhs : method
            Function that takes in t and y and returns the value of the differential
            equations
        algebraic : method
            Function that takes in t and y and returns the value of the algebraic
            equations
        y0_guess : array-like
            Array of the user's guess for the initial conditions, used to initialise
            the root finding algorithm
        jac : method
            Function that takes in t and y and returns the value of the jacobian for the
            algebraic equations

        Returns
        -------
        y0_consistent : array-like, same shape as y0_guess
            Initial conditions that are consistent with the algebraic equations (roots
            of the algebraic equations)
        """
        pybamm.logger.info("Start calculating consistent initial conditions")

        # Split y0_guess into differential and algebraic
        len_rhs = rhs(0, y0_guess).shape[0]
        y0_diff, y0_alg_guess = np.split(y0_guess, [len_rhs])

        def root_fun(y0_alg):
            "Evaluates algebraic using y0_diff (fixed) and y0_alg (changed by algo)"
            y0 = np.concatenate([y0_diff, y0_alg])
            out = algebraic(0, y0)
            pybamm.logger.debug(
                "Evaluating algebraic equations at t=0, L2-norm is {}".format(
                    np.linalg.norm(out)))
            return out

        if jac:
            if issparse(jac(0, y0_guess)):

                def jac_fn(y0_alg):
                    """
                    Evaluates jacobian using y0_diff (fixed) and y0_alg (varying)
                    """
                    y0 = np.concatenate([y0_diff, y0_alg])
                    return jac(0, y0)[:, len_rhs:].toarray()

            else:

                def jac_fn(y0_alg):
                    """
                    Evaluates jacobian using y0_diff (fixed) and y0_alg (varying)
                    """
                    y0 = np.concatenate([y0_diff, y0_alg])
                    return jac(0, y0)[:, len_rhs:]

        else:
            jac_fn = None
        # Find the values of y0_alg that are roots of the algebraic equations
        sol = optimize.root(
            root_fun,
            y0_alg_guess,
            jac=jac_fn,
            method=self.root_method,
            tol=self.root_tol,
        )
        # Return full set of consistent initial conditions (y0_diff unchanged)
        y0_consistent = np.concatenate([y0_diff, sol.x])

        if sol.success and np.all(sol.fun < self.root_tol * len(sol.x)):
            pybamm.logger.info(
                "Finish calculating consistent initial conditions")
            return y0_consistent
        elif not sol.success:
            raise pybamm.SolverError(
                "Could not find consistent initial conditions: {}".format(
                    sol.message))
        else:
            raise pybamm.SolverError("""
                Could not find consistent initial conditions: solver terminated
                successfully, but maximum solution error ({}) above tolerance ({})
                """.format(np.max(sol.fun), self.root_tol * len(sol.x)))
Esempio n. 11
0
    def _integrate(self, model, t_eval, inputs=None):
        """
        Calculate the solution of the algebraic equations through root-finding

        Parameters
        ----------
        model : :class:`pybamm.BaseModel`
            The model whose solution to calculate.
        t_eval : :class:`numpy.array`, size (k,)
            The times at which to compute the solution
        inputs : dict, optional
            Any input parameters to pass to the model when solving
        """
        inputs = inputs or {}
        if model.convert_to_format == "casadi":
            inputs = casadi.vertcat(*[x for x in inputs.values()])

        algebraic = model.algebraic_eval
        y0 = model.y0

        y = np.empty((len(y0), len(t_eval)))

        for idx, t in enumerate(t_eval):

            def root_fun(y):
                "Evaluates algebraic using y"
                out = algebraic(t, y, inputs)
                pybamm.logger.debug(
                    "Evaluating algebraic equations at t={}, L2-norm is {}".
                    format(t, np.linalg.norm(out)))
                return out

            if model.jacobian_eval is not None:

                def jac(y):
                    return model.jacobian_eval(t, y, inputs)

            else:
                jac = None

            # Evaluate algebraic with new t and previous y0, if it's already close
            # enough then keep it
            if np.all(abs(algebraic(t, y0, inputs)) < self.tol):
                pybamm.logger.debug("Keeping same solution at t={}".format(t))
                y[:, idx] = y0
            # Otherwise calculate new y0
            else:
                sol = optimize.root(
                    root_fun,
                    y0,
                    method=self.method,
                    tol=self.tol,
                    jac=jac,
                )

                if sol.success and np.all(abs(sol.fun) < self.tol):
                    # update initial guess for the next iteration
                    y0 = sol.x
                    # update solution array
                    y[:, idx] = y0
                elif not sol.success:
                    raise pybamm.SolverError(
                        "Could not find acceptable solution: {}".format(
                            sol.message))
                else:
                    raise pybamm.SolverError("""
                        Could not find acceptable solution: solver terminated
                        successfully, but maximum solution error ({})
                        above tolerance ({})
                        """.format(np.max(sol.fun), self.tol))

        # Return solution object (no events, so pass None to t_event, y_event)
        return pybamm.Solution(t_eval, y, termination="success")
Esempio n. 12
0
    def integrate(self,
                  derivs,
                  y0,
                  t_eval,
                  events=None,
                  mass_matrix=None,
                  jacobian=None):
        """
        Solve a model defined by dydt with initial conditions y0.

        Parameters
        ----------
        derivs : method
            A function that takes in t (size (1,)), y (size (n,))
            and returns the time-derivative dydt (size (n,))
        y0 : :class:`numpy.array`, size (n,)
            The initial conditions
        t_eval : :class:`numpy.array`, size (k,)
            The times at which to compute the solution
        events : method, optional
            A function that takes in t and y and returns conditions for the solver to
            stop
        mass_matrix : array_like, optional
            The (sparse) mass matrix for the chosen spatial method.
        jacobian : method, optional
            A function that takes in t and y and returns the Jacobian. If
            None, the solver will approximate the Jacobian.
        Returns
        -------
        object
            An object containing the times and values of the solution, as well as
            various diagnostic messages.

        """
        extra_options = {"rtol": self.rtol, "atol": self.atol}

        # check for user-supplied Jacobian
        implicit_methods = ["Radau", "BDF", "LSODA"]
        if np.any([self.method in implicit_methods]):
            if jacobian:
                extra_options.update({"jac": jacobian})

        # make events terminal so that the solver stops when they are reached
        if events:
            for event in events:
                event.terminal = True
            extra_options.update({"events": events})

        sol = it.solve_ivp(derivs, (t_eval[0], t_eval[-1]),
                           y0,
                           t_eval=t_eval,
                           method=self.method,
                           dense_output=True,
                           **extra_options)

        if sol.success:
            # Set the reason for termination
            if sol.message == "A termination event occurred.":
                termination = "event"
                t_event = []
                for time in sol.t_events:
                    if time:
                        t_event = np.append(t_event, np.max(time))
                t_event = np.array([np.max(t_event)])
                y_event = sol.sol(t_event)
            elif sol.message.startswith(
                    "The solver successfully reached the end"):
                termination = "final time"
                t_event = None
                y_event = np.array(None)
            return pybamm.Solution(sol.t, sol.y, t_event, y_event, termination)
        else:
            raise pybamm.SolverError(sol.message)
Esempio n. 13
0
    def _integrate(self, model, t_eval, inputs=None):
        """
        Calculate the solution of the algebraic equations through root-finding

        Parameters
        ----------
        model : :class:`pybamm.BaseModel`
            The model whose solution to calculate.
        t_eval : :class:`numpy.array`, size (k,)
            The times at which to compute the solution
        inputs : dict, optional
            Any input parameters to pass to the model when solving. If any input
            parameters that are present in the model are missing from "inputs", then
            the solution will consist of `ProcessedSymbolicVariable` objects, which must
            be provided with inputs to obtain their value.
        """
        # Record whether there are any symbolic inputs
        inputs = inputs or {}
        has_symbolic_inputs = any(isinstance(v, casadi.MX) for v in inputs.values())

        # Create casadi objects for the root-finder
        inputs = casadi.vertcat(*[x for x in inputs.values()])

        y0 = model.y0
        # The casadi algebraic solver can read rhs equations, but leaves them unchanged
        # i.e. the part of the solution vector that corresponds to the differential
        # equations will be equal to the initial condition provided. This allows this
        # solver to be used for initialising the DAE solvers
        if model.rhs == {}:
            len_rhs = 0
            y0_diff = casadi.DM()
            y0_alg = y0
        else:
            len_rhs = model.concatenated_rhs.size
            y0_diff = y0[:len_rhs]
            y0_alg = y0[len_rhs:]

        y_alg = None

        # Set up
        t_sym = casadi.MX.sym("t")
        y_alg_sym = casadi.MX.sym("y_alg", y0_alg.shape[0])
        y_sym = casadi.vertcat(y0_diff, y_alg_sym)
        p_sym = casadi.MX.sym("p", inputs.shape[0])

        t_p_sym = casadi.vertcat(t_sym, p_sym)
        alg = model.casadi_algebraic(t_sym, y_sym, p_sym)

        # Set constraints vector in the casadi format
        # Constrain the unknowns. 0 (default): no constraint on ui, 1: ui >= 0.0,
        # -1: ui <= 0.0, 2: ui > 0.0, -2: ui < 0.0.
        constraints = np.zeros_like(model.bounds[0], dtype=int)
        # If the lower bound is positive then the variable must always be positive
        constraints[model.bounds[0] >= 0] = 1
        # If the upper bound is negative then the variable must always be negative
        constraints[model.bounds[1] <= 0] = -1

        # Set up rootfinder
        roots = casadi.rootfinder(
            "roots",
            "newton",
            dict(x=y_alg_sym, p=t_p_sym, g=alg),
            {
                **self.extra_options,
                "abstol": self.tol,
                "constraints": list(constraints[len_rhs:]),
            },
        )
        for idx, t in enumerate(t_eval):
            # Evaluate algebraic with new t and previous y0, if it's already close
            # enough then keep it
            # We can't do this if there are symbolic inputs
            if has_symbolic_inputs is False and np.all(
                abs(model.casadi_algebraic(t, y0, inputs).full()) < self.tol
            ):
                pybamm.logger.debug(
                    "Keeping same solution at t={}".format(t * model.timescale_eval)
                )
                if y_alg is None:
                    y_alg = y0_alg
                else:
                    y_alg = casadi.horzcat(y_alg, y0_alg)
            # Otherwise calculate new y_sol
            else:
                t_inputs = casadi.vertcat(t, inputs)
                # Solve
                try:
                    y_alg_sol = roots(y0_alg, t_inputs)
                    success = True
                    message = None
                    # Check final output
                    y_sol = casadi.vertcat(y0_diff, y_alg_sol)
                    fun = model.casadi_algebraic(t, y_sol, inputs)
                except RuntimeError as err:
                    success = False
                    message = err.args[0]
                    fun = None

                # If there are no symbolic inputs, check the function is below the tol
                # Skip this check if there are symbolic inputs
                if success and (
                    has_symbolic_inputs is True or np.all(casadi.fabs(fun) < self.tol)
                ):
                    # update initial guess for the next iteration
                    y0_alg = y_alg_sol
                    # update solution array
                    if y_alg is None:
                        y_alg = y_alg_sol
                    else:
                        y_alg = casadi.horzcat(y_alg, y_alg_sol)
                elif not success:
                    raise pybamm.SolverError(
                        "Could not find acceptable solution: {}".format(message)
                    )
                else:
                    raise pybamm.SolverError(
                        """
                        Could not find acceptable solution: solver terminated
                        successfully, but maximum solution error ({})
                        above tolerance ({})
                        """.format(
                            casadi.mmax(casadi.fabs(fun)), self.tol
                        )
                    )

        # Concatenate differential part
        y_diff = casadi.horzcat(*[y0_diff] * len(t_eval))
        y_sol = casadi.vertcat(y_diff, y_alg)
        # Return solution object (no events, so pass None to t_event, y_event)
        return pybamm.Solution(t_eval, y_sol, termination="success")
Esempio n. 14
0
    def _integrate(self, model, t_eval, inputs=None):
        """
        Calculate the solution of the algebraic equations through root-finding

        Parameters
        ----------
        model : :class:`pybamm.BaseModel`
            The model whose solution to calculate.
        t_eval : :class:`numpy.array`, size (k,)
            The times at which to compute the solution
        inputs : dict, optional
            Any input parameters to pass to the model when solving
        """
        inputs = inputs or {}
        if model.convert_to_format == "casadi":
            inputs = casadi.vertcat(*[x for x in inputs.values()])

        y0 = model.y0
        if isinstance(y0, casadi.DM):
            y0 = y0.full().flatten()

        # The casadi algebraic solver can read rhs equations, but leaves them unchanged
        # i.e. the part of the solution vector that corresponds to the differential
        # equations will be equal to the initial condition provided. This allows this
        # solver to be used for initialising the DAE solvers
        # Split y0 into differential and algebraic
        if model.rhs == {}:
            len_rhs = 0
        else:
            len_rhs = model.rhs_eval(t_eval[0], y0, inputs).shape[0]
        y0_diff, y0_alg = np.split(y0, [len_rhs])

        algebraic = model.algebraic_eval

        y_alg = np.empty((len(y0_alg), len(t_eval)))

        for idx, t in enumerate(t_eval):

            def root_fun(y_alg):
                "Evaluates algebraic using y"
                y = np.concatenate([y0_diff, y_alg])
                out = algebraic(t, y, inputs)
                pybamm.logger.debug(
                    "Evaluating algebraic equations at t={}, L2-norm is {}".
                    format(t * model.timescale_eval, np.linalg.norm(out)))
                return out

            jac = model.jac_algebraic_eval
            if jac:
                if issparse(jac(t_eval[0], y0, inputs)):

                    def jac_fn(y_alg):
                        """
                        Evaluates jacobian using y0_diff (fixed) and y_alg (varying)
                        """
                        y = np.concatenate([y0_diff, y_alg])
                        return jac(0, y, inputs)[:, len_rhs:].toarray()

                else:

                    def jac_fn(y_alg):
                        """
                        Evaluates jacobian using y0_diff (fixed) and y_alg (varying)
                        """
                        y = np.concatenate([y0_diff, y_alg])
                        return jac(0, y, inputs)[:, len_rhs:]

            else:
                jac_fn = None

            # Evaluate algebraic with new t and previous y0, if it's already close
            # enough then keep it
            if np.all(abs(algebraic(t, y0, inputs)) < self.tol):
                pybamm.logger.debug("Keeping same solution at t={}".format(t))
                y_alg[:, idx] = y0_alg
            # Otherwise calculate new y0
            else:
                sol = optimize.root(
                    root_fun,
                    y0_alg,
                    method=self.method,
                    tol=self.tol,
                    jac=jac_fn,
                    options=self.extra_options,
                )

                if sol.success and np.all(abs(sol.fun) < self.tol):
                    # update initial guess for the next iteration
                    y0_alg = sol.x
                    # update solution array
                    y_alg[:, idx] = y0_alg
                elif not sol.success:
                    raise pybamm.SolverError(
                        "Could not find acceptable solution: {}".format(
                            sol.message))
                else:
                    raise pybamm.SolverError("""
                        Could not find acceptable solution: solver terminated
                        successfully, but maximum solution error ({})
                        above tolerance ({})
                        """.format(np.max(sol.fun), self.tol))

        # Concatenate differential part
        y_diff = np.r_[[y0_diff] * len(t_eval)].T
        y_sol = np.r_[y_diff, y_alg]
        # Return solution object (no events, so pass None to t_event, y_event)
        return pybamm.Solution(t_eval, y_sol, termination="success")
Esempio n. 15
0
    def _integrate(self, model, t_eval, inputs=None):
        """
        Solve a DAE model defined by residuals with initial conditions y0.

        Parameters
        ----------
        model : :class:`pybamm.BaseModel`
            The model whose solution to calculate.
        t_eval : numeric type
            The times at which to compute the solution
        inputs : dict, optional
            Any external variables or input parameters to pass to the model when solving
        """
        inputs = inputs or {}

        if self.mode == "fast":
            integrator = self.get_integrator(model, t_eval, inputs)
            solution = self._run_integrator(integrator, model, model.y0,
                                            inputs, t_eval)
            solution.termination = "final time"
            return solution
        elif not model.events:
            pybamm.logger.info("No events found, running fast mode")
            integrator = self.get_integrator(model, t_eval, inputs)
            solution = self._run_integrator(integrator, model, model.y0,
                                            inputs, t_eval)
            solution.termination = "final time"
            return solution
        elif self.mode == "safe":
            # Step-and-check
            t = t_eval[0]
            init_event_signs = np.sign(
                np.concatenate([
                    event(t, model.y0) for event in model.terminate_events_eval
                ]))
            pybamm.logger.info("Start solving {} with {}".format(
                model.name, self.name))
            y0 = model.y0
            # Initialize solution
            solution = pybamm.Solution(np.array([t]), y0[:, np.newaxis])
            solution.solve_time = 0
            for dt in np.diff(t_eval):
                # Step
                solved = False
                count = 0
                while not solved:
                    integrator = self.get_integrator(model,
                                                     np.array([t, t + dt]),
                                                     inputs)
                    # Try to solve with the current step, if it fails then halve the
                    # step size and try again. This will make solution.t slightly
                    # different to t_eval, but shouldn't matter too much as it should
                    # only happen near events.
                    try:
                        current_step_sol = self._run_integrator(
                            integrator, model, y0, inputs,
                            np.array([t, t + dt]))
                        solved = True
                    except pybamm.SolverError:
                        dt /= 2
                    count += 1
                    if count >= self.max_step_decrease_count:
                        raise pybamm.SolverError("""
                            Maximum number of decreased steps occurred at t={}. Try
                            solving the model up to this time only
                            """.format(t))
                # Check most recent y
                new_event_signs = np.sign(
                    np.concatenate([
                        event(t, current_step_sol.y[:, -1])
                        for event in model.terminate_events_eval
                    ]))
                # Exit loop if the sign of an event changes
                if (new_event_signs != init_event_signs).any():
                    solution.termination = "event"
                    solution.t_event = solution.t[-1]
                    solution.y_event = solution.y[:, -1]
                    break
                else:
                    # assign temporary solve time
                    current_step_sol.solve_time = np.nan
                    # append solution from the current step to solution
                    solution.append(current_step_sol)
                    # update time
                    t += dt
                    # update y0
                    y0 = solution.y[:, -1]

            return solution
Esempio n. 16
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")
Esempio n. 17
0
    def _integrate(self, model, t_eval, inputs_dict=None):
        """
        Solve a model defined by dydt with initial conditions y0.

        Parameters
        ----------
        model : :class:`pybamm.BaseModel`
            The model whose solution to calculate.
        t_eval : numeric type
            The times at which to compute the solution
        inputs_dict : dict, optional
            Any input parameters to pass to the model when solving

        """
        if model.rhs_eval.form == "casadi":
            inputs = casadi.vertcat(*[x for x in inputs_dict.values()])
        else:
            inputs = inputs_dict

        y0 = model.y0
        if isinstance(y0, casadi.DM):
            y0 = y0.full().flatten()

        derivs = model.rhs_eval
        events = model.terminate_events_eval
        jacobian = model.jacobian_eval

        def eqsydot(t, y, return_ydot):
            return_ydot[:] = derivs(t, y, inputs)

        def rootfn(t, y, return_root):
            return_root[:] = [event(t, y, inputs) for event in events]

        if jacobian:
            jac_y0_t0 = jacobian(t_eval[0], y0, inputs)
            if sparse.issparse(jac_y0_t0):

                def jacfn(t, y, fy, J):
                    J[:][:] = jacobian(t, y, inputs).toarray()

                def jac_times_vecfn(v, Jv, t, y, userdata):
                    Jv[:] = userdata._jac_eval * v
                    return 0

            else:

                def jacfn(t, y, fy, J):
                    J[:][:] = jacobian(t, y, inputs)

                def jac_times_vecfn(v, Jv, t, y, userdata):
                    Jv[:] = np.matmul(userdata._jac_eval, v)
                    return 0

            def jac_times_setupfn(t, y, fy, userdata):
                userdata._jac_eval = jacobian(t, y, inputs)
                return 0

        extra_options = {
            **self.extra_options,
            "old_api": False,
            "rtol": self.rtol,
            "atol": self.atol,
        }

        # Read linsolver (defaults to dense)
        linsolver = extra_options.get("linsolver", "dense")

        if jacobian:
            if linsolver in ("dense", "lapackdense"):
                extra_options.update({"jacfn": jacfn})
            elif linsolver in ("spgmr", "spbcgs", "sptfqmr"):
                extra_options.update(
                    {
                        "jac_times_setupfn": jac_times_setupfn,
                        "jac_times_vecfn": jac_times_vecfn,
                        "user_data": self,
                    }
                )

        if events:
            extra_options.update({"rootfn": rootfn, "nr_rootfns": len(events)})

        ode_solver = scikits_odes.ode(self.method, eqsydot, **extra_options)
        timer = pybamm.Timer()
        sol = ode_solver.solve(t_eval, y0)
        integration_time = timer.time()

        # return solution, we need to tranpose y to match scipy's ivp interface
        if sol.flag in [0, 2]:
            # 0 = solved for all t_eval
            if sol.flag == 0:
                termination = "final time"
            # 2 = found root(s)
            elif sol.flag == 2:
                termination = "event"
            if sol.roots.t is None:
                t_root = None
            else:
                t_root = sol.roots.t
            sol = pybamm.Solution(
                sol.values.t,
                np.transpose(sol.values.y),
                model,
                inputs_dict,
                t_root,
                np.transpose(sol.roots.y),
                termination,
            )
            sol.integration_time = integration_time
            return sol
        else:
            raise pybamm.SolverError(sol.message)
Esempio n. 18
0
    def solve(self, model, t_eval=None, external_variables=None, inputs=None):
        """
        Execute the solver setup and calculate the solution of the model at
        specified times.

        Parameters
        ----------
        model : :class:`pybamm.BaseModel`
            The model whose solution to calculate. Must have attributes rhs and
            initial_conditions
        t_eval : numeric type
            The times (in seconds) at which to compute the solution
        external_variables : dict
            A dictionary of external variables and their corresponding
            values at the current time
        inputs : dict, optional
            Any input parameters to pass to the model when solving

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

        """
        pybamm.logger.info("Start solving {} with {}".format(
            model.name, self.name))

        # Make sure model isn't empty
        if len(model.rhs) == 0 and len(model.algebraic) == 0:
            if not isinstance(self, pybamm.DummySolver):
                raise pybamm.ModelError(
                    "Cannot solve empty model, use `pybamm.DummySolver` instead"
                )

        # t_eval can only be None if the solver is an algebraic solver. In that case
        # set it to 0
        if t_eval is None:
            if self.algebraic_solver is True:
                t_eval = np.array([0])
            else:
                raise ValueError("t_eval cannot be None")
        # If t_eval is provided as [t0, tf] return the solution at 100 points
        elif isinstance(t_eval, list):
            if len(t_eval) == 1 and self.algebraic_solver is True:
                pass
            elif len(t_eval) != 2:
                raise pybamm.SolverError(
                    "'t_eval' can be provided as an array of times at which to "
                    "return the solution, or as a list [t0, tf] where t0 is the "
                    "initial time and tf is the final time, but has been provided "
                    "as a list of length {}.".format(len(t_eval)))
            else:
                t_eval = np.linspace(t_eval[0], t_eval[-1], 100)

        # Make sure t_eval is monotonic
        if (np.diff(t_eval) < 0).any():
            raise pybamm.SolverError("t_eval must increase monotonically")

        # Set up external variables and inputs
        ext_and_inputs = self._set_up_ext_and_inputs(model, external_variables,
                                                     inputs)

        # Set up
        timer = pybamm.Timer()

        # Set up (if not done already)
        if model not in self.models_set_up:
            self.set_up(model, ext_and_inputs, t_eval)
            self.models_set_up.update({
                model: {
                    "initial conditions": model.concatenated_initial_conditions
                }
            })
        else:
            ics_set_up = self.models_set_up[model]["initial conditions"]
            # Check that initial conditions have not been updated
            if ics_set_up.id != model.concatenated_initial_conditions.id:
                # If the new initial conditions are different, set up again
                # Doing the whole setup again might be slow, but no need to prematurely
                # optimize this
                self.set_up(model, ext_and_inputs, t_eval)
                self.models_set_up[model][
                    "initial conditions"] = model.concatenated_initial_conditions
        set_up_time = timer.time()
        timer.reset()

        # (Re-)calculate consistent initial conditions
        self._set_initial_conditions(model, ext_and_inputs, update_rhs=True)

        # Non-dimensionalise time
        t_eval_dimensionless = t_eval / model.timescale_eval

        # Calculate discontinuities
        discontinuities = [
            event.expression.evaluate(inputs=inputs)
            for event in model.discontinuity_events_eval
        ]

        # make sure they are increasing in time
        discontinuities = sorted(discontinuities)

        # remove any identical discontinuities
        discontinuities = [
            v for i, v in enumerate(discontinuities)
            if (i == len(discontinuities) -
                1 or discontinuities[i] < discontinuities[i + 1]) and v > 0
        ]

        # remove any discontinuities after end of t_eval
        discontinuities = [
            v for v in discontinuities if v < t_eval_dimensionless[-1]
        ]

        if len(discontinuities) > 0:
            pybamm.logger.info(
                "Discontinuity events found at t = {}".format(discontinuities))
        else:
            pybamm.logger.info("No discontinuity events found")

        # insert time points around discontinuities in t_eval
        # keep track of sub sections to integrate by storing start and end indices
        start_indices = [0]
        end_indices = []
        eps = sys.float_info.epsilon
        for dtime in discontinuities:
            dindex = np.searchsorted(t_eval_dimensionless, dtime, side="left")
            end_indices.append(dindex + 1)
            start_indices.append(dindex + 1)
            if dtime - eps < t_eval_dimensionless[dindex] < dtime + eps:
                t_eval_dimensionless[dindex] += eps
                t_eval_dimensionless = np.insert(t_eval_dimensionless, dindex,
                                                 dtime - eps)
            else:
                t_eval_dimensionless = np.insert(t_eval_dimensionless, dindex,
                                                 [dtime - eps, dtime + eps])
        end_indices.append(len(t_eval_dimensionless))

        # integrate separately over each time segment and accumulate into the solution
        # object, restarting the solver at each discontinuity (and recalculating a
        # consistent state afterwards if a dae)
        old_y0 = model.y0
        solution = None
        for start_index, end_index in zip(start_indices, end_indices):
            pybamm.logger.info("Calling solver for {} < t < {}".format(
                t_eval_dimensionless[start_index] * model.timescale_eval,
                t_eval_dimensionless[end_index - 1] * model.timescale_eval,
            ))
            new_solution = self._integrate(
                model, t_eval_dimensionless[start_index:end_index],
                ext_and_inputs)
            new_solution.solve_time = timer.time()
            if solution is None:
                solution = new_solution
            else:
                solution.append(new_solution, start_index=0)

            if solution.termination != "final time":
                break

            if end_index != len(t_eval_dimensionless):
                # setup for next integration subsection
                last_state = solution.y[:, -1]
                # update y0 (for DAE solvers, this updates the initial guess for the
                # rootfinder)
                model.y0 = last_state
                if len(model.algebraic) > 0:
                    model.y0 = self.calculate_consistent_state(
                        model, t_eval_dimensionless[end_index], ext_and_inputs)

        # Assign times
        solution.set_up_time = set_up_time
        solution.solve_time = timer.time()

        # restore old y0
        model.y0 = old_y0

        # Add model and inputs to solution
        solution.model = model
        solution.inputs = ext_and_inputs

        # Copy the timescale_eval and lengthscale_evals
        solution.timescale_eval = model.timescale_eval
        solution.length_scales_eval = model.length_scales_eval

        # Identify the event that caused termination
        termination = self.get_termination_reason(solution, model.events)

        pybamm.logger.info("Finish solving {} ({})".format(
            model.name, termination))
        pybamm.logger.info(
            "Set-up time: {}, Solve time: {}, Total time: {}".format(
                timer.format(solution.set_up_time),
                timer.format(solution.solve_time),
                timer.format(solution.total_time),
            ))

        # Raise error if solution only contains one timestep (except for algebraic
        # solvers, where we may only expect one time in the solution)
        if self.algebraic_solver is False and len(solution.t) == 1:
            raise pybamm.SolverError(
                "Solution time vector has length 1. "
                "Check whether simulation terminated too early.")

        return solution
Esempio n. 19
0
    def solve(
        self,
        t_eval=None,
        solver=None,
        check_model=True,
        save_at_cycles=None,
        starting_solution=None,
        **kwargs,
    ):
        """
        A method to solve the model. This method will automatically build
        and set the model parameters if not already done so.

        Parameters
        ----------
        t_eval : numeric type, optional
            The times (in seconds) at which to compute the solution. Can be
            provided as an array of times at which to return the solution, or as a
            list `[t0, tf]` where `t0` is the initial time and `tf` is the final time.
            If provided as a list the solution is returned at 100 points within the
            interval `[t0, tf]`.

            If not using an experiment or running a drive cycle simulation (current
            provided as data) `t_eval` *must* be provided.

            If running an experiment the values in `t_eval` are ignored, and the
            solution times are specified by the experiment.

            If None and the parameter "Current function [A]" is read from data
            (i.e. drive cycle simulation) the model will be solved at the times
            provided in the data.
        solver : :class:`pybamm.BaseSolver`, optional
            The solver to use to solve the model. If None, Simulation.solver is used
        check_model : bool, optional
            If True, model checks are performed after discretisation (see
            :meth:`pybamm.Discretisation.process_model`). Default is True.
        save_at_cycles : int or list of ints, optional
            Which cycles to save the full sub-solutions for. If None, all cycles are
            saved. If int, every multiple of save_at_cycles is saved. If list, every
            cycle in the list is saved.
        starting_solution : :class:`pybamm.Solution`
            The solution to start stepping from. If None (default), then self._solution
            is used. Must be None if not using an experiment.
        **kwargs
            Additional key-word arguments passed to `solver.solve`.
            See :meth:`pybamm.BaseSolver.solve`.
        """
        # Setup
        if solver is None:
            solver = self.solver

        if self.operating_mode in ["without experiment", "drive cycle"]:
            self.build(check_model=check_model)
            if save_at_cycles is not None:
                raise ValueError(
                    "'save_at_cycles' option can only be used if simulating an "
                    "Experiment ")
            if starting_solution is not None:
                raise ValueError(
                    "starting_solution can only be provided if simulating an Experiment"
                )
            if self.operating_mode == "without experiment":
                if t_eval is None:
                    raise pybamm.SolverError(
                        "'t_eval' must be provided if not using an experiment or "
                        "simulating a drive cycle. 't_eval' can be provided as an "
                        "array of times at which to return the solution, or as a "
                        "list [t0, tf] where t0 is the initial time and tf is the "
                        "final time. "
                        "For a constant current (dis)charge the suggested 't_eval'  "
                        "is [0, 3700/C] where C is the C-rate. "
                        "For example, run\n\n"
                        "\tsim.solve([0, 3700])\n\n"
                        "for a 1C discharge.")

            elif self.operating_mode == "drive cycle":
                # For drive cycles (current provided as data) we perform additional
                # tests on t_eval (if provided) to ensure the returned solution
                # captures the input.
                time_data = self._parameter_values["Current function [A]"].x[0]
                # If no t_eval is provided, we use the times provided in the data.
                if t_eval is None:
                    pybamm.logger.info(
                        "Setting t_eval as specified by the data")
                    t_eval = time_data
                # If t_eval is provided we first check if it contains all of the
                # times in the data to within 10-12. If it doesn't, we then check
                # that the largest gap in t_eval is smaller than the smallest gap in
                # the time data (to ensure the resolution of t_eval is fine enough).
                # We only raise a warning here as users may genuinely only want
                # the solution returned at some specified points.
                elif (set(np.round(time_data, 12)).issubset(
                        set(np.round(t_eval, 12)))) is False:
                    warnings.warn(
                        """
                        t_eval does not contain all of the time points in the data
                        set. Note: passing t_eval = None automatically sets t_eval
                        to be the points in the data.
                        """,
                        pybamm.SolverWarning,
                    )
                    dt_data_min = np.min(np.diff(time_data))
                    dt_eval_max = np.max(np.diff(t_eval))
                    if dt_eval_max > dt_data_min + sys.float_info.epsilon:
                        warnings.warn(
                            """
                            The largest timestep in t_eval ({}) is larger than
                            the smallest timestep in the data ({}). The returned
                            solution may not have the correct resolution to accurately
                            capture the input. Try refining t_eval. Alternatively,
                            passing t_eval = None automatically sets t_eval to be the
                            points in the data.
                            """.format(dt_eval_max, dt_data_min),
                            pybamm.SolverWarning,
                        )

            self._solution = solver.solve(self.built_model, t_eval, **kwargs)

        elif self.operating_mode == "with experiment":
            self.build_for_experiment(check_model=check_model)
            if t_eval is not None:
                pybamm.logger.warning(
                    "Ignoring t_eval as solution times are specified by the experiment"
                )
            # Re-initialize solution, e.g. for solving multiple times with different
            # inputs without having to build the simulation again
            self._solution = starting_solution
            # Step through all experimental conditions
            inputs = kwargs.get("inputs", {})
            pybamm.logger.info("Start running experiment")
            timer = pybamm.Timer()

            if starting_solution is None:
                starting_solution_cycles = []
            else:
                starting_solution_cycles = starting_solution.cycles.copy()

            cycle_offset = len(starting_solution_cycles)
            all_cycle_solutions = starting_solution_cycles
            current_solution = starting_solution

            idx = 0
            num_cycles = len(self.experiment.cycle_lengths)
            feasible = True  # simulation will stop if experiment is infeasible
            for cycle_num, cycle_length in enumerate(
                    self.experiment.cycle_lengths, start=1):
                pybamm.logger.notice(
                    f"Cycle {cycle_num+cycle_offset}/{num_cycles+cycle_offset} "
                    f"({timer.time()} elapsed) " + "-" * 20)
                steps = []
                cycle_solution = None

                for step_num in range(1, cycle_length + 1):
                    exp_inputs = self._experiment_inputs[idx]
                    dt = self._experiment_times[idx]
                    op_conds_str = self.experiment.operating_conditions_strings[
                        idx]
                    op_conds_elec = self.experiment.operating_conditions[
                        idx][:2]
                    model = self.op_conds_to_built_models[op_conds_elec]
                    # Use 1-indexing for printing cycle number as it is more
                    # human-intuitive
                    pybamm.logger.notice(
                        f"Cycle {cycle_num+cycle_offset}/{num_cycles+cycle_offset}, "
                        f"step {step_num}/{cycle_length}: {op_conds_str}")
                    inputs.update(exp_inputs)
                    kwargs["inputs"] = inputs
                    # Make sure we take at least 2 timesteps
                    npts = max(int(round(dt / exp_inputs["period"])) + 1, 2)
                    step_solution = solver.step(
                        current_solution,
                        model,
                        dt,
                        npts=npts,
                        save=False,
                        **kwargs,
                    )
                    steps.append(step_solution)
                    current_solution = step_solution

                    cycle_solution = cycle_solution + step_solution

                    # Only allow events specified by experiment
                    if not (cycle_solution is None
                            or cycle_solution.termination == "final time"
                            or "[experiment]" in cycle_solution.termination):
                        feasible = False
                        break

                    # Increment index for next iteration
                    idx += 1

                # Break if the experiment is infeasible
                if feasible is False:
                    pybamm.logger.warning(
                        "\n\n\tExperiment is infeasible: '{}' ".format(
                            cycle_solution.termination) +
                        "was triggered during '{}'. ".format(
                            self.experiment.operating_conditions_strings[idx])
                        + "The returned solution only contains the first "
                        "{} cycles. ".format(cycle_num - 1 + cycle_offset) +
                        "Try reducing the current, shortening the time interval, "
                        "or reducing the period.\n\n")
                    break

                # At the final step of the inner loop we save the cycle
                self._solution = self.solution + cycle_solution
                cycle_solution.steps = steps
                all_cycle_solutions.append(cycle_solution)

            if self.solution is not None:
                self.solution.cycles = all_cycle_solutions

            pybamm.logger.notice(
                "Finish experiment simulation, took {}".format(timer.time()))

        return self.solution
Esempio n. 20
0
    def step(
        self,
        old_solution,
        model,
        dt,
        npts=2,
        external_variables=None,
        inputs=None,
        save=True,
    ):
        """
        Step the solution of the model forward by a given time increment. The
        first time this method is called it executes the necessary setup by
        calling `self.set_up(model)`.

        Parameters
        ----------
        old_solution : :class:`pybamm.Solution` or None
            The previous solution to be added to. If `None`, a new solution is created.
        model : :class:`pybamm.BaseModel`
            The model whose solution to calculate. Must have attributes rhs and
            initial_conditions
        dt : numeric type
            The timestep (in seconds) over which to step the solution
        npts : int, optional
            The number of points at which the solution will be returned during
            the step dt. default is 2 (returns the solution at t0 and t0 + dt).
        external_variables : dict
            A dictionary of external variables and their corresponding
            values at the current time
        inputs : dict, optional
            Any input parameters to pass to the model when solving
        save : bool
            Turn on to store the solution of all previous timesteps

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

        """

        if old_solution is not None and not (
                old_solution.termination == "final time"
                or "[experiment]" in old_solution.termination):
            # Return same solution as an event has already been triggered
            # With hack to allow stepping past experiment current / voltage cut-off
            return old_solution

        # Make sure model isn't empty
        if len(model.rhs) == 0 and len(model.algebraic) == 0:
            if not isinstance(self, pybamm.DummySolver):
                raise pybamm.ModelError(
                    "Cannot step empty model, use `pybamm.DummySolver` instead"
                )

        # Set timer
        timer = pybamm.Timer()

        # Set up external variables and inputs
        external_variables = external_variables or {}
        inputs = inputs or {}
        ext_and_inputs = {**external_variables, **inputs}

        # Check that any inputs that may affect the scaling have not changed
        # Set model timescale
        temp_timescale_eval = model.timescale.evaluate(inputs=inputs)
        # Set model lengthscales
        temp_length_scales_eval = {
            domain: scale.evaluate(inputs=inputs)
            for domain, scale in model.length_scales.items()
        }
        if old_solution is not None:
            if temp_timescale_eval != old_solution.timescale_eval:
                raise pybamm.SolverError(
                    "The model timescale is a function of an input parameter "
                    "and the value has changed between steps!")
            for domain in temp_length_scales_eval.keys():
                old_dom_eval = old_solution.length_scales_eval[domain]
                if temp_length_scales_eval[domain] != old_dom_eval:
                    pybamm.logger.error(
                        "The {} domain lengthscale is a function of an input "
                        "parameter and the value has changed between "
                        "steps!".format(domain))
        # Run set up on first step
        if old_solution is None:
            pybamm.logger.info("Start stepping {} with {}".format(
                model.name, self.name))
            self.set_up(model, ext_and_inputs)
            t = 0.0
        else:
            # initialize with old solution
            t = old_solution.t[-1]
            model.y0 = old_solution.y[:, -1]
        set_up_time = timer.time()

        # (Re-)calculate consistent initial conditions
        self._set_initial_conditions(model, ext_and_inputs, update_rhs=False)

        # Non-dimensionalise dt
        dt_dimensionless = dt / model.timescale_eval

        # Step
        t_eval = np.linspace(t, t + dt_dimensionless, npts)
        pybamm.logger.info("Calling solver")
        timer.reset()
        solution = self._integrate(model, t_eval, ext_and_inputs)

        # Assign times
        solution.set_up_time = set_up_time
        solution.solve_time = timer.time()

        # Add model and inputs to solution
        solution.model = model
        solution.inputs = ext_and_inputs

        # Copy the timescale_eval and lengthscale_evals
        solution.timescale_eval = temp_timescale_eval
        solution.length_scales_eval = temp_length_scales_eval

        # Identify the event that caused termination
        termination = self.get_termination_reason(solution, model.events)

        pybamm.logger.debug("Finish stepping {} ({})".format(
            model.name, termination))
        if set_up_time:
            pybamm.logger.debug(
                "Set-up time: {}, Step time: {}, Total time: {}".format(
                    timer.format(solution.set_up_time),
                    timer.format(solution.solve_time),
                    timer.format(solution.total_time),
                ))
        else:
            pybamm.logger.debug("Step time: {}".format(
                timer.format(solution.solve_time)))
        if save is False or old_solution is None:
            return solution
        else:
            return old_solution + solution
Esempio n. 21
0
    def _integrate(self, model, t_eval, inputs=None):
        """
        Calculate the solution of the algebraic equations through root-finding

        Parameters
        ----------
        model : :class:`pybamm.BaseModel`
            The model whose solution to calculate.
        t_eval : :class:`numpy.array`, size (k,)
            The times at which to compute the solution
        inputs : dict, optional
            Any input parameters to pass to the model when solving
        """
        y0 = model.y0

        y = np.empty((len(y0), len(t_eval)))

        # Set up
        inputs = casadi.vertcat(*[x for x in inputs.values()])
        t_sym = casadi.MX.sym("t")
        y_sym = casadi.MX.sym("y_alg", y0.shape[0])
        p_sym = casadi.MX.sym("p", inputs.shape[0])

        t_p_sym = casadi.vertcat(t_sym, p_sym)
        alg = model.casadi_algebraic(t_sym, y_sym, p_sym)

        # Set up rootfinder
        roots = casadi.rootfinder(
            "roots",
            "newton",
            dict(x=y_sym, p=t_p_sym, g=alg),
            {
                **self.extra_options, "abstol": self.tol
            },
        )
        for idx, t in enumerate(t_eval):
            # Evaluate algebraic with new t and previous y0, if it's already close
            # enough then keep it
            if np.all(abs(model.algebraic_eval(t, y0, inputs)) < self.tol):
                pybamm.logger.debug("Keeping same solution at t={}".format(
                    t * model.timescale_eval))
                y[:, idx] = y0
            # Otherwise calculate new y0
            else:
                t_inputs = casadi.vertcat(t, inputs)
                # Solve
                try:
                    y_sol = roots(y0, t_inputs).full().flatten()
                    success = True
                    message = None
                    # Check final output
                    fun = model.casadi_algebraic(t, y_sol, inputs)
                except RuntimeError as err:
                    success = False
                    message = err.args[0]
                    fun = None

                if success and np.all(casadi.fabs(fun) < self.tol):
                    # update initial guess for the next iteration
                    y0 = y_sol
                    # update solution array
                    y[:, idx] = y_sol
                elif not success:
                    raise pybamm.SolverError(
                        "Could not find acceptable solution: {}".format(
                            message))
                else:
                    raise pybamm.SolverError("""
                        Could not find acceptable solution: solver terminated
                        successfully, but maximum solution error ({})
                        above tolerance ({})
                        """.format(casadi.mmax(fun), self.tol))

        # Return solution object (no events, so pass None to t_event, y_event)
        return pybamm.Solution(t_eval, y, termination="success")
Esempio n. 22
0
    def _integrate(self, model, t_eval, inputs_dict=None):
        """
        Solve a DAE model defined by residuals with initial conditions y0.

        Parameters
        ----------
        model : :class:`pybamm.BaseModel`
            The model whose solution to calculate.
        t_eval : numeric type
            The times at which to compute the solution
        inputs_dict : dict, optional
            Any external variables or input parameters to pass to the model when solving
        """
        # Record whether there are any symbolic inputs
        inputs_dict = inputs_dict or {}
        has_symbolic_inputs = any(
            isinstance(v, casadi.MX) for v in inputs_dict.values())

        # convert inputs to casadi format
        inputs = casadi.vertcat(*[x for x in inputs_dict.values()])

        # Calculate initial event signs needed for some of the modes
        if (has_symbolic_inputs is False and self.mode != "fast"
                and model.terminate_events_eval):
            init_event_signs = np.sign(
                np.concatenate([
                    event(t_eval[0], model.y0, inputs)
                    for event in model.terminate_events_eval
                ]))
        else:
            init_event_signs = np.sign([])

        if has_symbolic_inputs:
            # Create integrator without grid to avoid having to create several times
            self.create_integrator(model, inputs)
            solution = self._run_integrator(model,
                                            model.y0,
                                            inputs_dict,
                                            inputs,
                                            t_eval,
                                            use_grid=False)
            solution.termination = "final time"
            return solution
        elif self.mode in ["fast", "fast with events"] or not model.events:
            if not model.events:
                pybamm.logger.info("No events found, running fast mode")
            if self.mode == "fast with events":
                # Create the integrator with an event switch that will set the rhs to
                # zero when voltage limits are crossed
                use_event_switch = True
            else:
                use_event_switch = False
            # Create an integrator with the grid (we just need to do this once)
            self.create_integrator(model,
                                   inputs,
                                   t_eval,
                                   use_event_switch=use_event_switch)
            solution = self._run_integrator(model, model.y0, inputs_dict,
                                            inputs, t_eval)
            # Check if the sign of an event changes, if so find an accurate
            # termination point and exit
            solution = self._solve_for_event(solution, init_event_signs)
            return solution
        elif self.mode in ["safe", "safe without grid"]:
            y0 = model.y0
            # Step-and-check
            t = t_eval[0]
            t_f = t_eval[-1]

            pybamm.logger.debug("Start solving {} with {}".format(
                model.name, self.name))

            if self.mode == "safe without grid":
                # in "safe without grid" mode,
                # create integrator once, without grid,
                # to avoid having to create several times
                self.create_integrator(model, inputs)
                # Initialize solution
                solution = pybamm.Solution(np.array([t]), y0, model,
                                           inputs_dict)
                solution.solve_time = 0
                solution.integration_time = 0
                use_grid = False
            else:
                solution = None
                use_grid = True

            # Try to integrate in global steps of size dt_max. Note: dt_max must
            # be at least as big as the the biggest step in t_eval (multiplied
            # by some tolerance, here 1.01) to avoid an empty integration window below
            if self.dt_max:
                # Non-dimensionalise provided dt_max
                dt_max = self.dt_max / model.timescale_eval
            else:
                dt_max = 0.01
            dt_eval_max = np.max(np.diff(t_eval)) * 1.01
            dt_max = np.max([dt_max, dt_eval_max])
            while t < t_f:
                # Step
                solved = False
                count = 0
                dt = dt_max
                while not solved:
                    # Get window of time to integrate over (so that we return
                    # all the points in t_eval, not just t and t+dt)
                    t_window = np.concatenate(
                        ([t], t_eval[(t_eval > t) & (t_eval < t + dt)]))
                    # Sometimes near events the solver fails between two time
                    # points in t_eval (i.e. no points t < t_i < t+dt for t_i
                    # in t_eval), so we simply integrate from t to t+dt
                    if len(t_window) == 1:
                        t_window = np.array([t, t + dt])

                    if self.mode == "safe":
                        # update integrator with the grid
                        self.create_integrator(model, inputs, t_window)
                    # Try to solve with the current global step, if it fails then
                    # halve the step size and try again.
                    try:
                        current_step_sol = self._run_integrator(
                            model,
                            y0,
                            inputs_dict,
                            inputs,
                            t_window,
                            use_grid=use_grid)
                        solved = True
                    except pybamm.SolverError:
                        dt /= 2
                        # also reduce maximum step size for future global steps
                        dt_max = dt
                    count += 1
                    if count >= self.max_step_decrease_count:
                        raise pybamm.SolverError(
                            "Maximum number of decreased steps occurred at t={}. Try "
                            "solving the model up to this time only or reducing dt_max "
                            "(currently, dt_max={})."
                            "".format(t * model.timescale_eval,
                                      dt_max * model.timescale_eval))
                # Check if the sign of an event changes, if so find an accurate
                # termination point and exit
                current_step_sol = self._solve_for_event(
                    current_step_sol, init_event_signs)
                # assign temporary solve time
                current_step_sol.solve_time = np.nan
                # append solution from the current step to solution
                solution = solution + current_step_sol
                if current_step_sol.termination == "event":
                    break
                else:
                    # update time
                    t = t_window[-1]
                    # update y0
                    y0 = solution.all_ys[-1][:, -1]
            return solution
Esempio n. 23
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")
Esempio n. 24
0
    def _run_integrator(self,
                        model,
                        y0,
                        inputs_dict,
                        inputs,
                        t_eval,
                        use_grid=True):
        pybamm.logger.debug("Running CasADi integrator")
        if use_grid is True:
            t_eval_shifted = t_eval - t_eval[0]
            t_eval_shifted_rounded = np.round(t_eval_shifted,
                                              decimals=12).tobytes()
            integrator = self.integrators[model][t_eval_shifted_rounded]
        else:
            integrator = self.integrators[model]["no grid"]
        len_rhs = model.concatenated_rhs.size
        y0_diff = y0[:len_rhs]
        y0_alg = y0[len_rhs:]
        try:
            # Try solving
            if use_grid is True:
                t_min = t_eval[0]
                inputs_with_tmin = casadi.vertcat(inputs, t_min)
                # Call the integrator once, with the grid
                timer = pybamm.Timer()
                casadi_sol = integrator(x0=y0_diff,
                                        z0=y0_alg,
                                        p=inputs_with_tmin,
                                        **self.extra_options_call)
                integration_time = timer.time()
                y_sol = casadi.vertcat(casadi_sol["xf"], casadi_sol["zf"])
                sol = pybamm.Solution(t_eval, y_sol, model, inputs_dict)
                sol.integration_time = integration_time
                return sol
            else:
                # Repeated calls to the integrator
                x = y0_diff
                z = y0_alg
                y_diff = x
                y_alg = z
                for i in range(len(t_eval) - 1):
                    t_min = t_eval[i]
                    t_max = t_eval[i + 1]
                    inputs_with_tlims = casadi.vertcat(inputs, t_min, t_max)
                    timer = pybamm.Timer()
                    casadi_sol = integrator(x0=x,
                                            z0=z,
                                            p=inputs_with_tlims,
                                            **self.extra_options_call)
                    integration_time = timer.time()
                    x = casadi_sol["xf"]
                    z = casadi_sol["zf"]
                    y_diff = casadi.horzcat(y_diff, x)
                    if not z.is_empty():
                        y_alg = casadi.horzcat(y_alg, z)
                if z.is_empty():
                    sol = pybamm.Solution(t_eval, y_diff, model, inputs_dict)
                else:
                    y_sol = casadi.vertcat(y_diff, y_alg)
                    sol = pybamm.Solution(t_eval, y_sol, model, inputs_dict)

                sol.integration_time = integration_time
                return sol
        except RuntimeError as e:
            # If it doesn't work raise error
            raise pybamm.SolverError(e.args[0])
Esempio n. 25
0
    def solve(self, model, t_eval, external_variables=None, inputs=None):
        """
        Execute the solver setup and calculate the solution of the model at
        specified times.

        Parameters
        ----------
        model : :class:`pybamm.BaseModel`
            The model whose solution to calculate. Must have attributes rhs and
            initial_conditions
        t_eval : numeric type
            The times at which to compute the solution
        external_variables : dict
            A dictionary of external variables and their corresponding
            values at the current time
        inputs : dict, optional
            Any input parameters to pass to the model when solving

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

        """
        pybamm.logger.info("Start solving {} with {}".format(
            model.name, self.name))

        # Make sure model isn't empty
        if len(model.rhs) == 0 and len(model.algebraic) == 0:
            raise pybamm.ModelError("Cannot solve empty model")

        # Make sure t_eval is monotonic
        if (np.diff(t_eval) < 0).any():
            raise pybamm.SolverError("t_eval must increase monotonically")

        # Non-dimensionalise t_eval

        # Set up
        timer = pybamm.Timer()

        # Set up external variables and inputs
        external_variables = external_variables or {}
        inputs = inputs or {}
        ext_and_inputs = {**external_variables, **inputs}

        # Raise warning if t_eval looks like it was supposed to be dimensionless
        # already
        if t_eval[-1] < 0.5:
            raise pybamm.SolverError(
                """It looks like t_eval might be dimensionless.
                t_eval should now be provided in seconds""")

        # Set up (if not done already)
        if model not in self.models_set_up:
            self.set_up(model, ext_and_inputs)
            set_up_time = timer.time()
            self.models_set_up.add(model)
        else:
            set_up_time = 0
        # Non-dimensionalise time
        t_eval_dimensionless = t_eval / model.timescale_eval
        # Solve
        # Set inputs and external
        self.set_inputs(model, ext_and_inputs)

        # Calculate discontinuities
        discontinuities = [
            event.expression.evaluate(u=inputs)
            for event in model.discontinuity_events_eval
        ]

        # make sure they are increasing in time
        discontinuities = sorted(discontinuities)

        # remove any identical discontinuities
        discontinuities = [
            v for i, v in enumerate(discontinuities)
            if (i == len(discontinuities) -
                1 or discontinuities[i] < discontinuities[i + 1]) and v > 0
        ]

        if len(discontinuities) > 0:
            pybamm.logger.info(
                "Discontinuity events found at t = {}".format(discontinuities))
        else:
            pybamm.logger.info("No discontinuity events found")

        # insert time points around discontinuities in t_eval
        # keep track of sub sections to integrate by storing start and end indices
        start_indices = [0]
        end_indices = []
        eps = sys.float_info.epsilon
        for dtime in discontinuities:
            dindex = np.searchsorted(t_eval_dimensionless, dtime, side="left")
            end_indices.append(dindex + 1)
            start_indices.append(dindex + 1)
            if dtime - eps < t_eval_dimensionless[dindex] < dtime + eps:
                t_eval_dimensionless[dindex] += eps
                t_eval_dimensionless = np.insert(t_eval_dimensionless, dindex,
                                                 dtime - eps)
            else:
                t_eval_dimensionless = np.insert(t_eval_dimensionless, dindex,
                                                 [dtime - eps, dtime + eps])
        end_indices.append(len(t_eval_dimensionless))

        # integrate separately over each time segment and accumulate into the solution
        # object, restarting the solver at each discontinuity (and recalculating a
        # consistent state afterwards if a dae)
        old_y0 = model.y0
        solution = None
        for start_index, end_index in zip(start_indices, end_indices):
            pybamm.logger.info("Calling solver for {} < t < {}".format(
                t_eval_dimensionless[start_index] * model.timescale_eval,
                t_eval_dimensionless[end_index - 1] * model.timescale_eval,
            ))
            timer.reset()
            new_solution = self._integrate(
                model, t_eval_dimensionless[start_index:end_index],
                ext_and_inputs)
            new_solution.solve_time = timer.time()
            if solution is None:
                solution = new_solution
            else:
                solution.append(new_solution, start_index=0)

            if solution.termination != "final time":
                break

            if end_index != len(t_eval_dimensionless):
                # setup for next integration subsection
                last_state = solution.y[:, -1]
                if len(model.algebraic) > 0:
                    model.y0 = self.calculate_consistent_state(
                        model,
                        t_eval_dimensionless[end_index],
                        last_state,
                        ext_and_inputs,
                    )
                else:
                    model.y0 = last_state

        # restore old y0
        model.y0 = old_y0

        # Assign times
        solution.set_up_time = set_up_time

        # Add model and inputs to solution
        solution.model = model
        solution.inputs = ext_and_inputs

        # Identify the event that caused termination
        termination = self.get_termination_reason(solution, model.events)

        pybamm.logger.info("Finish solving {} ({})".format(
            model.name, termination))
        pybamm.logger.info(
            "Set-up time: {}, Solve time: {}, Total time: {}".format(
                timer.format(solution.set_up_time),
                timer.format(solution.solve_time),
                timer.format(solution.total_time),
            ))
        return solution
Esempio n. 26
0
    def integrate(self, residuals, y0, t_eval, events, mass_matrix, jacobian):
        """
        Solve a DAE model defined by residuals with initial conditions y0.

        Parameters
        ----------
        residuals : method
            A function that takes in t, y and ydot and returns the residuals of the
            equations
        y0 : numeric type
            The initial conditions
        t_eval : numeric type
            The times at which to compute the solution
        events : method,
            A function that takes in t and y and returns conditions for the solver to
            stop
        mass_matrix : array_like,
            The (sparse) mass matrix for the chosen spatial method.
        jacobian : method,
            A function that takes in t and y and returns the Jacobian. If
            None, the solver will approximate the Jacobian.
            (see `SUNDIALS docs. <https://computation.llnl.gov/projects/sundials>`).
        """

        if jacobian is None:
            pybamm.SolverError("KLU requires the Jacobian to be provided")

        if events is None:
            pybamm.SolverError("KLU requires events to be provided")

        rtol = self._rtol
        atol = self._atol

        if jacobian:
            jac_y0_t0 = jacobian(t_eval[0], y0)
            if sparse.issparse(jac_y0_t0):

                def jacfn(t, y, cj):
                    j = jacobian(t, y) - cj * mass_matrix
                    return j

            else:

                def jacfn(t, y, cj):
                    jac_eval = jacobian(t, y) - cj * mass_matrix
                    return sparse.csr_matrix(jac_eval)

        class SundialsJacobian:
            def __init__(self):
                self.J = None

                random = np.random.random(size=y0.size)
                J = jacfn(10, random, 20)
                self.nnz = J.nnz  # hoping nnz remains constant...

            def jac_res(self, t, y, cj):
                # must be of form j_res = (dr/dy) - (cj) (dr/dy')
                # cj is just the input parameter
                # see p68 of the ida_guide.pdf for more details
                self.J = jacfn(t, y, cj)

            def get_jac_data(self):
                return self.J.data

            def get_jac_row_vals(self):
                return self.J.indices

            def get_jac_col_ptrs(self):
                return self.J.indptr

        # solver works with ydot0 set to zero
        ydot0 = np.zeros_like(y0)

        jac_class = SundialsJacobian()

        num_of_events = len(events)
        use_jac = 1

        def rootfn(t, y):
            return_root = np.ones((num_of_events, ))
            return_root[:] = [event(t, y) for event in events]

            return return_root

        # get ids of rhs and algebraic variables
        rhs_ids = np.ones(self.rhs(0, y0).shape)
        alg_ids = np.zeros(self.algebraic(0, y0).shape)
        ids = np.concatenate((rhs_ids, alg_ids))

        # solve
        sol = idaklu.solve(
            t_eval,
            y0,
            ydot0,
            self.residuals,
            jac_class.jac_res,
            jac_class.get_jac_data,
            jac_class.get_jac_row_vals,
            jac_class.get_jac_col_ptrs,
            jac_class.nnz,
            rootfn,
            num_of_events,
            use_jac,
            ids,
            rtol,
            atol,
        )

        t = sol.t
        number_of_timesteps = t.size
        number_of_states = y0.size
        y_out = sol.y.reshape((number_of_timesteps, number_of_states))

        # return solution, we need to tranpose y to match scipy's interface
        if sol.flag in [0, 2]:
            # 0 = solved for all t_eval
            if sol.flag == 0:
                termination = "final time"
            # 2 = found root(s)
            elif sol.flag == 2:
                termination = "event"
            return pybamm.Solution(sol.t, np.transpose(y_out), t[-1],
                                   np.transpose(y_out[-1]), termination)
        else:
            raise pybamm.SolverError(sol.message)
Esempio n. 27
0
    def _integrate(self, model, t_eval, inputs=None):
        """
        Solve a DAE model defined by residuals with initial conditions y0.

        Parameters
        ----------
        model : :class:`pybamm.BaseModel`
            The model whose solution to calculate.
        t_eval : numeric type
            The times at which to compute the solution
        """
        if model.rhs_eval.form == "casadi":
            # stack inputs
            inputs = casadi.vertcat(*[x for x in inputs.values()])

        if model.jacobian_eval is None:
            raise pybamm.SolverError(
                "KLU requires the Jacobian to be provided")

        try:
            atol = model.atol
        except AttributeError:
            atol = self._atol

        y0 = model.y0
        if isinstance(y0, casadi.DM):
            y0 = y0.full().flatten()

        rtol = self._rtol
        atol = self._check_atol_type(atol, y0.size)

        mass_matrix = model.mass_matrix.entries

        if model.jacobian_eval:
            jac_y0_t0 = model.jacobian_eval(t_eval[0], y0, inputs)
            if sparse.issparse(jac_y0_t0):

                def jacfn(t, y, cj):
                    j = model.jacobian_eval(t, y, inputs) - cj * mass_matrix
                    return j

            else:

                def jacfn(t, y, cj):
                    jac_eval = model.jacobian_eval(t, y,
                                                   inputs) - cj * mass_matrix
                    return sparse.csr_matrix(jac_eval)

        class SundialsJacobian:
            def __init__(self):
                self.J = None

                random = np.random.random(size=y0.size)
                J = jacfn(10, random, 20)
                self.nnz = J.nnz  # hoping nnz remains constant...

            def jac_res(self, t, y, cj):
                # must be of form j_res = (dr/dy) - (cj) (dr/dy')
                # cj is just the input parameter
                # see p68 of the ida_guide.pdf for more details
                self.J = jacfn(t, y, cj)

            def get_jac_data(self):
                return self.J.data

            def get_jac_row_vals(self):
                return self.J.indices

            def get_jac_col_ptrs(self):
                return self.J.indptr

        # solver works with ydot0 set to zero
        ydot0 = np.zeros_like(y0)

        jac_class = SundialsJacobian()

        num_of_events = len(model.terminate_events_eval)
        use_jac = 1

        def rootfn(t, y):
            return_root = np.ones((num_of_events, ))
            return_root[:] = [
                event(t, y, inputs) for event in model.terminate_events_eval
            ]

            return return_root

        # get ids of rhs and algebraic variables
        rhs_ids = np.ones(model.rhs_eval(0, y0, inputs).shape)
        alg_ids = np.zeros(len(y0) - len(rhs_ids))
        ids = np.concatenate((rhs_ids, alg_ids))

        # solve
        timer = pybamm.Timer()
        sol = idaklu.solve(
            t_eval,
            y0,
            ydot0,
            lambda t, y, ydot: model.residuals_eval(t, y, ydot, inputs),
            jac_class.jac_res,
            jac_class.get_jac_data,
            jac_class.get_jac_row_vals,
            jac_class.get_jac_col_ptrs,
            jac_class.nnz,
            rootfn,
            num_of_events,
            use_jac,
            ids,
            atol,
            rtol,
        )
        integration_time = timer.time()

        t = sol.t
        number_of_timesteps = t.size
        number_of_states = y0.size
        y_out = sol.y.reshape((number_of_timesteps, number_of_states))

        # return solution, we need to tranpose y to match scipy's interface
        if sol.flag in [0, 2]:
            # 0 = solved for all t_eval
            if sol.flag == 0:
                termination = "final time"
            # 2 = found root(s)
            elif sol.flag == 2:
                termination = "event"

            sol = pybamm.Solution(
                sol.t,
                np.transpose(y_out),
                t[-1],
                np.transpose(y_out[-1])[:, np.newaxis],
                termination,
            )
            sol.integration_time = integration_time
            return sol
        else:
            raise pybamm.SolverError(sol.message)
Esempio n. 28
0
    def _integrate(self, model, t_eval, inputs_dict=None):
        """
        Solve a model defined by dydt with initial conditions y0.

        Parameters
        ----------
        model : :class:`pybamm.BaseModel`
            The model whose solution to calculate.
        t_eval : numeric type
            The times at which to compute the solution
        inputs_dict : dict, optional
            Any input parameters to pass to the model when solving

        """
        inputs_dict = inputs_dict or {}
        if model.convert_to_format == "casadi":
            inputs = casadi.vertcat(*[x for x in inputs_dict.values()])
        else:
            inputs = inputs_dict

        y0 = model.y0
        if isinstance(y0, casadi.DM):
            y0 = y0.full().flatten()

        residuals = model.residuals_eval
        events = model.terminate_events_eval
        jacobian = model.jacobian_eval
        mass_matrix = model.mass_matrix.entries

        def eqsres(t, y, ydot, return_residuals):
            return_residuals[:] = residuals(t, y, ydot, inputs)

        def rootfn(t, y, ydot, return_root):
            return_root[:] = [event(t, y, inputs) for event in events]

        extra_options = {
            **self.extra_options,
            "old_api": False,
            "rtol": self.rtol,
            "atol": self.atol,
        }

        if jacobian:
            jac_y0_t0 = jacobian(t_eval[0], y0, inputs)
            if sparse.issparse(jac_y0_t0):

                def jacfn(t, y, ydot, residuals, cj, J):
                    jac_eval = jacobian(t, y, inputs) - cj * mass_matrix
                    J[:][:] = jac_eval.toarray()

            else:

                def jacfn(t, y, ydot, residuals, cj, J):
                    jac_eval = jacobian(t, y, inputs) - cj * mass_matrix
                    J[:][:] = jac_eval

            extra_options.update({"jacfn": jacfn})

        if events:
            extra_options.update({"rootfn": rootfn, "nr_rootfns": len(events)})

        # solver works with ydot0 set to zero
        ydot0 = np.zeros_like(y0)

        # set up and solve
        dae_solver = scikits_odes.dae(self.method, eqsres, **extra_options)
        timer = pybamm.Timer()
        sol = dae_solver.solve(t_eval, y0, ydot0)
        integration_time = timer.time()

        # return solution, we need to tranpose y to match scipy's interface
        if sol.flag in [0, 2]:
            # 0 = solved for all t_eval
            if sol.flag == 0:
                termination = "final time"
            # 2 = found root(s)
            elif sol.flag == 2:
                termination = "event"
            if sol.roots.t is None:
                t_root = None
            else:
                t_root = sol.roots.t
            sol = pybamm.Solution(
                sol.values.t,
                np.transpose(sol.values.y),
                model,
                inputs_dict,
                t_root,
                np.transpose(sol.roots.y),
                termination,
            )
            sol.integration_time = integration_time
            return sol
        else:
            raise pybamm.SolverError(sol.message)
Esempio n. 29
0
    def _integrate(self, model, t_eval, inputs=None):
        """
        Solve a DAE model defined by residuals with initial conditions y0.

        Parameters
        ----------
        model : :class:`pybamm.BaseModel`
            The model whose solution to calculate.
        t_eval : numeric type
            The times at which to compute the solution
        inputs : dict, optional
            Any external variables or input parameters to pass to the model when solving
        """
        inputs = inputs or {}
        # convert inputs to casadi format
        inputs = casadi.vertcat(*[x for x in inputs.values()])

        if self.mode == "fast":
            integrator = self.get_integrator(model, t_eval, inputs)
            solution = self._run_integrator(integrator, model, model.y0,
                                            inputs, t_eval)
            solution.termination = "final time"
            return solution
        elif not model.events:
            pybamm.logger.info("No events found, running fast mode")
            integrator = self.get_integrator(model, t_eval, inputs)
            solution = self._run_integrator(integrator, model, model.y0,
                                            inputs, t_eval)
            solution.termination = "final time"
            return solution
        elif self.mode == "safe":
            y0 = model.y0
            if isinstance(y0, casadi.DM):
                y0 = y0.full().flatten()
            # Step-and-check
            t = t_eval[0]
            t_f = t_eval[-1]
            init_event_signs = np.sign(
                np.concatenate([
                    event(t, y0, inputs)
                    for event in model.terminate_events_eval
                ]))
            pybamm.logger.info("Start solving {} with {}".format(
                model.name, self.name))

            # Initialize solution
            solution = pybamm.Solution(np.array([t]), y0[:, np.newaxis])
            solution.solve_time = 0

            # Try to integrate in global steps of size dt_max. Note: dt_max must
            # be at least as big as the the biggest step in t_eval (multiplied
            # by some tolerance, here 1.01) to avoid an empty integration window below
            if self.dt_max:
                # Non-dimensionalise provided dt_max
                dt_max = self.dt_max / model.timescale_eval
            else:
                dt_max = 0.01
            dt_eval_max = np.max(np.diff(t_eval)) * 1.01
            dt_max = np.max([dt_max, dt_eval_max])
            while t < t_f:
                # Step
                solved = False
                count = 0
                dt = dt_max
                while not solved:
                    # Get window of time to integrate over (so that we return
                    # all the points in t_eval, not just t and t+dt)
                    t_window = np.concatenate(
                        ([t], t_eval[(t_eval > t) & (t_eval < t + dt)]))
                    # Sometimes near events the solver fails between two time
                    # points in t_eval (i.e. no points t < t_i < t+dt for t_i
                    # in t_eval), so we simply integrate from t to t+dt
                    if len(t_window) == 1:
                        t_window = np.array([t, t + dt])

                    integrator = self.get_integrator(model, t_window, inputs)
                    # Try to solve with the current global step, if it fails then
                    # halve the step size and try again.
                    try:
                        current_step_sol = self._run_integrator(
                            integrator, model, y0, inputs, t_window)
                        solved = True
                    except pybamm.SolverError:
                        dt /= 2
                        # also reduce maximum step size for future global steps
                        dt_max = dt
                    count += 1
                    if count >= self.max_step_decrease_count:
                        raise pybamm.SolverError("""
                            Maximum number of decreased steps occurred at t={}. Try
                            solving the model up to this time only or reducing dt_max.
                            """.format(t))
                # Check most recent y to see if any events have been crossed
                new_event_signs = np.sign(
                    np.concatenate([
                        event(t, current_step_sol.y[:, -1], inputs)
                        for event in model.terminate_events_eval
                    ]))
                # Exit loop if the sign of an event changes
                # Locate the event time using a root finding algorithm and
                # event state using interpolation. The solution is then truncated
                # so that only the times up to the event are returned
                if (new_event_signs != init_event_signs).any():
                    # get the index of the events that have been crossed
                    event_ind = np.where(
                        new_event_signs != init_event_signs)[0]
                    active_events = [
                        model.terminate_events_eval[i] for i in event_ind
                    ]

                    # create interpolant to evaluate y in the current integration
                    # window
                    y_sol = interp1d(current_step_sol.t, current_step_sol.y)

                    # loop over events to compute the time at which they were triggered
                    t_events = [None] * len(active_events)
                    for i, event in enumerate(active_events):

                        def event_fun(t):
                            return event(t, y_sol(t), inputs)

                        if np.isnan(event_fun(current_step_sol.t[-1])[0]):
                            # bracketed search fails if f(a) or f(b) is NaN, so we
                            # need to find the times for which we can evaluate the event
                            times = [
                                t for t in current_step_sol.t
                                if event_fun(t)[0] == event_fun(t)[0]
                            ]
                        else:
                            times = current_step_sol.t
                        # skip if sign hasn't changed
                        if np.sign(event_fun(times[0])) != np.sign(
                                event_fun(times[-1])):
                            t_events[i] = brentq(lambda t: event_fun(t),
                                                 times[0], times[-1])
                        else:
                            t_events[i] = np.nan

                    # t_event is the earliest event triggered
                    t_event = np.nanmin(t_events)
                    y_event = y_sol(t_event)

                    # return truncated solution
                    t_truncated = current_step_sol.t[
                        current_step_sol.t < t_event]
                    y_trunctaed = current_step_sol.y[:, 0:len(t_truncated)]
                    truncated_step_sol = pybamm.Solution(
                        t_truncated, y_trunctaed)
                    # assign temporary solve time
                    truncated_step_sol.solve_time = np.nan
                    # append solution from the current step to solution
                    solution.append(truncated_step_sol)

                    solution.termination = "event"
                    solution.t_event = t_event
                    solution.y_event = y_event
                    break
                else:
                    # assign temporary solve time
                    current_step_sol.solve_time = np.nan
                    # append solution from the current step to solution
                    solution.append(current_step_sol)
                    # update time
                    t = t_window[-1]
                    # update y0
                    y0 = solution.y[:, -1]
            return solution
        elif self.mode == "old safe":
            y0 = model.y0
            if isinstance(y0, casadi.DM):
                y0 = y0.full().flatten()
            # Step-and-check
            t = t_eval[0]
            init_event_signs = np.sign(
                np.concatenate([
                    event(t, y0, inputs)
                    for event in model.terminate_events_eval
                ]))
            pybamm.logger.info("Start solving {} with {}".format(
                model.name, self.name))

            # Initialize solution
            solution = pybamm.Solution(np.array([t]), y0[:, np.newaxis])
            solution.solve_time = 0
            for dt in np.diff(t_eval):
                # Step
                solved = False
                count = 0
                while not solved:
                    integrator = self.get_integrator(model,
                                                     np.array([t, t + dt]),
                                                     inputs)
                    # Try to solve with the current step, if it fails then halve the
                    # step size and try again. This will make solution.t slightly
                    # different to t_eval, but shouldn't matter too much as it should
                    # only happen near events.
                    try:
                        current_step_sol = self._run_integrator(
                            integrator, model, y0, inputs,
                            np.array([t, t + dt]))
                        solved = True
                    except pybamm.SolverError:
                        dt /= 2
                    count += 1
                    if count >= self.max_step_decrease_count:
                        raise pybamm.SolverError("""
                            Maximum number of decreased steps occurred at t={}. Try
                            solving the model up to this time only.
                            """.format(t))
                # Check most recent y
                new_event_signs = np.sign(
                    np.concatenate([
                        event(t, current_step_sol.y[:, -1], inputs)
                        for event in model.terminate_events_eval
                    ]))
                # Exit loop if the sign of an event changes
                if (new_event_signs != init_event_signs).any():
                    solution.termination = "event"
                    solution.t_event = solution.t[-1]
                    solution.y_event = solution.y[:, -1]
                    break
                else:
                    # assign temporary solve time
                    current_step_sol.solve_time = np.nan
                    # append solution from the current step to solution
                    solution.append(current_step_sol)
                    # update time
                    t += dt
                    # update y0
                    y0 = solution.y[:, -1]
            return solution
Esempio n. 30
0
    def get_termination_reason(self, solution, events):
        """
        Identify the cause for termination. In particular, if the solver terminated
        due to an event, (try to) pinpoint which event was responsible. If an event
        occurs the event time and state are added to the solution object.
        Note that the current approach (evaluating all the events and then finding which
        one is smallest at the final timestep) is pretty crude, but is the easiest one
        that works for all the different solvers.

        Parameters
        ----------
        solution : :class:`pybamm.Solution`
            The solution object
        events : dict
            Dictionary of events
        """
        if solution.termination == "final time":
            return (
                solution,
                "the solver successfully reached the end of the integration interval",
            )
        elif solution.termination == "event":
            pybamm.logger.debug("Start post-processing events")
            # Get final event value
            final_event_values = {}

            for event in events:
                if event.event_type == pybamm.EventType.TERMINATION:
                    final_event_values[event.name] = abs(
                        event.expression.evaluate(
                            solution.t_event,
                            solution.y_event,
                            inputs=solution.all_inputs[-1],
                        )
                    )
            termination_event = min(final_event_values, key=final_event_values.get)

            # Check that it's actually an event
            if abs(final_event_values[termination_event]) > 0.1:  # pragma: no cover
                # Hard to test this
                raise pybamm.SolverError(
                    "Could not determine which event was triggered "
                    "(possibly due to NaNs)"
                )
            # Add the event to the solution object
            solution.termination = "event: {}".format(termination_event)
            # Update t, y and inputs to include event time and state
            # Note: if the final entry of t is equal to the event time we skip
            # this (having duplicate entries causes an error later in ProcessedVariable)
            if solution.t_event != solution.all_ts[-1][-1]:
                event_sol = pybamm.Solution(
                    solution.t_event,
                    solution.y_event,
                    solution.all_models[-1],
                    solution.all_inputs[-1],
                    solution.t_event,
                    solution.y_event,
                    solution.termination,
                )
                event_sol.solve_time = 0
                event_sol.integration_time = 0
                solution = solution + event_sol

            pybamm.logger.debug("Finish post-processing events")
            return solution, solution.termination
        elif solution.termination == "success":
            return solution, solution.termination