Skip to content

Commit 2d561b7

Browse files
Merge pull request #2823 from pybamm-team/issue-1442-ram
Revert lru_cache to functional form; Add lru-dict for integrators
2 parents a8bfe24 + 49a1b90 commit 2d561b7

File tree

6 files changed

+145
-9
lines changed

6 files changed

+145
-9
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,17 @@
22

33
## Features
44

5+
- 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)
56
- Added new variables, related to electrode balance, for the `ElectrodeSOH` model ([#2807](https://github.com/pybamm-team/PyBaMM/pull/2807))
7+
- 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))
68
- Renamed "Terminal voltage [V]" to just "Voltage [V]". "Terminal voltage [V]" can still be used and will return the same value as "Voltage [V]". ([#2740](https://github.com/pybamm-team/PyBaMM/pull/2740))
79
- Added "Negative electrode surface potential difference at separator interface [V]", which is the value of the surface potential difference (`phi_s - phi_e`) at the anode/separator interface, commonly controlled in fast-charging algorithms to avoid plating. Also added "Positive electrode surface potential difference at separator interface [V]". ([#2740](https://github.com/pybamm-team/PyBaMM/pull/2740))
810
- 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))
911
- Added an example for `plot_voltage_components`, explaining what the different voltage components are. ([#2740](https://github.com/pybamm-team/PyBaMM/pull/2740))
10-
- 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))
1112

1213
## Bug fixes
1314

15+
- Fixed excessive RAM consumption when running multiple simulations ([#2823](https://github.com/pybamm-team/PyBaMM/pull/2823))
1416
- Fixed a bug where variable bounds could not contain `InputParameters` ([#2795](https://github.com/pybamm-team/PyBaMM/pull/2795))
1517
- Improved `model.latexify()` to have a cleaner and more readable output ([#2764](https://github.com/pybamm-team/PyBaMM/pull/2764))
1618
- Fixed electrolyte conservation in the case of concentration-dependent transference number ([#2758](https://github.com/pybamm-team/PyBaMM/pull/2758))

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 leaves the number of integrators unbound. Default is 100.
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=100,
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 # Allow parent to handle exception
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 # Allow parent to handle exception
32+
return super().get(key)
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
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+
# assertCountEqual checks that the same elements are present in
22+
# both lists, not just that the lists are of equal count
23+
self.assertCountEqual(set(d.keys()), set(dd.keys()))
24+
self.assertCountEqual(set(d.values()), set(dd.values()))
25+
26+
def test_lrudict_noitems(self):
27+
"""Edge case: no items in LRU, raises KeyError on assignment"""
28+
d = LRUDict(maxsize=-1)
29+
with self.assertRaises(KeyError):
30+
d["a"] = 1
31+
32+
def test_lrudict_singleitem(self):
33+
"""Only the last added element should ever be present"""
34+
d = LRUDict(maxsize=1)
35+
item_list = range(1, 100)
36+
self.assertEqual(len(d), 0)
37+
for item in item_list:
38+
d[item] = item
39+
self.assertEqual(len(d), 1)
40+
self.assertIsNotNone(d[item])
41+
# Finally, pop the only item and check that the dictionary is empty
42+
d.popitem()
43+
self.assertEqual(len(d), 0)
44+
45+
def test_lrudict_multiitem(self):
46+
"""Check that the correctly ordered items are always present"""
47+
for maxsize in range(1, 101, 5):
48+
d = LRUDict(maxsize=maxsize)
49+
expected = OrderedDict()
50+
for key in range(1, 100):
51+
value = f"v{key}"
52+
d[key] = value
53+
expected[key] = value
54+
if key % 5 == 0 and maxsize > 3:
55+
# Push item to front of the list
56+
key_to_read = key - 3
57+
d.get(key_to_read)
58+
expected.move_to_end(key_to_read)
59+
expected = OrderedDict(
60+
(k, expected[k]) for k in list(expected.keys())[-maxsize:]
61+
)
62+
self.assertListEqual(list(d.keys()), list(expected.keys()))
63+
self.assertListEqual(list(d.values()), list(expected.values()))
64+
65+
def test_lrudict_invalidkey(self):
66+
d = LRUDict()
67+
value = 1
68+
d["a"] = value
69+
# Access with valid key
70+
self.assertEqual(d["a"], value) # checks getitem()
71+
self.assertEqual(d.get("a"), value) # checks get()
72+
# Access with invalid key
73+
with self.assertRaises(KeyError):
74+
_ = d["b"] # checks getitem()
75+
self.assertIsNone(d.get("b")) # checks get()

0 commit comments

Comments
 (0)