Skip to content

Commit 0f6a970

Browse files
committed
Use type+index matching for checkpoints instead of UUIDs
1 parent 93c065c commit 0f6a970

8 files changed

Lines changed: 114 additions & 91 deletions

File tree

src/pathsim/blocks/_block.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -530,11 +530,13 @@ def state(self, val):
530530

531531
# checkpoint methods ----------------------------------------------------------------
532532

533-
def to_checkpoint(self, recordings=False):
533+
def to_checkpoint(self, prefix, recordings=False):
534534
"""Serialize block state for checkpointing.
535535
536536
Parameters
537537
----------
538+
prefix : str
539+
key prefix for NPZ arrays (assigned by simulation)
538540
recordings : bool
539541
include recording data (for Scope blocks)
540542
@@ -545,10 +547,7 @@ def to_checkpoint(self, recordings=False):
545547
npz_data : dict
546548
numpy arrays keyed by path
547549
"""
548-
prefix = self.id
549-
550550
json_data = {
551-
"id": self.id,
552551
"type": self.__class__.__name__,
553552
"active": self._active,
554553
}
@@ -567,27 +566,28 @@ def to_checkpoint(self, recordings=False):
567566
#internal events
568567
if self.events:
569568
evt_jsons = []
570-
for event in self.events:
571-
e_json, e_npz = event.to_checkpoint()
569+
for i, event in enumerate(self.events):
570+
evt_prefix = f"{prefix}/evt_{i}"
571+
e_json, e_npz = event.to_checkpoint(evt_prefix)
572572
evt_jsons.append(e_json)
573573
npz_data.update(e_npz)
574574
json_data["events"] = evt_jsons
575575

576576
return json_data, npz_data
577577

578578

579-
def load_checkpoint(self, json_data, npz):
579+
def load_checkpoint(self, prefix, json_data, npz):
580580
"""Restore block state from checkpoint.
581581
582582
Parameters
583583
----------
584+
prefix : str
585+
key prefix for NPZ arrays (assigned by simulation)
584586
json_data : dict
585587
block metadata from checkpoint JSON
586588
npz : dict-like
587589
numpy arrays from checkpoint NPZ
588590
"""
589-
prefix = json_data["id"]
590-
591591
#verify type
592592
if json_data["type"] != self.__class__.__name__:
593593
raise ValueError(
@@ -611,8 +611,8 @@ def load_checkpoint(self, json_data, npz):
611611

612612
#restore internal events
613613
if self.events and "events" in json_data:
614-
for event, evt_data in zip(self.events, json_data["events"]):
615-
event.load_checkpoint(evt_data, npz)
614+
for i, (event, evt_data) in enumerate(zip(self.events, json_data["events"])):
615+
event.load_checkpoint(f"{prefix}/evt_{i}", evt_data, npz)
616616

617617

618618
# methods for block output and state updates ----------------------------------------

src/pathsim/blocks/delay.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -142,10 +142,9 @@ def reset(self):
142142
self._ring.extend([0.0] * self._n)
143143

144144

145-
def to_checkpoint(self, recordings=False):
145+
def to_checkpoint(self, prefix, recordings=False):
146146
"""Serialize Delay state including buffer data."""
147-
json_data, npz_data = super().to_checkpoint(recordings=recordings)
148-
prefix = self.id
147+
json_data, npz_data = super().to_checkpoint(prefix, recordings=recordings)
149148

150149
json_data["sampling_period"] = self.sampling_period
151150

@@ -160,10 +159,9 @@ def to_checkpoint(self, recordings=False):
160159
return json_data, npz_data
161160

162161

163-
def load_checkpoint(self, json_data, npz):
162+
def load_checkpoint(self, prefix, json_data, npz):
164163
"""Restore Delay state including buffer data."""
165-
super().load_checkpoint(json_data, npz)
166-
prefix = json_data["id"]
164+
super().load_checkpoint(prefix, json_data, npz)
167165

168166
if self.sampling_period is None:
169167
#continuous mode

src/pathsim/blocks/scope.py

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -448,10 +448,9 @@ def save(self, path="scope.csv"):
448448
wrt.writerow(sample)
449449

450450

451-
def to_checkpoint(self, recordings=False):
451+
def to_checkpoint(self, prefix, recordings=False):
452452
"""Serialize Scope state including optional recording data."""
453-
json_data, npz_data = super().to_checkpoint(recordings=recordings)
454-
prefix = self.id
453+
json_data, npz_data = super().to_checkpoint(prefix, recordings=recordings)
455454

456455
json_data["_incremental_idx"] = self._incremental_idx
457456
if hasattr(self, '_sample_next_timestep'):
@@ -464,10 +463,9 @@ def to_checkpoint(self, recordings=False):
464463
return json_data, npz_data
465464

466465

467-
def load_checkpoint(self, json_data, npz):
466+
def load_checkpoint(self, prefix, json_data, npz):
468467
"""Restore Scope state including optional recording data."""
469-
super().load_checkpoint(json_data, npz)
470-
prefix = json_data["id"]
468+
super().load_checkpoint(prefix, json_data, npz)
471469

472470
self._incremental_idx = json_data.get("_incremental_idx", 0)
473471
if hasattr(self, '_sample_next_timestep'):
@@ -479,9 +477,6 @@ def load_checkpoint(self, json_data, npz):
479477
if rt_key in npz and rd_key in npz:
480478
self.recording_time = npz[rt_key].tolist()
481479
self.recording_data = [row for row in npz[rd_key]]
482-
else:
483-
self.recording_time = []
484-
self.recording_data = []
485480

486481

487482
def update(self, t):

src/pathsim/blocks/spectrum.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -283,19 +283,19 @@ def step(self, t, dt):
283283
return True, 0.0, None
284284

285285

286-
def to_checkpoint(self, recordings=False):
286+
def to_checkpoint(self, prefix, recordings=False):
287287
"""Serialize Spectrum state including integration time."""
288-
json_data, npz_data = super().to_checkpoint(recordings=recordings)
288+
json_data, npz_data = super().to_checkpoint(prefix, recordings=recordings)
289289

290290
json_data["time"] = self.time
291291
json_data["t_sample"] = self.t_sample
292292

293293
return json_data, npz_data
294294

295295

296-
def load_checkpoint(self, json_data, npz):
296+
def load_checkpoint(self, prefix, json_data, npz):
297297
"""Restore Spectrum state including integration time."""
298-
super().load_checkpoint(json_data, npz)
298+
super().load_checkpoint(prefix, json_data, npz)
299299

300300
self.time = json_data.get("time", 0.0)
301301
self.t_sample = json_data.get("t_sample", 0.0)

src/pathsim/blocks/switch.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,13 +82,13 @@ def select(self, switch_state=0):
8282
self.switch_state = switch_state
8383

8484

85-
def to_checkpoint(self, recordings=False):
86-
json_data, npz_data = super().to_checkpoint(recordings=recordings)
85+
def to_checkpoint(self, prefix, recordings=False):
86+
json_data, npz_data = super().to_checkpoint(prefix, recordings=recordings)
8787
json_data["switch_state"] = self.switch_state
8888
return json_data, npz_data
8989

90-
def load_checkpoint(self, json_data, npz):
91-
super().load_checkpoint(json_data, npz)
90+
def load_checkpoint(self, prefix, json_data, npz):
91+
super().load_checkpoint(prefix, json_data, npz)
9292
self.switch_state = json_data.get("switch_state", None)
9393

9494
def update(self, t):

src/pathsim/events/_event.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -211,25 +211,27 @@ def resolve(self, t):
211211

212212
# checkpoint methods ----------------------------------------------------------------
213213

214-
def to_checkpoint(self):
214+
def to_checkpoint(self, prefix):
215215
"""Serialize event state for checkpointing.
216216
217+
Parameters
218+
----------
219+
prefix : str
220+
key prefix for NPZ arrays (assigned by simulation)
221+
217222
Returns
218223
-------
219224
json_data : dict
220225
JSON-serializable metadata
221226
npz_data : dict
222227
numpy arrays keyed by path
223228
"""
224-
prefix = self.id
225-
226229
#extract history eval value
227230
hist_eval, hist_time = self._history
228231
if hist_eval is not None and hasattr(hist_eval, 'item'):
229232
hist_eval = float(hist_eval)
230233

231234
json_data = {
232-
"id": self.id,
233235
"type": self.__class__.__name__,
234236
"active": self._active,
235237
"history_eval": hist_eval,
@@ -243,18 +245,18 @@ def to_checkpoint(self):
243245
return json_data, npz_data
244246

245247

246-
def load_checkpoint(self, json_data, npz):
248+
def load_checkpoint(self, prefix, json_data, npz):
247249
"""Restore event state from checkpoint.
248250
249251
Parameters
250252
----------
253+
prefix : str
254+
key prefix for NPZ arrays (assigned by simulation)
251255
json_data : dict
252256
event metadata from checkpoint JSON
253257
npz : dict-like
254258
numpy arrays from checkpoint NPZ
255259
"""
256-
prefix = json_data["id"]
257-
258260
self._active = json_data["active"]
259261
self._history = json_data["history_eval"], json_data["history_time"]
260262

src/pathsim/simulation.py

Lines changed: 59 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -338,11 +338,34 @@ def plot(self, *args, **kwargs):
338338

339339
# checkpoint methods ----------------------------------------------------------
340340

341+
@staticmethod
342+
def _checkpoint_key(type_name, type_counts):
343+
"""Generate a deterministic checkpoint key from block/event type
344+
and occurrence index (e.g. 'Integrator_0', 'Scope_1').
345+
346+
Parameters
347+
----------
348+
type_name : str
349+
class name of the block or event
350+
type_counts : dict
351+
running counter per type name, mutated in place
352+
353+
Returns
354+
-------
355+
key : str
356+
deterministic checkpoint key
357+
"""
358+
idx = type_counts.get(type_name, 0)
359+
type_counts[type_name] = idx + 1
360+
return f"{type_name}_{idx}"
361+
362+
341363
def save_checkpoint(self, path, recordings=False):
342364
"""Save simulation state to checkpoint files (JSON + NPZ).
343365
344366
Creates two files: {path}.json (structure/metadata) and
345-
{path}.npz (numerical data).
367+
{path}.npz (numerical data). Blocks and events are keyed by
368+
type and insertion order for deterministic cross-instance matching.
346369
347370
Parameters
348371
----------
@@ -371,22 +394,28 @@ def save_checkpoint(self, path, recordings=False):
371394
"tolerance_fpi": self.tolerance_fpi,
372395
"iterations_max": self.iterations_max,
373396
},
374-
"blocks": {},
375-
"events": {},
397+
"blocks": [],
398+
"events": [],
376399
}
377400

378401
npz_data = {}
379402

380-
#checkpoint all blocks (keyed by UUID)
403+
#checkpoint all blocks (keyed by type + insertion index)
404+
type_counts = {}
381405
for block in self.blocks:
382-
b_json, b_npz = block.to_checkpoint(recordings=recordings)
383-
checkpoint["blocks"][block.id] = b_json
406+
key = self._checkpoint_key(block.__class__.__name__, type_counts)
407+
b_json, b_npz = block.to_checkpoint(key, recordings=recordings)
408+
b_json["_key"] = key
409+
checkpoint["blocks"].append(b_json)
384410
npz_data.update(b_npz)
385411

386-
#checkpoint external events (keyed by UUID)
412+
#checkpoint external events (keyed by type + insertion index)
413+
type_counts = {}
387414
for event in self.events:
388-
e_json, e_npz = event.to_checkpoint()
389-
checkpoint["events"][event.id] = e_json
415+
key = self._checkpoint_key(event.__class__.__name__, type_counts)
416+
e_json, e_npz = event.to_checkpoint(key)
417+
e_json["_key"] = key
418+
checkpoint["events"].append(e_json)
390419
npz_data.update(e_npz)
391420

392421
#write files
@@ -400,8 +429,9 @@ def load_checkpoint(self, path):
400429
"""Load simulation state from checkpoint files (JSON + NPZ).
401430
402431
Restores simulation time and all block/event states from a
403-
previously saved checkpoint. The simulation must have the same
404-
blocks and events as when the checkpoint was saved.
432+
previously saved checkpoint. Matching is based on block/event
433+
type and insertion order, so the simulation must be constructed
434+
with the same block types in the same order.
405435
406436
Parameters
407437
----------
@@ -444,26 +474,32 @@ def load_checkpoint(self, path):
444474
f"current solver '{self.Solver.__name__}'"
445475
)
446476

447-
#restore blocks
448-
block_data = checkpoint.get("blocks", {})
477+
#index checkpoint blocks by key
478+
block_data = {b["_key"]: b for b in checkpoint.get("blocks", [])}
479+
480+
#restore blocks by type + insertion order
481+
type_counts = {}
449482
for block in self.blocks:
450-
if block.id in block_data:
451-
block.load_checkpoint(block_data[block.id], npz)
483+
key = self._checkpoint_key(block.__class__.__name__, type_counts)
484+
if key in block_data:
485+
block.load_checkpoint(key, block_data[key], npz)
452486
else:
453487
warnings.warn(
454-
f"Block {block.__class__.__name__} (id={block.id[:8]}...) "
455-
f"not found in checkpoint"
488+
f"Block '{key}' not found in checkpoint"
456489
)
457490

458-
#restore external events
459-
event_data = checkpoint.get("events", {})
491+
#index checkpoint events by key
492+
event_data = {e["_key"]: e for e in checkpoint.get("events", [])}
493+
494+
#restore external events by type + insertion order
495+
type_counts = {}
460496
for event in self.events:
461-
if event.id in event_data:
462-
event.load_checkpoint(event_data[event.id], npz)
497+
key = self._checkpoint_key(event.__class__.__name__, type_counts)
498+
if key in event_data:
499+
event.load_checkpoint(key, event_data[key], npz)
463500
else:
464501
warnings.warn(
465-
f"Event {event.__class__.__name__} (id={event.id[:8]}...) "
466-
f"not found in checkpoint"
502+
f"Event '{key}' not found in checkpoint"
467503
)
468504

469505
finally:

0 commit comments

Comments
 (0)