Beispiel #1
0
    def test_timer_format(self):
        t = pybamm.Timer()
        self.assertEqual(t.format(1e-9), "1.000 ns")
        self.assertEqual(t.format(0.000000123456789), "123.457 ns")
        self.assertEqual(t.format(1e-6), "1.000 us")
        self.assertEqual(t.format(0.000123456789), "123.457 us")
        self.assertEqual(t.format(0.999e-3), "999.000 us")
        self.assertEqual(t.format(1e-3), "1.000 ms")
        self.assertEqual(t.format(0.123456789), "123.457 ms")
        self.assertEqual(t.format(2), "2.000 s")
        self.assertEqual(t.format(2.5), "2.500 s")
        self.assertEqual(t.format(12.5), "12.500 s")
        self.assertEqual(t.format(59.41), "59.410 s")
        self.assertEqual(t.format(59.4126347547), "59.413 s")
        self.assertEqual(t.format(60.2), "1 minute, 0 seconds")
        self.assertEqual(t.format(61), "1 minute, 1 second")
        self.assertEqual(t.format(121), "2 minutes, 1 second")
        self.assertEqual(
            t.format(604800), "1 week, 0 days, 0 hours, 0 minutes, 0 seconds"
        )
        self.assertEqual(
            t.format(2 * 604800 + 3 * 3600 + 60 + 4),
            "2 weeks, 0 days, 3 hours, 1 minute, 4 seconds",
        )

        # Test without argument
        self.assertIsInstance(t.format(), str)
Beispiel #2
0
    def compute_solution(self, model, t_eval):
        """Calculate the solution of the model at specified times. In this class, we
        overwrite the behaviour of :class:`pybamm.DaeSolver`, since CasADi requires
        slightly different syntax.

        Parameters
        ----------
        model : :class:`pybamm.BaseModel`
            The model whose solution to calculate. Must have attributes rhs and
            initial_conditions
        t_eval : numeric type
            The times at which to compute the solution

        """
        timer = pybamm.Timer()

        solve_start_time = timer.time()
        pybamm.logger.info("Calling DAE solver")
        solution = self.integrate_casadi(
            self.casadi_problem, self.y0, t_eval, mass_matrix=model.mass_matrix.entries
        )
        solve_time = timer.time() - solve_start_time

        # Events not implemented, termination is always 'final time'
        termination = "final time"

        return solution, solve_time, termination
Beispiel #3
0
    def compute_solution(self, model, t_eval):
        """Calculate the solution of the model at specified times.

        Parameters
        ----------
        model : :class:`pybamm.BaseModel`
            The model whose solution to calculate. Must have attributes rhs and
            initial_conditions
        t_eval : numeric type
            The times at which to compute the solution

        """
        timer = pybamm.Timer()

        solve_start_time = timer.time()
        pybamm.logger.info("Calling DAE solver")
        solution = self.integrate(
            self.residuals,
            self.y0,
            t_eval,
            events=self.event_funs,
            mass_matrix=model.mass_matrix.entries,
            jacobian=self.jacobian,
        )
        solve_time = timer.time() - solve_start_time

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

        return solution, solve_time, termination
Beispiel #4
0
    def test_timer_format(self):
        import sys

        t = pybamm.Timer()
        self.assertEqual(t.format(1e-3), "0.001 seconds")
        self.assertEqual(t.format(0.000123456789), "0.000123456789 seconds")
        self.assertEqual(t.format(0.123456789), "0.12 seconds")
        if sys.hexversion < 0x3000000:
            self.assertEqual(t.format(2), "2.0 seconds")
        else:
            self.assertEqual(t.format(2), "2 seconds")
        self.assertEqual(t.format(2.5), "2.5 seconds")
        self.assertEqual(t.format(12.5), "12.5 seconds")
        self.assertEqual(t.format(59.41), "59.41 seconds")
        self.assertEqual(t.format(59.4126347547), "59.41 seconds")
        self.assertEqual(t.format(60.2), "1 minute, 0 seconds")
        self.assertEqual(t.format(61), "1 minute, 1 second")
        self.assertEqual(t.format(121), "2 minutes, 1 second")
        self.assertEqual(t.format(604800),
                         "1 week, 0 days, 0 hours, 0 minutes, 0 seconds")
        self.assertEqual(
            t.format(2 * 604800 + 3 * 3600 + 60 + 4),
            "2 weeks, 0 days, 3 hours, 1 minute, 4 seconds",
        )

        # Test without argument
        self.assertIsInstance(t.format(), str)
    def solve(self, model):
        """Calculate the solution of the model.

        Parameters
        ----------
        model : :class:`pybamm.BaseModel`
            The model whose solution to calculate. Must only contain algebraic
            equations.

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

        # Set up
        timer = pybamm.Timer()
        start_time = timer.time()
        concatenated_algebraic, jac = self.set_up(model)
        set_up_time = timer.time() - start_time

        # Create function to evaluate algebraic
        def algebraic(y):
            return concatenated_algebraic.evaluate(0, y, known_evals={})[0][:,
                                                                            0]

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

            def jacobian(y):
                # Note: we only use this solver for time independent algebraic
                # systems, so jac is arbitrarily evaluated at t=0. Also, needs
                # to be converted from sparse to dense, so in very large
                # algebraic models it may be best to switch use_jacobian to False
                # by default.
                return jac.evaluate(0, y, known_evals={})[0].toarray()

        else:
            jacobian = None

        # Use "initial conditions" set in model as initial guess
        y0_guess = model.concatenated_initial_conditions.evaluate(t=0)

        # Solve
        solve_start_time = timer.time()
        pybamm.logger.info("Calling root finding algorithm")
        solution = self.root(algebraic, y0_guess, jacobian=jacobian)
        solution.model = model

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

        pybamm.logger.info("Finish solving {}".format(model.name))
        pybamm.logger.info(
            "Set-up time: {}, Solve time: {}, Total time: {}".format(
                timer.format(solution.set_up_time),
                timer.format(solution.solve_time),
                timer.format(solution.total_time),
            ))
        return solution
Beispiel #6
0
    def test_append(self):
        model = pybamm.lithium_ion.SPMe()
        # create geometry
        geometry = model.default_geometry

        # load parameter values and process model and geometry
        param = model.default_parameter_values
        param.process_model(model)
        param.process_geometry(geometry)

        # set mesh
        mesh = pybamm.Mesh(geometry, model.default_submesh_types,
                           model.default_var_pts)

        # discretise model
        disc = pybamm.Discretisation(mesh, model.default_spatial_methods)
        disc.process_model(model)

        # solve model
        t_eval = np.linspace(0, 3600, 100)
        solver = model.default_solver
        solution = solver.solve(model, t_eval)

        # step model
        old_t = 0
        step_solver = model.default_solver
        step_solution = None
        # dt should be dimensional
        solution_times_dimensional = solution.t * model.timescale_eval
        for t in solution_times_dimensional[1:]:
            dt = t - old_t
            step_solution = step_solver.step(step_solution,
                                             model,
                                             dt=dt,
                                             npts=10)
            if t == solution_times_dimensional[1]:
                # Create voltage variable
                step_solution.update("Terminal voltage")
            old_t = t

        # Step solution should have been updated as we go along so be quicker to
        # calculate
        timer = pybamm.Timer()
        step_solution.update("Terminal voltage")
        step_sol_time = timer.time()
        timer.reset()
        solution.update("Terminal voltage")
        sol_time = timer.time()
        self.assertLess(step_sol_time, sol_time)
        # Check both give the same answer
        np.testing.assert_array_almost_equal(
            solution["Terminal voltage"](solution.t[:-1] *
                                         model.timescale_eval),
            step_solution["Terminal voltage"](solution.t[:-1] *
                                              model.timescale_eval),
            decimal=4,
        )
Beispiel #7
0
def test_notebook(path, executable="python"):
    """
    Tests a single notebook, exists if it doesn't finish.
    """
    import nbconvert
    import pybamm

    b = pybamm.Timer()
    print("Test " + path + " ... ", end="")
    sys.stdout.flush()

    # Load notebook, convert to python
    e = nbconvert.exporters.PythonExporter()
    code, __ = e.from_filename(path)

    # Remove coding statement, if present
    code = "\n".join([x for x in code.splitlines() if x[:9] != "# coding"])

    # Tell matplotlib not to produce any figures
    env = dict(os.environ)
    env["MPLBACKEND"] = "Template"

    # If notebook makes use of magic commands then
    # the script must be ran using ipython
    # https://github.com/jupyter/nbconvert/issues/503#issuecomment-269527834
    executable = "ipython" if ("run_cell_magic(" in code) else executable

    # Run in subprocess
    cmd = [executable] + ["-c", code]
    try:
        p = subprocess.Popen(cmd,
                             stdout=subprocess.PIPE,
                             stderr=subprocess.PIPE,
                             env=env)
        stdout, stderr = p.communicate()
        # TODO: Use p.communicate(timeout=3600) if Python3 only
        if p.returncode != 0:
            # Show failing code, output and errors before returning
            print("ERROR")
            print("-- script " + "-" * (79 - 10))
            for i, line in enumerate(code.splitlines()):
                j = str(1 + i)
                print(j + " " * (5 - len(j)) + line)
            print("-- stdout " + "-" * (79 - 10))
            print(str(stdout, "utf-8"))
            print("-- stderr " + "-" * (79 - 10))
            print(str(stderr, "utf-8"))
            print("-" * 79)
            return False
    except KeyboardInterrupt:
        p.terminate()
        print("ABORTED")
        sys.exit(1)

    # Sucessfully run
    print("ok (" + b.format() + ")")
    return True
Beispiel #8
0
 def test_timing(self):
     t = pybamm.Timer()
     a = t.time().value
     self.assertGreaterEqual(a, 0)
     for _ in range(100):
         self.assertGreater(t.time().value, a)
     a = t.time().value
     t.reset()
     b = t.time().value
     self.assertGreaterEqual(b, 0)
     self.assertLess(b, a)
Beispiel #9
0
def pybamm2():
    # Writing lead-acid model example

    full = pybamm.lead_acid.Full()

    loqs = pybamm.lead_acid.LOQS()

    composite = pybamm.lead_acid.Composite()

    # load models
    models = [loqs, composite, full]

    # process parameters
    param = models[0].default_parameter_values
    param["Current function [A]"] = "[input]"
    for model in models:
        param.process_model(model)

    for model in models:
        # load and process default geometry
        geometry = model.default_geometry
        param.process_geometry(geometry)

        # discretise using default settings
        mesh = pybamm.Mesh(geometry, model.default_submesh_types,
                           model.default_var_pts)
        disc = pybamm.Discretisation(mesh, model.default_spatial_methods)
        disc.process_model(model)

    timer = pybamm.Timer()
    solutions = {}
    t_eval = np.linspace(0, 3600 * 17, 100)  # time in seconds
    solver = pybamm.CasadiSolver()
    for model in models:
        timer.reset()
        solution = solver.solve(model,
                                t_eval,
                                inputs={"Current function [A]": 1})
        print("Solved the {} in {}".format(model.name, timer.time()))
        solutions[model] = solution

    for model in models:
        time = solutions[model]["Time [h]"].entries
        voltage = solutions[model]["Terminal voltage [V]"].entries
        plt.plot(time, voltage, lw=2, label=model.name)
    plt.xlabel("Time [h]", fontsize=15)
    plt.ylabel("Terminal voltage [V]", fontsize=15)
    plt.legend(fontsize=15)

    # plt.show()
    plt.savefig('foo1.png')
Beispiel #10
0
    def solve(self, model, t_eval):
        """
        Execute the solver setup and calculate the solution of the model at
        specified times.

        Parameters
        ----------
        model : :class:`pybamm.BaseModel`
            The model whose solution to calculate. Must have attributes rhs and
            initial_conditions
        t_eval : numeric type
            The times at which to compute the solution

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

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

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

        # Set up
        timer = pybamm.Timer()
        start_time = timer.time()
        if model.convert_to_format == "casadi" or isinstance(self, pybamm.CasadiSolver):
            self.set_up_casadi(model)
        else:
            self.set_up(model)
        set_up_time = timer.time() - start_time

        # Solve
        solution, solve_time, termination = self.compute_solution(model, t_eval)

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

        pybamm.logger.info("Finish solving {} ({})".format(model.name, termination))
        pybamm.logger.info(
            "Set-up time: {}, Solve time: {}, Total time: {}".format(
                timer.format(solution.set_up_time),
                timer.format(solution.solve_time),
                timer.format(solution.total_time),
            )
        )
        return solution
def test_script(path, executable="python"):
    """
    Tests a single notebook, exists if it doesn't finish.
    """
    import pybamm

    b = pybamm.Timer()
    print("Test " + path + " ... ", end="")
    sys.stdout.flush()

    # Tell matplotlib not to produce any figures
    env = dict(os.environ)
    env["MPLBACKEND"] = "Template"

    # Run in subprocess
    cmd = [executable] + [path]
    try:
        p = subprocess.Popen(cmd,
                             stdout=subprocess.PIPE,
                             stderr=subprocess.PIPE,
                             env=env)
        stdout, stderr = p.communicate()
        # TODO: Use p.communicate(timeout=3600) if Python3 only
        if p.returncode != 0:
            # Show failing code, output and errors before returning
            print("ERROR")
            print("-- stdout " + "-" * (79 - 10))
            print(str(stdout, "utf-8"))
            print("-- stderr " + "-" * (79 - 10))
            print(str(stderr, "utf-8"))
            print("-" * 79)
            return False
    except KeyboardInterrupt:
        p.terminate()
        print("ABORTED")
        sys.exit(1)

    # Sucessfully run
    print("ok (" + b.format() + ")")
    return True
Beispiel #12
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 : :class:`numpy.array`, size (k,)
            The times at which to compute the solution
        inputs_dict : dict, optional
            Any input parameters to pass to the model when solving

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

        """
        timer = pybamm.Timer()
        if model not in self._cached_solves:
            self._cached_solves[model] = self.create_solve(model, t_eval)

        y = self._cached_solves[model](inputs_dict).block_until_ready()
        integration_time = timer.time()

        # convert to a normal numpy array
        y = onp.array(y)

        termination = "final time"
        t_event = None
        y_event = onp.array(None)
        sol = pybamm.Solution(t_eval, y, model, inputs_dict, t_event, y_event,
                              termination)
        sol.integration_time = integration_time
        return sol
Beispiel #13
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 at which to compute the solution. If None and the parameter
            "Current function [A]" is not read from data the model will
            be solved for a full discharge (1 hour / C_rate). If None and the
            parameter "Current function [A]" is read from data the model will be
            solved at the times provided in the data.
        solver : :class:`pybamm.BaseSolver`
            The solver to use to solve the model.
        external_variables : dict
            A dictionary of external variables and their corresponding
            values at the current time. The variables must correspond to
            the variables that would normally be found by solving the
            submodels that have been made external.
        inputs : dict, optional
            Any input parameters to pass to the model when solving
        check_model : bool, optional
            If True, model checks are performed after discretisation (see
            :meth:`pybamm.Discretisation.process_model`). Default is True.
        """
        # Setup
        self.build(check_model=check_model)
        if solver is None:
            solver = self.solver

        if self.operating_mode == "without experiment":
            # For drive cycles (current provided as data) we perform additional tests
            # on t_eval (if provided) to ensure the returned solution captures the
            # input. If the current is provided as data then the "Current function [A]"
            # is the tuple (filename, data).
            # First, read the current function (if provided, otherwise return None)
            current = self._parameter_values.get("Current function [A]")
            if isinstance(current, tuple):
                filename = self._parameter_values["Current function [A]"][0]
                time_data = self._parameter_values["Current function [A]"][1][:, 0]
                # If no t_eval is provided, we use the times provided in the data.
                if t_eval is None:
                    pybamm.logger.info(
                        "Setting t_eval as specified by the data '{}'".format(filename)
                    )
                    t_eval = time_data
                # If t_eval is provided we first check if it contains all of the times
                # in the data to within 10-12. If it doesn't, we then check
                # that the largest gap in t_eval is smaller than the smallest gap in the
                # time data (to ensure the resolution of t_eval is fine enough).
                # We only raise a warning here as users may genuinely only want
                # the solution returned at some specified points.
                elif (
                    set(np.round(time_data, 12)).issubset(set(np.round(t_eval, 12)))
                ) is False:
                    warnings.warn(
                        """
                        t_eval does not contain all of the time points in the data
                        '{}'. Note: passing t_eval = None automatically sets t_eval
                        to be the points in the data.
                        """.format(
                            filename
                        ),
                        pybamm.SolverWarning,
                    )
                    dt_data_min = np.min(np.diff(time_data))
                    dt_eval_max = np.max(np.diff(t_eval))
                    if dt_eval_max > dt_data_min + sys.float_info.epsilon:
                        warnings.warn(
                            """
                            The largest timestep in t_eval ({}) is larger than
                            the smallest timestep in the data ({}). The returned
                            solution may not have the correct resolution to accurately
                            capture the input. Try refining t_eval. Alternatively,
                            passing t_eval = None automatically sets t_eval to be the
                            points in the data.
                            """.format(
                                dt_eval_max, dt_data_min
                            ),
                            pybamm.SolverWarning,
                        )
            # If not using a drive cycle and t_eval is not provided, set t_eval
            # to correspond to a single discharge
            elif t_eval is None:
                if current is None:
                    t_end = 1
                else:
                    # Get C-rate, return None if it doesn't exist
                    capacity = self.parameter_values["Cell capacity [A.h]"]
                    if isinstance(current, pybamm.InputParameter):
                        C_rate = inputs["Current function [A]"] / capacity
                        t_end = 3600 / C_rate
                    else:
                        try:
                            C_rate = current / capacity
                            t_end = 3600 / C_rate
                        except TypeError:
                            t_end = 3600
                t_eval = np.linspace(0, t_end, 100)

            self.t_eval = t_eval
            self._solution = solver.solve(
                self.built_model,
                t_eval,
                external_variables=external_variables,
                inputs=inputs,
            )

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

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

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

        algebraic = model.algebraic_eval

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

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

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

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

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

                else:

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

            else:
                jac_fn = None

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

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

                        if jac_fn is None:
                            jac_norm = None
                        else:

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

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

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

        # Concatenate differential part
        y_diff = np.r_[[y0_diff] * len(t_eval)].T
        y_sol = np.r_[y_diff, y_alg]
        # Return solution object (no events, so pass None to t_event, y_event)
        sol = pybamm.Solution(t_eval,
                              y_sol,
                              model,
                              inputs_dict,
                              termination="success")
        sol.integration_time = integration_time
        return sol
Beispiel #15
0
    #     rht = RHT(T_inf=T_inf, savesol=True, plot=False)
    #     rht.direct_solve()

    # Try using scipy.minimize
    npoints = 129
    rht = RHT(T_inf=10,
              savesol=False,
              plot=False,
              lambda_reg=1,
              npoints=npoints)
    x = np.linspace(0, 1, 129)
    rht.beta = 1 + 0.1 * np.sin(x)
    rht.direct_solve()
    data = rht.direct_solve()

    def objective(x):
        rht.beta = x
        return rht.getObj(data)

    def jac(x):
        rht.beta = x
        return rht.adjoint_solve(data)

    timer = pybamm.Timer()
    sol = minimize(objective, [1.1] * 129)
    print("Without jac: ", timer.time())
    timer.reset()
    sol = minimize(objective, [1.1] * 129, jac=jac)
    print("With jac: ", timer.time())
    # print(sol)
Beispiel #16
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 = {
            **self.extra_options, "rtol": self.rtol,
            "atol": self.atol
        }

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

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

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

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

                event_fn.terminal = True
                return event_fn

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

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

        if sol.success:
            # Set the reason for termination
            if sol.message == "A termination event occurred.":
                termination = "event"
                t_event = []
                for time in sol.t_events:
                    if time.size > 0:
                        t_event = np.append(t_event, np.max(time))
                t_event = np.array([np.max(t_event)])
                y_event = sol.sol(t_event)
            elif sol.message.startswith(
                    "The solver successfully reached the end"):
                termination = "final time"
                t_event = None
                y_event = np.array(None)
            sol = pybamm.Solution(sol.t, sol.y, t_event, y_event, termination)
            sol.integration_time = integration_time
            return sol
        else:
            raise pybamm.SolverError(sol.message)
Beispiel #17
0
    def step(self, model, dt, npts=2):
        """
        Step the solution of the model forward by a given time increment. The
        first time this method is called it executes the necessary setup by
        calling `self.set_up(model)`.

        Parameters
        ----------
        model : :class:`pybamm.BaseModel`
            The model whose solution to calculate. Must have attributes rhs and
            initial_conditions
        dt : numeric type
            The timestep over which to step the solution
        npts : int, optional
            The number of points at which the solution will be returned during
            the step dt. default is 2 (returns the solution at t0 and t0 + dt).

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

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

        # Set timer
        timer = pybamm.Timer()

        # Run set up on first step
        if not hasattr(self, "y0"):
            start_time = timer.time()
            self.set_up(model)
            self.t = 0.0
            set_up_time = timer.time() - start_time
        else:
            set_up_time = None

        # Step
        pybamm.logger.info("Start stepping {}".format(model.name))
        t_eval = np.linspace(self.t, self.t + dt, npts)
        solution, solve_time, termination = self.compute_solution(model, t_eval)

        # Assign times
        solution.solve_time = solve_time
        if set_up_time:
            solution.total_time = timer.time() - start_time
            solution.set_up_time = set_up_time

        # Set self.t and self.y0 to their values at the final step
        self.t = solution.t[-1]
        self.y0 = solution.y[:, -1]

        pybamm.logger.info("Finish stepping {} ({})".format(model.name, termination))
        if set_up_time:
            pybamm.logger.info(
                "Set-up time: {}, Step time: {}, Total time: {}".format(
                    timer.format(solution.set_up_time),
                    timer.format(solution.solve_time),
                    timer.format(solution.total_time),
                )
            )
        else:
            pybamm.logger.info(
                "Step time: {}".format(timer.format(solution.solve_time))
            )
        return solution
Beispiel #18
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])
Beispiel #19
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)
Beispiel #20
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
Beispiel #21
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
Beispiel #22
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 over which to step the solution
        npts : int, optional
            The number of points at which the solution will be returned during
            the step dt. default is 2 (returns the solution at t0 and t0 + dt).
        external_variables : dict
            A dictionary of external variables and their corresponding
            values at the current time
        inputs : dict, optional
            Any input parameters to pass to the model when solving
        save : bool
            Turn on to store the solution of all previous timesteps

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

        """

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

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

        # Set timer
        timer = pybamm.Timer()

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

        # Run set up on first step
        if old_solution is None:
            pybamm.logger.info("Start stepping {} with {}".format(
                model.name, self.name))
            self.set_up(model, ext_and_inputs)
            t = 0.0
            set_up_time = timer.time()
        else:
            # initialize with old solution
            t = old_solution.t[-1]
            model.y0 = old_solution.y[:, -1]
            set_up_time = 0

        # Non-dimensionalise dt
        dt_dimensionless = dt / model.timescale_eval
        # Step
        t_eval = np.linspace(t, t + dt_dimensionless, npts)
        # Set inputs and external
        self.set_inputs(model, ext_and_inputs)

        pybamm.logger.info("Calling solver")
        timer.reset()
        solution = self._integrate(model, t_eval, ext_and_inputs)

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

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

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

        pybamm.logger.debug("Finish stepping {} ({})".format(
            model.name, termination))
        if set_up_time:
            pybamm.logger.debug(
                "Set-up time: {}, Step time: {}, Total time: {}".format(
                    timer.format(solution.set_up_time),
                    timer.format(solution.solve_time),
                    timer.format(solution.total_time),
                ))
        else:
            pybamm.logger.debug("Step time: {}".format(
                timer.format(solution.solve_time)))
        if save is False or old_solution is None:
            return solution
        else:
            return old_solution + solution
Beispiel #23
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
Beispiel #24
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)
Beispiel #25
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 : dict, optional
            Any input parameters to pass to the model when solving
        save : bool
            Turn on to store the solution of all previous timesteps

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

        """

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

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

        # Make sure dt is positive
        if dt <= 0:
            raise pybamm.SolverError("Step time must be positive")

        # Set timer
        timer = pybamm.Timer()

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

        # Check that any inputs that may affect the scaling have not changed
        # Set model timescale
        temp_timescale_eval = model.timescale.evaluate(inputs=inputs)
        # Set model lengthscales
        temp_length_scales_eval = {
            domain: scale.evaluate(inputs=inputs)
            for domain, scale in model.length_scales.items()
        }
        if old_solution is not None:
            if temp_timescale_eval != old_solution.timescale_eval:
                raise pybamm.SolverError(
                    "The model timescale is a function of an input parameter "
                    "and the value has changed between steps!"
                )
            for domain in temp_length_scales_eval.keys():
                old_dom_eval = old_solution.length_scales_eval[domain]
                if temp_length_scales_eval[domain] != old_dom_eval:
                    pybamm.logger.error(
                        "The {} domain lengthscale is a function of an input "
                        "parameter and the value has changed between "
                        "steps!".format(domain)
                    )

        if old_solution is None:
            # Run set up on first step
            pybamm.logger.verbose(
                "Start stepping {} with {}".format(model.name, self.name)
            )
            self.set_up(model, ext_and_inputs)
            self.models_set_up.update(
                {model: {"initial conditions": model.concatenated_initial_conditions}}
            )
            t = 0.0
        elif model not in self.models_set_up:
            # Run set up if the model has changed
            self.set_up(model, ext_and_inputs)
            self.models_set_up.update(
                {model: {"initial conditions": model.concatenated_initial_conditions}}
            )

        if old_solution is not None:
            t = old_solution.all_ts[-1][-1]
            if old_solution.all_models[-1] == model:
                # initialize with old solution
                model.y0 = old_solution.all_ys[-1][:, -1]
            else:
                model.y0 = (
                    model.set_initial_conditions_from(old_solution)
                    .concatenated_initial_conditions.evaluate(0, inputs=ext_and_inputs)
                    .flatten()
                )
        set_up_time = timer.time()

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

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

        # Step
        t_eval = np.linspace(t, t + dt_dimensionless, npts)
        pybamm.logger.verbose(
            "Stepping for {:.0f} < t < {:.0f}".format(
                t * model.timescale_eval,
                (t + dt_dimensionless) * model.timescale_eval,
            )
        )
        timer.reset()
        solution = self._integrate(model, t_eval, ext_and_inputs)
        solution.solve_time = timer.time()

        # Check if extrapolation occurred
        extrapolation = self.check_extrapolation(solution, model.events)
        if extrapolation:
            warnings.warn(
                "While solving {} extrapolation occurred for {}".format(
                    model.name, extrapolation
                ),
                pybamm.SolverWarning,
            )

        # Identify the event that caused termination and update the solution to
        # include the event time and state
        solution, termination = self.get_termination_reason(solution, model.events)

        # Assign setup time
        solution.set_up_time = set_up_time

        # Report times
        pybamm.logger.verbose("Finish stepping {} ({})".format(model.name, termination))
        pybamm.logger.verbose(
            (
                "Set-up time: {}, Step time: {} (of which integration time: {}), "
                "Total time: {}"
            ).format(
                solution.set_up_time,
                solution.solve_time,
                solution.integration_time,
                solution.total_time,
            )
        )

        # Return solution
        if save is False:
            return solution
        else:
            return old_solution + solution
Beispiel #26
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_dict
                                )
                                < self.extrap_tol
                            )
                        ):
                            extrap_event_names.append(event.name[12:])

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

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

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

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

        # Concatenate differential part
        y_diff = casadi.horzcat(*[y0_diff] * len(t_eval))
        y_sol = casadi.vertcat(y_diff, y_alg)
        # Return solution object (no events, so pass None to t_event, y_event)
        sol = pybamm.Solution(
            [t_eval], y_sol, model, inputs_dict, termination="success"
        )
        sol.integration_time = integration_time
        return sol
Beispiel #27
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
Beispiel #28
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
    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)