Skip to content

Commit 6847690

Browse files
committed
group logic for step/snap correspondence in _caching.StepSnap
1 parent 70438d4 commit 6847690

File tree

3 files changed

+137
-72
lines changed

3 files changed

+137
-72
lines changed

src/stagpy/_caching.py

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
from __future__ import annotations
22

3+
import re
34
import typing
5+
from abc import ABC, abstractmethod
46
from collections import deque
57
from dataclasses import dataclass
68
from functools import cached_property
79

10+
from . import phyvars, stagyyparsers
11+
812
if typing.TYPE_CHECKING:
13+
from typing import Mapping
14+
915
from .datatypes import Field
16+
from .stagyydata import StagyyData
1017

1118

1219
@dataclass(frozen=True)
@@ -57,3 +64,119 @@ def evict_istep(self, istep: int) -> None:
5764
self._stack.clear()
5865
self._stack.extend(to_keep)
5966
assert len(self._stack) == len(self._data)
67+
68+
69+
class StepSnap(ABC):
70+
"""Keep track of the step/snap correspondence."""
71+
72+
@abstractmethod
73+
def istep(self, *, isnap: int) -> int | None: ...
74+
75+
@abstractmethod
76+
def isnap(self, *, istep: int) -> int | None: ...
77+
78+
@abstractmethod
79+
def len_snap(self) -> int: ...
80+
81+
82+
@dataclass(frozen=True)
83+
class StepSnapInfo:
84+
step_to_snap: Mapping[int, int]
85+
snap_to_step: Mapping[int, int]
86+
isnap_max: int
87+
88+
89+
@dataclass(frozen=True)
90+
class StepSnapH5(StepSnap):
91+
sdat: StagyyData
92+
93+
@cached_property
94+
def _info(self) -> StepSnapInfo:
95+
assert self.sdat.hdf5 is not None
96+
isnap = -1
97+
step_to_snap = {}
98+
snap_to_step = {}
99+
for isnap, istep in stagyyparsers.read_time_h5(self.sdat.hdf5):
100+
step_to_snap[istep] = isnap
101+
snap_to_step[isnap] = istep
102+
return StepSnapInfo(
103+
step_to_snap=step_to_snap,
104+
snap_to_step=snap_to_step,
105+
isnap_max=isnap,
106+
)
107+
108+
def istep(self, *, isnap: int) -> int | None:
109+
return self._info.snap_to_step.get(isnap)
110+
111+
def isnap(self, *, istep: int) -> int | None:
112+
return self._info.step_to_snap.get(istep)
113+
114+
def len_snap(self) -> int:
115+
return self._info.isnap_max + 1
116+
117+
118+
@dataclass(frozen=True)
119+
class StepSnapLegacy(StepSnap):
120+
sdat: StagyyData
121+
122+
@cached_property
123+
def _step_to_snap(self) -> dict[int, int | None]:
124+
return {}
125+
126+
@cached_property
127+
def _snap_to_step(self) -> dict[int, int | None]:
128+
return {}
129+
130+
@cached_property
131+
def isnap_max(self) -> int:
132+
imax = -1
133+
out_stem = re.escape(self.sdat.par.legacy_output("_").name[:-1])
134+
rgx = re.compile(f"^{out_stem}_([a-zA-Z]+)([0-9]{{5}})$")
135+
fstems = set(fstem for fstem in phyvars.FIELD_FILES)
136+
for fname in self.sdat._files:
137+
match = rgx.match(fname.name)
138+
if match is not None and match.group(1) in fstems:
139+
imax = max(int(match.group(2)), imax)
140+
return imax
141+
142+
def len_snap(self) -> int:
143+
return self.isnap_max + 1
144+
145+
def istep(self, *, isnap: int) -> int | None:
146+
if isnap < 0 or isnap > self.isnap_max:
147+
return None
148+
istep = self._snap_to_step.get(isnap, -1)
149+
if istep == -1:
150+
binfiles = self.sdat._binfiles_set(isnap)
151+
if binfiles:
152+
istep = stagyyparsers.field_istep(binfiles.pop())
153+
else:
154+
istep = None
155+
self._snap_to_step[isnap] = istep
156+
if istep is not None:
157+
self._step_to_snap[istep] = isnap
158+
return istep
159+
160+
def isnap(self, *, istep: int) -> int | None:
161+
if istep < 0:
162+
return None
163+
isnap = self._step_to_snap.get(istep, -1)
164+
if isnap == -1:
165+
istep_try = None
166+
# might be more efficient to do 0 and -1 then bisection, even if
167+
# that means losing intermediate information
168+
while (istep_try is None or istep_try < istep) and isnap < 99999:
169+
isnap += 1
170+
try:
171+
istep_try = self.sdat.snaps[isnap].istep
172+
except KeyError:
173+
pass
174+
# all intermediate istep could have their isnap to None
175+
self._snap_to_step[isnap] = istep_try
176+
if istep_try is not None:
177+
self._step_to_snap[istep_try] = isnap
178+
179+
if istep_try != istep:
180+
self._step_to_snap[istep] = None
181+
182+
return self._step_to_snap[istep]

src/stagpy/stagyydata.py

Lines changed: 13 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88

99
from __future__ import annotations
1010

11-
import re
1211
import typing
1312
from collections import abc
1413
from dataclasses import dataclass, field
@@ -20,7 +19,7 @@
2019

2120
from . import _helpers, error, phyvars, stagyyparsers, step
2221
from . import datatypes as dt
23-
from ._caching import FieldCache
22+
from ._caching import FieldCache, StepSnap, StepSnapH5, StepSnapLegacy
2423
from .parfile import StagyyPar
2524
from .stagyyparsers import FieldXmf, TracersXmf
2625
from .step import Step
@@ -390,15 +389,10 @@ class Snaps:
390389

391390
def __init__(self, sdat: StagyyData):
392391
self.sdat = sdat
393-
self._all_isteps_known = False
394392

395393
def __repr__(self) -> str:
396394
return f"{self.sdat!r}.snaps"
397395

398-
@cached_property
399-
def _isteps(self) -> dict[int, int | None]:
400-
return {}
401-
402396
@typing.overload
403397
def __getitem__(self, isnap: int) -> Step: ...
404398

@@ -412,53 +406,21 @@ def __getitem__(self, isnap: int | slice | Sequence[StepIndex]) -> Step | StepsV
412406
assert isinstance(isnap, int)
413407
if isnap < 0:
414408
isnap += len(self)
415-
if isnap < 0 or isnap >= len(self):
416-
istep = None
417-
else:
418-
istep = self._isteps.get(isnap, None if self._all_isteps_known else -1)
419-
if istep == -1:
420-
# isnap not in _isteps but not all isteps known, keep looking
421-
binfiles = self.sdat._binfiles_set(isnap)
422-
if binfiles:
423-
istep = stagyyparsers.field_istep(binfiles.pop())
424-
else:
425-
istep = None
426-
if istep is not None:
427-
self._bind(isnap, istep)
428-
else:
429-
self._isteps[isnap] = None
409+
istep = self.sdat._step_snap.istep(isnap=isnap)
430410
if istep is None:
431411
raise error.InvalidSnapshotError(self.sdat, isnap, "Invalid snapshot index")
432412
return self.sdat.steps[istep]
433413

434414
def __delitem__(self, isnap: int | None) -> None:
435415
if isnap is not None:
436-
istep = self._isteps.get(isnap)
416+
istep = self.sdat._step_snap.istep(isnap=isnap)
437417
del self.sdat.steps[istep]
438418

439-
@cached_property
440-
def _len(self) -> int:
441-
length = -1
442-
if self.sdat.hdf5:
443-
isnap = -1
444-
for isnap, istep in stagyyparsers.read_time_h5(self.sdat.hdf5):
445-
self._bind(isnap, istep)
446-
length = isnap
447-
self._all_isteps_known = True
448-
if length < 0:
449-
out_stem = re.escape(self.sdat.par.legacy_output("_").name[:-1])
450-
rgx = re.compile(f"^{out_stem}_([a-zA-Z]+)([0-9]{{5}})$")
451-
fstems = set(fstem for fstem in phyvars.FIELD_FILES)
452-
for fname in self.sdat._files:
453-
match = rgx.match(fname.name)
454-
if match is not None and match.group(1) in fstems:
455-
length = max(int(match.group(2)), length)
456-
if length < 0:
457-
raise error.NoSnapshotError(self.sdat)
458-
return length + 1
459-
460419
def __len__(self) -> int:
461-
return self._len
420+
length = self.sdat._step_snap.len_snap()
421+
if length <= 0:
422+
raise error.NoSnapshotError(self.sdat)
423+
return length
462424

463425
def __iter__(self) -> Iterator[Step]:
464426
return iter(self[:])
@@ -490,16 +452,6 @@ def at_time(self, time: float, after: bool = False) -> Step:
490452
igp -= 1
491453
return self[igp]
492454

493-
def _bind(self, isnap: int, istep: int) -> None:
494-
"""Register the isnap / istep correspondence.
495-
496-
Args:
497-
isnap: snapshot index.
498-
istep: time step index.
499-
"""
500-
self._isteps[isnap] = istep
501-
self.sdat.steps[istep]._isnap = isnap
502-
503455
def filter(
504456
self,
505457
snap: bool = False,
@@ -824,3 +776,9 @@ def _binfiles_set(self, isnap: int) -> set[Path]:
824776
@cached_property
825777
def _field_cache(self) -> FieldCache:
826778
return FieldCache(maxsize=50)
779+
780+
@cached_property
781+
def _step_snap(self) -> StepSnap:
782+
if self.hdf5 is not None:
783+
return StepSnapH5(sdat=self)
784+
return StepSnapLegacy(sdat=self)

src/stagpy/step.py

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -596,7 +596,6 @@ def __init__(self, istep: int, sdat: StagyyData):
596596
)
597597
self.tracers = Tracers(self)
598598
self.rprofs = Rprofs(self)
599-
self._isnap: int | None = -1
600599

601600
def __repr__(self) -> str:
602601
if self.isnap is not None:
@@ -642,19 +641,4 @@ def isnap(self) -> int | None:
642641
643642
It is None if no snapshot exists for the time step.
644643
"""
645-
if self._isnap == -1:
646-
istep = None
647-
isnap = -1
648-
# could be more efficient if do 0 and -1 then bisection
649-
# (but loose intermediate <- would probably use too much
650-
# memory for what it's worth if search algo is efficient)
651-
while (istep is None or istep < self.istep) and isnap < 99999:
652-
isnap += 1
653-
try:
654-
istep = self.sdat.snaps[isnap].istep
655-
except KeyError:
656-
pass
657-
# all intermediate istep could have their ._isnap to None
658-
if istep != self.istep:
659-
self._isnap = None
660-
return self._isnap
644+
return self.sdat._step_snap.isnap(istep=self.istep)

0 commit comments

Comments
 (0)