Skip to content

Commit 25936fc

Browse files
committed
Revert lru_cache to functional form; Add lru-dict for integrators
1 parent c78bed2 commit 25936fc

File tree

6 files changed

+129
-8
lines changed

6 files changed

+129
-8
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@
88
- Added "Open-circuit voltage [V]", which is the open-circuit voltage as calculated from the bulk particle concentrations. The old variable "Measured open circuit voltage [V]", which referred to the open-circuit potential as calculated from the surface particle concentrations, has been renamed to "Surface open-circuit voltage [V]". ([#2740](https://github.com/pybamm-team/PyBaMM/pull/2740))
99
- Added an example for `plot_voltage_components`, explaining what the different voltage components are. ([#2740](https://github.com/pybamm-team/PyBaMM/pull/2740))
1010
- Added method to calculate maximum theoretical energy. ([#2777](https://github.com/pybamm-team/PyBaMM/pull/2777)) and add to summary variables ([#2781](https://github.com/pybamm-team/PyBaMM/pull/2781))
11+
- Added option to limit the number of integrators stored in CasadiSolver, which is particularly relevant when running simulations back-to-back [#2823](https://github.com/pybamm-team/PyBaMM/pull/2823)
1112

1213
## Bug fixes
1314

1415
- Fixed a bug where variable bounds could not contain `InputParameters` ([#2795](https://github.com/pybamm-team/PyBaMM/pull/2795))
1516
- Improved `model.latexify()` to have a cleaner and more readable output ([#2764](https://github.com/pybamm-team/PyBaMM/pull/2764))
1617
- Fixed electrolyte conservation in the case of concentration-dependent transference number ([#2758](https://github.com/pybamm-team/PyBaMM/pull/2758))
1718
- Fixed `plot_voltage_components` so that the sum of overpotentials is now equal to the voltage ([#2740](https://github.com/pybamm-team/PyBaMM/pull/2740))
19+
- Fixed excessive RAM consumption when running multiple simulations ([#2823](https://github.com/pybamm-team/PyBaMM/pull/2823))
1820

1921
## Optimizations
2022

pybamm/models/full_battery_models/lithium_ion/electrode_soh.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -205,14 +205,18 @@ def __init__(
205205

206206
self.lims_ocp = (x0_min, x100_max, y100_min, y0_max)
207207
self.OCV_function = None
208+
self._get_electrode_soh_sims_full = lru_cache()(
209+
self.__get_electrode_soh_sims_full
210+
)
211+
self._get_electrode_soh_sims_split = lru_cache()(
212+
self.__get_electrode_soh_sims_split
213+
)
208214

209-
@lru_cache
210-
def _get_electrode_soh_sims_full(self):
215+
def __get_electrode_soh_sims_full(self):
211216
full_model = _ElectrodeSOH(param=self.param, known_value=self.known_value)
212217
return pybamm.Simulation(full_model, parameter_values=self.parameter_values)
213218

214-
@lru_cache
215-
def _get_electrode_soh_sims_split(self):
219+
def __get_electrode_soh_sims_split(self):
216220
x100_model = _ElectrodeSOH(
217221
param=self.param, solve_for=["x_100"], known_value=self.known_value
218222
)

pybamm/simulation.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,23 @@ def __init__(
128128

129129
warnings.filterwarnings("ignore")
130130

131+
self.get_esoh_solver = lru_cache()(self._get_esoh_solver)
132+
133+
def __getstate__(self):
134+
"""
135+
Return dictionary of picklable items
136+
"""
137+
result = self.__dict__.copy()
138+
result["get_esoh_solver"] = None # Exclude LRU cache
139+
return result
140+
141+
def __setstate__(self, state):
142+
"""
143+
Unpickle, restoring unpicklable relationships
144+
"""
145+
self.__dict__ = state
146+
self.get_esoh_solver = lru_cache()(self._get_esoh_solver)
147+
131148
def set_up_and_parameterise_experiment(self):
132149
"""
133150
Set up a simulation to run with an experiment. This creates a dictionary of
@@ -900,8 +917,7 @@ def step(
900917

901918
return self.solution
902919

903-
@lru_cache
904-
def get_esoh_solver(self, calc_esoh):
920+
def _get_esoh_solver(self, calc_esoh):
905921
if (
906922
calc_esoh is False
907923
or isinstance(self.model, pybamm.lead_acid.BaseModel)

pybamm/solvers/casadi_solver.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import numpy as np
77
import warnings
88
from scipy.interpolate import interp1d
9+
from .lrudict import LRUDict
910

1011

1112
class CasadiSolver(pybamm.BaseSolver):
@@ -67,6 +68,10 @@ class CasadiSolver(pybamm.BaseSolver):
6768
Whether to perturb algebraic initial conditions to avoid a singularity. This
6869
can sometimes slow down the solver, but is kept True as default for "safe" mode
6970
as it seems to be more robust (False by default for other modes).
71+
integrators_maxcount : int, optional
72+
The maximum number of integrators that the solver will retain before
73+
ejecting past integrators using an LRU methodology. A value of 0 or
74+
None (default) leaves the number of integrators unbound.
7075
"""
7176

7277
def __init__(
@@ -83,6 +88,7 @@ def __init__(
8388
extra_options_call=None,
8489
return_solution_if_failed_early=False,
8590
perturb_algebraic_initial_conditions=None,
91+
integrators_maxcount=None,
8692
):
8793
super().__init__(
8894
"problem dependent",
@@ -123,8 +129,9 @@ def __init__(
123129
self.name = "CasADi solver with '{}' mode".format(mode)
124130

125131
# Initialize
126-
self.integrators = {}
127-
self.integrator_specs = {}
132+
self.integrators_maxcount = integrators_maxcount
133+
self.integrators = LRUDict(maxsize=self.integrators_maxcount)
134+
self.integrator_specs = LRUDict(maxsize=self.integrators_maxcount)
128135
self.y_sols = {}
129136

130137
pybamm.citations.register("Andersson2019")

pybamm/solvers/lrudict.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from collections import OrderedDict
2+
3+
4+
class LRUDict(OrderedDict):
5+
"""LRU extension of a dictionary"""
6+
7+
def __init__(self, maxsize=None):
8+
"""maxsize limits the item count based on an LRU strategy
9+
10+
The dictionary remains unbound when maxsize = 0 | None
11+
"""
12+
super().__init__()
13+
self.maxsize = maxsize
14+
15+
def __setitem__(self, key, value):
16+
super().__setitem__(key, value)
17+
while self.maxsize and self.__len__() > self.maxsize:
18+
self.popitem(last=False)
19+
20+
def __getitem__(self, key):
21+
try:
22+
self.move_to_end(key, last=True)
23+
except KeyError:
24+
pass
25+
return super().__getitem__(key)
26+
27+
def get(self, key):
28+
try:
29+
self.move_to_end(key, last=True)
30+
except KeyError:
31+
pass
32+
return super().get(key)
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
#
2+
# Tests for the LRUDict class
3+
#
4+
import unittest
5+
from pybamm.solvers.lrudict import LRUDict
6+
from collections import OrderedDict
7+
8+
9+
class TestLRUDict(unittest.TestCase):
10+
def test_lrudict_defaultbehaviour(self):
11+
"""Default behaviour [no LRU] mimics Dict"""
12+
d = LRUDict()
13+
dd = dict()
14+
for count in range(1, 100):
15+
d[count] = f"v{count}"
16+
dd[count] = f"v{count}"
17+
if count % 5 == 0:
18+
# LRU will reorder list, but not remove entries
19+
d.get(count - 2)
20+
dd.get(count - 2)
21+
assert set(d.keys()) == set(dd.keys())
22+
assert set(d.values()) == set(dd.values())
23+
24+
def test_lrudict_noitems(self):
25+
"""Edge case: no items in LRU, raises KeyError on assignment"""
26+
d = LRUDict(maxsize=-1)
27+
with self.assertRaises(KeyError):
28+
d["a"] = 1
29+
30+
def test_lrudict_singleitem(self):
31+
"""Only the last added element should be present"""
32+
d = LRUDict(maxsize=1)
33+
item_list = range(1, 100)
34+
assert len(d) == 0
35+
for item in item_list:
36+
d[item] = item
37+
assert len(d) == 1
38+
assert d[item]
39+
d.popitem()
40+
assert len(d) == 0
41+
42+
def test_lrudict_multiitem(self):
43+
"""Check that the correctly ordered items are always present"""
44+
for maxsize in range(1, 101, 5):
45+
d = LRUDict(maxsize=maxsize)
46+
expected = OrderedDict()
47+
for key in range(1, 100):
48+
value = f"v{key}"
49+
d[key] = value
50+
expected[key] = value
51+
if key % 5 == 0 and maxsize > 3:
52+
# Push item to front of the list
53+
key_to_read = key - 3
54+
d.get(key_to_read)
55+
expected.move_to_end(key_to_read)
56+
expected = OrderedDict(
57+
(k, expected[k]) for k in list(expected.keys())[-maxsize:]
58+
)
59+
assert list(d.keys()) == list(expected.keys())
60+
assert list(d.values()) == list(expected.values())

0 commit comments

Comments
 (0)