Why Is My Simulation Loop Much Slower Than a Single Experiment?

Hi,

I noticed a big difference in the time it takes to run two similar simulations in PyBaMM, and I don’t understand why. Here’s what I mean:

Case 1:

This code runs in 12.6 seconds:

import pybamm

options = {"thermal": "lumped"}
model = pybamm.lithium_ion.DFN(options=options)
parameter_values = pybamm.ParameterValues("Chen2020")
experiment = pybamm.Experiment(["Charge at 1 A for 1 second"], period="1 second")
sim = pybamm.Simulation(model, parameter_values=parameter_values, experiment=experiment)
sol = sim.solve()

voltage_entries = sol["Terminal voltage [V]"].entries
voltage = float(voltage_entries[-1])
print(voltage)

def battery(current):
    global sol
    
    new_model = model.set_initial_conditions_from(sol, inplace=False)
    sim = pybamm.Simulation(new_model, parameter_values=parameter_values, experiment=experiment)
    sol = sim.solve()
    
    voltage_entries = sol["Terminal voltage [V]"].entries
    voltage = float(voltage_entries[-1])

    return voltage

for i in range(10):
    current = 1
    voltage = battery(current)
    print(voltage)

Case 2:

This code, which does the same total charge duration (11 seconds), runs in 1.2 seconds:

import pybamm

options = {"thermal": "lumped"}
model = pybamm.lithium_ion.DFN(options=options)
parameter_values = pybamm.ParameterValues("Chen2020")
experiment = pybamm.Experiment(["Charge at 1 A for 11 seconds"], period="1 second")
sim = pybamm.Simulation(model, parameter_values=parameter_values, experiment=experiment)
sol = sim.solve()

voltage_entries = sol["Terminal voltage [V]"].entries
print(voltage_entries)

Question:

Both codes are charging the battery for 11 seconds. The only difference is that in the first case, I run the simulation in a loop for 1 second each time and update the initial conditions.

Why is the first case much slower? Is there a way to make the loop faster?

Thanks!

Hi @ErfanSamadi1998, the first case is slower because every time you call

sim = pybamm.Simulation(model, parameter_values=parameter_values, experiment=experiment)

it creates an entirely new simulation whose equations must be regenerated from scratch.

A faster alternative is to provide a starting_solution keyword argument to solve:

import pybamm

options = {"thermal": "lumped"}
model = pybamm.lithium_ion.DFN(options=options)
parameter_values = pybamm.ParameterValues("Chen2020")
experiment = pybamm.Experiment(["Charge at 1 A for 1 second"], period="1 second")
sim = pybamm.Simulation(model, parameter_values=parameter_values, experiment=experiment)

def battery2(sim, starting_solution):
    sol = sim.solve(starting_solution=starting_solution)
    return sol

sol = None
for i in range(11):
    sol = battery2(sim, sol)
    voltage = sol["Voltage [V]"](sol.t[-1])
    print(voltage)

While faster than Case 1, this will still not be as fast as Case 2 because the system of DAEs must be reinitialized from the previous starting point and there is some overhead in calling solve().

Hi,

Thank you so much for your answer and the time you spent helping me. I really appreciate it!

I realized I missed an important line in my first code example:

experiment = pybamm.Experiment([f"Discharge at {current} A for 1 seconds"], period="1 second")

The main problem is that the current (current) can change during each iteration. Here’s the corrected version of my Case 1 code, where I now define current values explicitly and use them in the loop:

import pybamm

options = {"thermal": "lumped"}
model = pybamm.lithium_ion.DFN(options=options)
parameter_values = pybamm.ParameterValues("Chen2020")
experiment = pybamm.Experiment(["Discharge at 1 A for 1 seconds"], period="1 second")
sim = pybamm.Simulation(model, parameter_values=parameter_values, experiment=experiment)
sol = sim.solve()
voltage_entries = sol["Terminal voltage [V]"].entries
voltage = float(voltage_entries[-1])
print(voltage)


def battery(current):
    global sol
    
    new_model = model.set_initial_conditions_from(sol, inplace=False)
    experiment = pybamm.Experiment([f"Discharge at {current} A for 1 seconds"], period="1 second")
    sim = pybamm.Simulation(new_model, parameter_values=parameter_values, experiment=experiment)
    sol = sim.solve()
    
    voltage_entries = sol["Terminal voltage [V]"].entries
    voltage = float(voltage_entries[-1])

    return voltage


# Explicitly define the currents
currents = [1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9]

for i in range(10):
    voltage = battery(currents[i])
    print(voltage)

In this version, the currents are explicitly defined as a list, and the loop iterates over the first 10 values.
I apologize for not explaining this properly the first time. Your solution really helps a lot! But in this case, is there a way to make the loop faster when the current changes dynamically?

Thanks again!

Yes, you can run a piecewise constant current input using the following:

import pybamm

options = {"thermal": "lumped"}
model = pybamm.lithium_ion.DFN(options=options)
parameter_values = pybamm.ParameterValues("Chen2020")
parameter_values.update({"Current function [A]": pybamm.InputParameter("Current [A]")})
solver = pybamm.IDAKLUSolver()
sim = pybamm.Simulation(model,
    parameter_values=parameter_values,
    solver=solver,
)

def battery3(sim, starting_solution, current):
    sol = sim.step(dt=1, starting_solution=starting_solution, inputs={"Current [A]": current})
    return sol

sol = None
currents = [1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9]
for current in currents:
    sol = battery3(sim, sol, current)
    current = sol["Current [A]"](sol.t[-1])
    voltage = sol["Voltage [V]"](sol.t[-1])
    
    print("Current: ", current)
    print("Voltage: ", voltage)

Updates to the script:

  • Removed the Experiment argument and added "Current [A]" as an InputParameter, which allows you to update the value without regenerating the model
  • Replaced sim.solve() with sim.step() and added a dt value
  • This is optional, but I changed the solver from the default to the IDAKLUSolver which is faster

By convention, charging currents in PyBaMM are negative, and discharge currents are positive.

1 Like

Thank you so much for taking the time to answer my question and write the code for me. Your help was really useful, and I truly appreciate the effort you put into it. Thanks again!

If I want to specify an initial SOC for the battery, how should I apply it?

Previously, I was using the following approach:

sim.solve(initial_soc=0.1)

However, I am unsure how to achieve this when using sim.step. Could you please guide me on how to set an initial SOC in this case?

Thank you for your help!

You can set the initial SOC with parameter_values.set_initial_stoichiometries:

parameter_values = pybamm.ParameterValues("Chen2020")
parameter_values.update({"Current function [A]": pybamm.InputParameter("Current [A]")})
parameter_values.set_initial_stoichiometries(0.1)
solver = pybamm.IDAKLUSolver()
sim = pybamm.Simulation(model,
    parameter_values=parameter_values,
    solver=solver,
)

Thank you for your help.
I have noticed some issues while working with sim.step during the battery charge and discharge simulation process:

  1. If the process reaches the lower cut-off voltage or upper cut-off voltage during the simulation, unlike other modes where an out-of-range voltage warning is displayed and the simulation stops, in sim.step, the system seems to freeze. It continues to provide output without any error, but the outputs remain locked at a constant value.

  2. When sim.step is called many times (as in long simulations with small dt), RAM usage increases significantly, even though the sol file size remains relatively small. From my initial analysis, this issue appears to be related to the data recorded by the solver, which, surprisingly, is not cleared by Python’s garbage collection process. This becomes more critical as it gradually slows down the simulation over time.

High RAM Usage Example with PyBaMM

If you run the following code:

import pybamm

# Define model and solver options
options = {"thermal": "lumped"}
model = pybamm.lithium_ion.DFN(options=options)
parameter_values = pybamm.ParameterValues("OKane2022")
parameter_values.update({"Current function [A]": pybamm.InputParameter("Current [A]")})
parameter_values.set_initial_stoichiometries(0.03)
solver = pybamm.IDAKLUSolver()
sim = pybamm.Simulation(model, parameter_values=parameter_values, solver=solver)

# Initialize variables
sol = None
voltage = 2
j = 0

# Start simulation loop
for i in range(30):
    print(f"1/2 cycle number: {i + 1}")
    while voltage < 4.1:
        j += 1
        current = -6  # Discharge phase
        sol = sim.step(dt=1, starting_solution=sol, inputs={"Current [A]": current})
        voltage = sol["Terminal voltage [V]"](sol.t[-1])
        if j % 1000 == 5:
            print(f"voltage: {voltage}")

    print(f"2/2 cycle number: {i + 1}")
    while voltage > 2.6:
        j += 1
        current = 6  # Charge phase
        sol = sim.step(dt=1, starting_solution=sol, inputs={"Current [A]": current})
        voltage = sol["Terminal voltage [V]"](sol.t[-1])
        if j % 1000 == 5:
            print(f"voltage: {voltage}")

Observed Output

After running the script, the output looks like this:

1/2 cycle number: 1
voltage: 3.2656128698140425
voltage: 3.845636151281104
2/2 cycle number: 1
voltage: 3.700176770100468
voltage: 3.3734337555645357
1/2 cycle number: 2
voltage: 3.526926352985715
voltage: 3.896283147538643

Issue

At this point, Python’s RAM usage reaches around 1.2 GB, which seems too high.

Question

Is there any solution to reduce this high RAM usage?

Each step of the simulation stores the adaptive time stepping points for high accuracy post-processing. To only store the first and final step of each simulation, you can specify the interpolation save points t_interp as

sim.step(dt=1, t_interp = [0,1], starting_solution=sol, inputs={"Current [A]": current})

This will help limit your memory usage. We are also working on additional memory bug fixes for the garbage collection issue which are coming soon.

Once you hit the voltage limit, the simulation tries to take a step using the following current but cannot because the voltage limit is actively being violated. You can disable the lower/upper voltage limits by setting them to -np.inf and np.inf. respectively, and/or monitor the voltage within your while loop.

Thank you for the explanation! It really clarified how to limit memory usage with t_interp and was very helpful. By the way, if possible, could you also share an example of how to disable the lower/upper voltage limits by setting them to -np.inf and np.inf?

Thanks!

Check out this guide here: Tutorial 4 - Setting parameter values — PyBaMM v25.1.1 Manual

The relevant parameters are Lower voltage cut-off [V] and Upper voltage cut-off [V].