Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

Using Input Parameters to efficiently re-run simulations with different parameters#

The previous notebook described the PyBaMM pipeline and how you can use the pybamm.Simulation class to efficiently run simulations. Recall the pipeline was:

  1. Parameter replacement

  2. Discretisation

  3. Solver setup

  4. Solver solve

  5. Post-processing

An obvious question is how can we efficiently re-run simulations with different parameters (step 1 in the pipeline)? Ideally we’d like to do this without having to re-build the model and discretisation each time, which as we’ve seen is a costly process.

[1]:
%pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed
import pybamm
import numpy as np
Note: you may need to restart the kernel to use updated packages.

Using Input Parameters#

The answer is to use input parameters and the `pybamm.InputParameter <https://docs.pybamm.org/en/stable/source/api/expression_tree/input_parameter.html>`__ class. Input parameters are a special type of parameter that is not replaced by a value during step 1 of the pipeline, and can be used to replace many of the parameters in the model (as we’ll see later this does not apply to some geometric parameters).

If you use the pybamm.ParameterVariables class to set the parameters of your model, you can set any parameter in the model to be an input parameter by updating its parameter value to be the string [input]. For example, to set the current in the SPM model to be an input parameter, you can do the following:

[2]:
model = pybamm.lithium_ion.SPM()
parameter_values = model.default_parameter_values
parameter_values["Current function [A]"] = "[input]"

If you are building up a model from scratch, you can also use pybamm.InputParameter directly to create an input parameter. For example, the code below creates an exponential decay model with a decay constant as an input parameter:

[3]:
model = pybamm.BaseModel()
k = pybamm.InputParameter("k")
x = pybamm.Variable("x")
model.rhs = {x: -k * x}

Example#

Let’s see how we can use input parameters to efficiently re-run simulations with different parameters. We’ll start with a script that loops over the current to solve the model, and then show how we can use input parameters to do this more efficiently.

[4]:
import time

average_time = 0
n = 9
model = pybamm.lithium_ion.SPM()
solver = pybamm.IDAKLUSolver()
params = model.default_parameter_values
for current in np.linspace(-1.1, 1.0, n):
    time_start = time.perf_counter()
    params["Current function [A]"] = current
    sim = pybamm.Simulation(model, solver=solver, parameter_values=params)
    sol = sim.solve([0, 3600])
    t_evals = np.linspace(0, 3600, 100)
    voltage = sol["Terminal voltage [V]"](t_evals)
    time_end = time.perf_counter()
    average_time += time_end - time_start
print(f"Average time taken: {average_time / n:.3f} seconds")
Average time taken: 0.193 seconds

The most important thing to note about this script is that we are creating the simulation object on every iteration of this loop. This means we are running though all the steps of the pipeline, rebuilding the model and performing the discretisations, at each iteration of the loop. We do this even though most of the structure of the model, and in particular the numerical discretisations of the spatial gradients, is unchanged. As we’ve seen previously, the discretisation step is often the most expensive part of the pipeline, so we’d like to avoid repeating it if possible.

Now let’s see how we will use input parameters to do this more efficiently. To do this we will move the simulation object creation outside of the loop, and use an input parameter for the current instead.

[5]:
average_time = 0
n = 10
model = pybamm.lithium_ion.SPM()
solver = pybamm.IDAKLUSolver()
params = model.default_parameter_values
params["Current function [A]"] = "[input]"
sim = pybamm.Simulation(model, solver=solver, parameter_values=params)
for current in np.linspace(0.1, 1.0, n):
    time_start = time.perf_counter()
    sol = sim.solve([0, 3600], inputs={"Current function [A]": current})
    t_evals = np.linspace(0, 3600, 100)
    voltage = sol["Terminal voltage [V]"](t_evals)
    time_end = time.perf_counter()
    average_time += time_end - time_start
print(f"Average time taken: {average_time / n:.3f} seconds")
Average time taken: 0.033 seconds

You can see that we’ve improved the performance of our loop significantly by moving the creation of the simulation object outside of the loop, thus avoiding the need to rebuild the model from scratch at each iteration.

Limitations#

Geometric parameters cannot be input parameters#

Input parameters cannot be used for parameters that are used during the descretisation step. These are generally parameters affecting the geometry, such as the electrode or separator thicknesses. If you try to use an input parameter for a parameter that affects the discretisation, you will get an error, most likely during the discretisation step. For example, these are the parameters that fail for the SPM model, along with the error messages:

[6]:
import traceback

model = pybamm.lithium_ion.SPM()
model_params = model.get_parameter_info()
for param in model_params:
    if model_params[param][1] == "Parameter":
        params = model.default_parameter_values
        original_param = params[param]
        params[param] = "[input]"
        sim = pybamm.Simulation(model, parameter_values=params)
        try:
            sim.solve([0, 3600], inputs={param: original_param})
        except Exception as e:
            print(f"Failed for parameter {param}. Error was {e}")
            tb = traceback.format_exc()
            print(tb)
Failed for parameter Separator thickness [m]. Error was Cannot interpret 'Addition(-0x1e5235efb4a04db1, +, children=['0.0001', 'Separator thickness [m]'], domains={})' as a data type
Traceback (most recent call last):
  File "/tmp/ipykernel_1968898/3514874421.py", line 12, in <module>
    sim.solve([0, 3600], inputs={param: original_param})
  File "/home/mrobins/git/PyBaMM/src/pybamm/simulation.py", line 472, in solve
    self.build(initial_soc=initial_soc, inputs=inputs)
  File "/home/mrobins/git/PyBaMM/src/pybamm/simulation.py", line 328, in build
    self._mesh = pybamm.Mesh(self._geometry, self._submesh_types, self._var_pts)
  File "/home/mrobins/git/PyBaMM/src/pybamm/meshes/meshes.py", line 117, in __init__
    self[domain] = submesh_types[domain](geometry[domain], submesh_pts[domain])
  File "/home/mrobins/git/PyBaMM/src/pybamm/meshes/meshes.py", line 301, in __call__
    return self.submesh_type(lims, npts, **self.submesh_params)
  File "/home/mrobins/git/PyBaMM/src/pybamm/meshes/one_dimensional_submeshes.py", line 130, in __init__
    edges = np.linspace(spatial_lims["min"], spatial_lims["max"], npts + 1)
  File "/home/mrobins/git/PyBaMM/env/lib/python3.10/site-packages/numpy/core/function_base.py", line 132, in linspace
    dt = result_type(start, stop, float(num))
TypeError: Cannot interpret 'Addition(-0x1e5235efb4a04db1, +, children=['0.0001', 'Separator thickness [m]'], domains={})' as a data type

Failed for parameter Negative electrode thickness [m]. Error was Cannot interpret 'InputParameter(0x6c630c7d1f75ef82, Negative electrode thickness [m], children=[], domains={})' as a data type
Traceback (most recent call last):
  File "/tmp/ipykernel_1968898/3514874421.py", line 12, in <module>
    sim.solve([0, 3600], inputs={param: original_param})
  File "/home/mrobins/git/PyBaMM/src/pybamm/simulation.py", line 472, in solve
    self.build(initial_soc=initial_soc, inputs=inputs)
  File "/home/mrobins/git/PyBaMM/src/pybamm/simulation.py", line 328, in build
    self._mesh = pybamm.Mesh(self._geometry, self._submesh_types, self._var_pts)
  File "/home/mrobins/git/PyBaMM/src/pybamm/meshes/meshes.py", line 117, in __init__
    self[domain] = submesh_types[domain](geometry[domain], submesh_pts[domain])
  File "/home/mrobins/git/PyBaMM/src/pybamm/meshes/meshes.py", line 301, in __call__
    return self.submesh_type(lims, npts, **self.submesh_params)
  File "/home/mrobins/git/PyBaMM/src/pybamm/meshes/one_dimensional_submeshes.py", line 130, in __init__
    edges = np.linspace(spatial_lims["min"], spatial_lims["max"], npts + 1)
  File "/home/mrobins/git/PyBaMM/env/lib/python3.10/site-packages/numpy/core/function_base.py", line 132, in linspace
    dt = result_type(start, stop, float(num))
TypeError: Cannot interpret 'InputParameter(0x6c630c7d1f75ef82, Negative electrode thickness [m], children=[], domains={})' as a data type

Failed for parameter Positive electrode thickness [m]. Error was Cannot interpret 'Addition(-0x30e83b9fe1971dbd, +, children=['0.000125', 'Positive electrode thickness [m]'], domains={})' as a data type
Traceback (most recent call last):
  File "/tmp/ipykernel_1968898/3514874421.py", line 12, in <module>
    sim.solve([0, 3600], inputs={param: original_param})
  File "/home/mrobins/git/PyBaMM/src/pybamm/simulation.py", line 472, in solve
    self.build(initial_soc=initial_soc, inputs=inputs)
  File "/home/mrobins/git/PyBaMM/src/pybamm/simulation.py", line 328, in build
    self._mesh = pybamm.Mesh(self._geometry, self._submesh_types, self._var_pts)
  File "/home/mrobins/git/PyBaMM/src/pybamm/meshes/meshes.py", line 117, in __init__
    self[domain] = submesh_types[domain](geometry[domain], submesh_pts[domain])
  File "/home/mrobins/git/PyBaMM/src/pybamm/meshes/meshes.py", line 301, in __call__
    return self.submesh_type(lims, npts, **self.submesh_params)
  File "/home/mrobins/git/PyBaMM/src/pybamm/meshes/one_dimensional_submeshes.py", line 130, in __init__
    edges = np.linspace(spatial_lims["min"], spatial_lims["max"], npts + 1)
  File "/home/mrobins/git/PyBaMM/env/lib/python3.10/site-packages/numpy/core/function_base.py", line 132, in linspace
    dt = result_type(start, stop, float(num))
TypeError: Cannot interpret 'Addition(-0x30e83b9fe1971dbd, +, children=['0.000125', 'Positive electrode thickness [m]'], domains={})' as a data type