Skip to content

Commit 4005bc0

Browse files
committed
Eliminate UNDETERMINED logic to cache data
Caching is now handled by the misc.CachedReadOnlyProperty descriptor, which allows for less boilerplate code when handling data that should be cached.
1 parent 503ce81 commit 4005bc0

File tree

3 files changed

+148
-153
lines changed

3 files changed

+148
-153
lines changed

stagpy/_step.py

Lines changed: 43 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,7 @@
1414
import numpy as np
1515

1616
from . import error, misc, phyvars, stagyyparsers
17-
18-
19-
UNDETERMINED = object()
20-
# dummy object with a unique identifier,
21-
# useful to mark stuff as yet undetermined,
22-
# as opposed to either some value or None if
23-
# non existent
17+
from .misc import CachedReadOnlyProperty as crop
2418

2519

2620
class _Geometry:
@@ -209,8 +203,6 @@ class _Fields(Mapping):
209203

210204
def __init__(self, step, variables, extravars, files, filesh5):
211205
self.step = step
212-
self._header = UNDETERMINED
213-
self._geom = UNDETERMINED
214206
self._vars = variables
215207
self._extra = extravars
216208
self._files = files
@@ -232,7 +224,7 @@ def __getitem__(self, name):
232224
raise error.MissingDataError(
233225
f'Missing field {name} in step {self.step.istep}')
234226
header, fields = parsed_data
235-
self._header = header
227+
self._cropped__header = header
236228
for fld_name, fld in zip(fld_names, fields):
237229
if self._header['xyp'] == 0:
238230
if not self.geom.twod_yz:
@@ -281,7 +273,6 @@ def _get_raw_data(self, name):
281273
if filestem in phyvars.SFIELD_FILES_H5:
282274
xmff = 'Data{}.xmf'.format(
283275
'Bottom' if name.endswith('bot') else 'Surface')
284-
_ = self.geom
285276
header = self._header
286277
else:
287278
xmff = 'Data.xmf'
@@ -304,31 +295,25 @@ def __delitem__(self, name):
304295
if name in self._data:
305296
del self._data[name]
306297

307-
@property
298+
@crop
299+
def _header(self):
300+
binfiles = self.step.sdat._binfiles_set(self.step.isnap)
301+
if binfiles:
302+
return stagyyparsers.fields(binfiles.pop(), only_header=True)
303+
elif self.step.sdat.hdf5:
304+
xmf = self.step.sdat.hdf5 / 'Data.xmf'
305+
return stagyyparsers.read_geom_h5(xmf, self.step.isnap)[0]
306+
307+
@crop
308308
def geom(self):
309309
"""Geometry information.
310310
311311
:class:`_Geometry` instance holding geometry information. It is
312312
issued from binary files holding field information. It is set to
313313
None if not available for this time step.
314314
"""
315-
if self._header is UNDETERMINED:
316-
binfiles = self.step.sdat._binfiles_set(self.step.isnap)
317-
if binfiles:
318-
self._header = stagyyparsers.fields(binfiles.pop(),
319-
only_header=True)
320-
elif self.step.sdat.hdf5:
321-
xmf = self.step.sdat.hdf5 / 'Data.xmf'
322-
self._header, _ = stagyyparsers.read_geom_h5(xmf,
323-
self.step.isnap)
324-
else:
325-
self._header = None
326-
if self._geom is UNDETERMINED:
327-
if self._header is None:
328-
self._geom = None
329-
else:
330-
self._geom = _Geometry(self._header, self.step.sdat.par)
331-
return self._geom
315+
if self._header is not None:
316+
return _Geometry(self._header, self.step.sdat.par)
332317

333318

334319
class _Tracers:
@@ -393,17 +378,15 @@ class _Rprofs:
393378

394379
def __init__(self, step):
395380
self.step = step
396-
self._data = UNDETERMINED
397381
self._cached_extra = {}
398-
self._centers = UNDETERMINED
399-
self._walls = UNDETERMINED
400-
self._bounds = UNDETERMINED
382+
383+
@crop
384+
def _data(self):
385+
step = self.step
386+
return step.sdat._rprof_and_times[0].get(step.istep)
401387

402388
@property
403389
def _rprofs(self):
404-
if self._data is UNDETERMINED:
405-
step = self.step
406-
self._data = step.sdat._rprof_and_times[0].get(step.istep)
407390
if self._data is None:
408391
step = self.step
409392
raise error.MissingDataError(
@@ -434,43 +417,38 @@ def __getitem__(self, name):
434417

435418
return Rprof(rprof, rad, meta)
436419

437-
@property
420+
@crop
438421
def centers(self):
439422
"""Radial position of cell centers."""
440-
if self._centers is UNDETERMINED:
441-
self._centers = self._rprofs['r'].values + self.bounds[0]
442-
return self._centers
423+
return self._rprofs['r'].values + self.bounds[0]
443424

444-
@property
425+
@crop
445426
def walls(self):
446427
"""Radial position of cell walls."""
447-
if self._walls is UNDETERMINED:
448-
rbot, rtop = self.bounds
449-
centers = self.centers
450-
# assume walls are mid-way between T-nodes
451-
# could be T-nodes at center between walls
452-
self._walls = (centers[:-1] + centers[1:]) / 2
453-
self._walls = np.insert(self._walls, 0, rbot)
454-
self._walls = np.append(self._walls, rtop)
455-
return self._walls
456-
457-
@property
428+
rbot, rtop = self.bounds
429+
centers = self.centers
430+
# assume walls are mid-way between T-nodes
431+
# could be T-nodes at center between walls
432+
walls = (centers[:-1] + centers[1:]) / 2
433+
walls = np.insert(walls, 0, rbot)
434+
walls = np.append(walls, rtop)
435+
return walls
436+
437+
@crop
458438
def bounds(self):
459439
"""Radial or vertical position of boundaries.
460440
461441
Radial/vertical positions of boundaries of the domain.
462442
"""
463-
if self._bounds is UNDETERMINED:
464-
step = self.step
465-
if step.geom is not None:
466-
rcmb = step.geom.rcmb
467-
else:
468-
rcmb = step.sdat.par['geometry']['r_cmb']
469-
if step.sdat.par['geometry']['shape'].lower() == 'cartesian':
470-
rcmb = 0
471-
rcmb = max(rcmb, 0)
472-
self._bounds = rcmb, rcmb + 1
473-
return self._bounds
443+
step = self.step
444+
if step.geom is not None:
445+
rcmb = step.geom.rcmb
446+
else:
447+
rcmb = step.sdat.par['geometry']['r_cmb']
448+
if step.sdat.par['geometry']['shape'].lower() == 'cartesian':
449+
rcmb = 0
450+
rcmb = max(rcmb, 0)
451+
return rcmb, rcmb + 1
474452

475453

476454
class Step:
@@ -520,7 +498,7 @@ def __init__(self, istep, sdat):
520498
phyvars.SFIELD_FILES, phyvars.SFIELD_FILES_H5)
521499
self.tracers = _Tracers(self)
522500
self.rprofs = _Rprofs(self)
523-
self._isnap = UNDETERMINED
501+
self._isnap = -1
524502

525503
def __repr__(self):
526504
if self.isnap is not None:
@@ -549,7 +527,7 @@ def isnap(self):
549527
550528
It is set to None if no snapshot exists for the time step.
551529
"""
552-
if self._isnap is UNDETERMINED:
530+
if self._isnap == -1:
553531
istep = None
554532
isnap = -1
555533
# could be more efficient if do 0 and -1 then bisection

stagpy/misc.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,39 @@ def find_in_sorted_arr(value, array, after=False):
121121
return ielt
122122

123123

124+
class CachedReadOnlyProperty:
125+
"""Descriptor implementation of read-only cached properties.
126+
127+
Properties are cached as _cropped_{name} instance attribute.
128+
129+
This is preferable to using a combination of property and
130+
functools.lru_cache since the cache is bound to instances and therefore get
131+
GCd with the instance when the latter is no longer in use instead of
132+
staying in the cache which would use the instance itself as its key.
133+
134+
This also has an advantage over @cached_property (Python>3.8): the property
135+
is read-only instead of being writeable.
136+
"""
137+
138+
def __init__(self, thunk):
139+
self._thunk = thunk
140+
self._name = thunk.__name__
141+
self._cache_name = f'_cropped_{self._name}'
142+
143+
def __get__(self, instance, _):
144+
try:
145+
return getattr(instance, self._cache_name)
146+
except AttributeError:
147+
pass
148+
cached_value = self._thunk(instance)
149+
setattr(instance, self._cache_name, cached_value)
150+
return cached_value
151+
152+
def __set__(self, instance, _):
153+
raise AttributeError(
154+
f'Cannot set {self._name} property of {instance!r}')
155+
156+
124157
class InchoateFiles:
125158
"""Context manager handling files whose names are not known yet.
126159

0 commit comments

Comments
 (0)