python-drs 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
drs/plot.py ADDED
@@ -0,0 +1,254 @@
1
+ import matplotlib.pyplot as plt
2
+ import matplotlib.ticker as mtick
3
+ import matplotlib.gridspec as gridspec
4
+ import numpy as np
5
+ import pandas as pd
6
+ import seaborn as sns
7
+
8
+ # Visual styling constants
9
+ DEFAULT_FIGSIZE = (10, 6)
10
+ DEFAULT_STYLE = "seaborn-v0_8-whitegrid"
11
+ DEFAULT_COLORS = ["#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd", "#8c564b", "#e377c2", "#7f7f7f", "#bcbd22", "#17becf"]
12
+
13
+
14
+ def apply_plot_style() -> None:
15
+ """
16
+ Apply the default matplotlib stylesheet and seaborn configuration.
17
+
18
+ Power User Note: Standardizes visual aesthetics across all plots.
19
+ """
20
+ plt.style.use(DEFAULT_STYLE)
21
+
22
+
23
+ def _setup_axes(ax=None, figsize=DEFAULT_FIGSIZE):
24
+ """
25
+ [INTERNAL] Helper to initialize or reuse a matplotlib axes.
26
+
27
+ Power User Note: Reduces boilerplate for creating figure/axes objects.
28
+ """
29
+ apply_plot_style()
30
+ if ax is None:
31
+ fig, ax = plt.subplots(figsize=figsize)
32
+ return fig, ax, True
33
+ return ax.figure, ax, False
34
+
35
+
36
+ def plot_time_series(
37
+ df,
38
+ y_columns: list,
39
+ time_col: str = "time",
40
+ title: str = None,
41
+ y_label: str = None,
42
+ is_step: bool = False,
43
+ ax=None,
44
+ add_legend: bool = True,
45
+ colors: list = None,
46
+ **line_kwargs,
47
+ ):
48
+ """Plot multiple time-series curves on a single axis."""
49
+ if time_col not in df.columns:
50
+ raise ValueError(
51
+ f"DataFrame must contain a '{time_col}' column for time-series plotting."
52
+ )
53
+
54
+ fig, ax, own_ax = _setup_axes(ax, DEFAULT_FIGSIZE)
55
+ plot_colors = colors or DEFAULT_COLORS
56
+
57
+ for i, col in enumerate(y_columns):
58
+ if col in df.columns:
59
+ color = plot_colors[i % len(plot_colors)]
60
+
61
+ kwargs = dict(line_kwargs)
62
+ kwargs.setdefault("linewidth", 2)
63
+
64
+ if is_step:
65
+ kwargs.setdefault("where", "post")
66
+ ax.step(df[time_col], df[col], label=col, color=color, **kwargs)
67
+ else:
68
+ ax.plot(df[time_col], df[col], label=col, color=color, **kwargs)
69
+
70
+ if title:
71
+ ax.set_title(title, fontsize=14, pad=15)
72
+ if y_label:
73
+ ax.set_ylabel(y_label, fontsize=12)
74
+
75
+ if add_legend:
76
+ ax.legend(
77
+ loc="upper right",
78
+ bbox_to_anchor=(1, 1.1),
79
+ ncol=len(y_columns),
80
+ frameon=True,
81
+ )
82
+
83
+ if own_ax:
84
+ fig.tight_layout()
85
+ return fig
86
+ return ax
87
+
88
+
89
+ def plot_safety_margin(
90
+ df,
91
+ level_col: str,
92
+ constraint_value: float,
93
+ time_col: str = "time",
94
+ constraint_type: str = "upper",
95
+ title: str = "Safety Margin (Distance to Constraint)",
96
+ danger_threshold: float = None,
97
+ ax=None,
98
+ ):
99
+ """Plot level safety margin relative to upper or lower constraints."""
100
+ fig, ax, own_ax = _setup_axes(ax, (12, 6))
101
+
102
+ if constraint_type == "upper":
103
+ margin = constraint_value - df[level_col]
104
+ else:
105
+ margin = df[level_col] - constraint_value
106
+
107
+ ax.plot(df[time_col], margin, label="Safety Margin", color="steelblue", linewidth=2)
108
+ ax.axhline(
109
+ y=0,
110
+ color="red",
111
+ linestyle="-",
112
+ linewidth=1.5,
113
+ alpha=0.8,
114
+ label="Constraint Boundary",
115
+ )
116
+
117
+ if danger_threshold is not None:
118
+ ax.axhline(
119
+ y=danger_threshold,
120
+ color="orange",
121
+ linestyle="--",
122
+ linewidth=1,
123
+ alpha=0.7,
124
+ label=f"Danger Threshold ({danger_threshold})",
125
+ )
126
+ ax.fill_between(
127
+ df[time_col],
128
+ margin,
129
+ 0,
130
+ where=(margin < danger_threshold),
131
+ color="red",
132
+ alpha=0.15,
133
+ label="Danger Zone",
134
+ )
135
+
136
+ ax.fill_between(
137
+ df[time_col],
138
+ margin,
139
+ 0,
140
+ where=(margin < 0),
141
+ color="red",
142
+ alpha=0.3,
143
+ label="Constraint Violated",
144
+ )
145
+
146
+ ax.set_title(title, fontsize=14, pad=15)
147
+ ax.set_xlabel("Simulation Time", fontsize=12)
148
+ ax.set_ylabel("Margin (distance to constraint)", fontsize=12)
149
+ ax.legend(loc="best", frameon=True)
150
+
151
+ if own_ax:
152
+ fig.tight_layout()
153
+ return fig
154
+ return ax
155
+
156
+
157
+ def plot_dual_axis_step(
158
+ df,
159
+ y1_col: str,
160
+ y2_col: str,
161
+ y1_label: str = "Axis 1",
162
+ y2_label: str = "Axis 2",
163
+ y1_color: str = "saddlebrown",
164
+ y2_color: str = "darkorange",
165
+ time_col: str = "time",
166
+ title: str = "Dual Axis Step Plot",
167
+ ax=None,
168
+ ):
169
+ """Plot two steps on dual y-axes for scale-disparate variables."""
170
+ fig, ax, own_ax = _setup_axes(ax, DEFAULT_FIGSIZE)
171
+
172
+ if y1_col in df.columns:
173
+ line1 = ax.step(
174
+ df[time_col],
175
+ df[y1_col],
176
+ label=y1_label,
177
+ color=y1_color,
178
+ where="post",
179
+ linewidth=2,
180
+ )
181
+ ax.set_ylabel(y1_label, color=y1_color, fontsize=12)
182
+ ax.tick_params(axis="y", labelcolor=y1_color)
183
+ else:
184
+ line1 = []
185
+
186
+ if y2_col in df.columns:
187
+ ax_twin = ax.twinx()
188
+ line2 = ax_twin.step(
189
+ df[time_col],
190
+ df[y2_col],
191
+ label=y2_label,
192
+ color=y2_color,
193
+ where="post",
194
+ linewidth=2,
195
+ )
196
+ ax_twin.set_ylabel(y2_label, color=y2_color, fontsize=12)
197
+ ax_twin.tick_params(axis="y", labelcolor=y2_color)
198
+ else:
199
+ line2 = []
200
+
201
+ lines = line1 + line2
202
+ if lines:
203
+ labels = [l.get_label() for l in lines]
204
+ ax.legend(lines, labels, loc="upper right", bbox_to_anchor=(1.12, 1))
205
+
206
+ ax.set_title(title, fontsize=14, pad=15)
207
+ ax.grid(True)
208
+ ax.set_xlabel("Simulation Time", fontsize=12)
209
+
210
+ if own_ax:
211
+ fig.tight_layout()
212
+ return fig
213
+ return ax
214
+
215
+
216
+ def build_dashboard(df, plot_configs, title="Simulation Dashboard", figsize=(16, 20)):
217
+ """Assemble a multi-plot layout sharing x-axes where appropriate."""
218
+ num_plots = len(plot_configs)
219
+ fig = plt.figure(figsize=figsize)
220
+ gs = gridspec.GridSpec(num_plots, 1, figure=fig)
221
+
222
+ axes = []
223
+ time_ax = None
224
+ for i, config in enumerate(plot_configs):
225
+ func = config["func"]
226
+ is_time_series = config.get("is_time_series", func.__name__ not in [
227
+ "plot_mode_distribution",
228
+ "plot_mode_dwell_times",
229
+ "plot_normalized_deviation_violin",
230
+ "plot_deficit_disparity",
231
+ "plot_deficit_breakdown_pie",
232
+ "plot_deficit_breakdown_bar",
233
+ "plot_structural_vs_operational_by_mode",
234
+ ])
235
+
236
+ if is_time_series:
237
+ ax = fig.add_subplot(gs[i, 0], sharex=time_ax)
238
+ if time_ax is None:
239
+ time_ax = ax
240
+ else:
241
+ ax = fig.add_subplot(gs[i, 0])
242
+
243
+ axes.append(ax)
244
+
245
+ func = config["func"]
246
+ kwargs = config.get("kwargs", {})
247
+
248
+ func(df, ax=ax, **kwargs)
249
+
250
+ ax.tick_params(labelbottom=True)
251
+
252
+ fig.suptitle(title, fontsize=18, fontweight="bold", y=0.98)
253
+ fig.tight_layout(rect=[0, 0, 1, 0.97])
254
+ return fig
drs/serialize.py ADDED
@@ -0,0 +1,408 @@
1
+ import json
2
+ import math
3
+ import random
4
+ from typing import Any, Optional
5
+ from .module import Module
6
+ from .variables import serialize_val, deserialize_val
7
+
8
+
9
+ def save_state(model: Module, filepath: str) -> None:
10
+ state = model.state_dict()
11
+ with open(filepath, "w") as f:
12
+ json.dump(state, f, indent=2)
13
+
14
+
15
+ def load_state(model: Module, filepath: str) -> None:
16
+ with open(filepath, "r") as f:
17
+ state = json.load(f)
18
+ model.load_state_dict(state)
19
+
20
+
21
+ def export_architecture(model: Module, filepath: str) -> None:
22
+ arch = model.to_dict()
23
+ with open(filepath, "w") as f:
24
+ json.dump(arch, f, indent=2)
25
+
26
+
27
+ def _serialize_module_structure(model: Module) -> dict[str, Any]:
28
+ def _build_struct(mod):
29
+ serialized_hooks = []
30
+ for hook in mod._post_step_hooks:
31
+ if hasattr(hook, "__self__") and hasattr(hook, "__name__"):
32
+ obj = hook.__self__
33
+ serialized_hooks.append(f"{type(obj).__name__}.{hook.__name__}")
34
+ elif hasattr(hook, "__name__"):
35
+ serialized_hooks.append(hook.__name__)
36
+ else:
37
+ serialized_hooks.append(str(hook))
38
+
39
+ children = {}
40
+ for name, sub_mod in mod._modules.items():
41
+ children[name] = _build_struct(sub_mod)
42
+
43
+ variables = {}
44
+ for name, var in mod._variables.items():
45
+ variables[name] = type(var).__name__
46
+
47
+ attributes = {}
48
+ for k, v in mod.__dict__.items():
49
+ if k.startswith("_") or k in (
50
+ "parent",
51
+ "config",
52
+ "telemetry",
53
+ "_variables",
54
+ "_modules",
55
+ ):
56
+ continue
57
+ if isinstance(v, (int, float, str, bool, list, dict)) or v is None:
58
+ try:
59
+ json.dumps(v)
60
+ attributes[k] = json.loads(json.dumps(v))
61
+ except Exception:
62
+ pass
63
+
64
+ return {
65
+ "class": type(mod).__name__,
66
+ "variables": variables,
67
+ "hooks": serialized_hooks,
68
+ "attributes": attributes,
69
+ "children": children,
70
+ }
71
+
72
+ return _build_struct(model)
73
+
74
+
75
+ def _validate_structure(
76
+ current: dict[str, Any], saved: dict[str, Any], path: str = ""
77
+ ) -> None:
78
+ if current.get("class") != saved.get("class"):
79
+ raise ValueError(
80
+ f"Structural mismatch at '{path or 'root'}': class names do not match. "
81
+ f"Current: {current.get('class')}, Saved: {saved.get('class')}"
82
+ )
83
+ curr_vars = current.get("variables", {})
84
+ saved_vars = saved.get("variables", {})
85
+ if curr_vars != saved_vars:
86
+ raise ValueError(
87
+ f"Structural mismatch at '{path or 'root'}': variables do not match. "
88
+ f"Current: {curr_vars}, Saved: {saved_vars}"
89
+ )
90
+ curr_children = current.get("children", {})
91
+ saved_children = saved.get("children", {})
92
+ if set(curr_children.keys()) != set(saved_children.keys()):
93
+ raise ValueError(
94
+ f"Structural mismatch at '{path or 'root'}': children submodules do not match. "
95
+ f"Current keys: {list(curr_children.keys())}, Saved keys: {list(saved_children.keys())}"
96
+ )
97
+ for name in curr_children:
98
+ sub_path = f"{path}.{name}" if path else name
99
+ _validate_structure(curr_children[name], saved_children[name], sub_path)
100
+
101
+
102
+ def _restore_module_attributes(mod: Module, saved_struct: dict[str, Any]) -> None:
103
+ saved_attrs = saved_struct.get("attributes", {})
104
+ for k, v in saved_attrs.items():
105
+ setattr(mod, k, v)
106
+ curr_children = mod._modules
107
+ saved_children = saved_struct.get("children", {})
108
+ for name, child_mod in curr_children.items():
109
+ if name in saved_children:
110
+ _restore_module_attributes(child_mod, saved_children[name])
111
+
112
+
113
+ def _serialize_dependency_topology(model: Module) -> dict[str, Any]:
114
+ id_to_path = {id(mod): path for path, mod in model.named_modules()}
115
+ edges = []
116
+ seen = set()
117
+
118
+ for target_path, target_mod in model.named_modules():
119
+ for source_mod, variable in getattr(target_mod, "_dependencies", []):
120
+ source_path = id_to_path.get(id(source_mod))
121
+ if source_path is None:
122
+ continue
123
+ edge = {
124
+ "kind": "read",
125
+ "source": source_path,
126
+ "target": target_path,
127
+ "variable": variable.name,
128
+ }
129
+ edge_key = (edge["kind"], edge["source"], edge["target"], edge["variable"])
130
+ if edge_key not in seen:
131
+ seen.add(edge_key)
132
+ edges.append(edge)
133
+
134
+ for source_mod in getattr(target_mod, "_flow_dependencies", []):
135
+ source_path = id_to_path.get(id(source_mod))
136
+ if source_path is None:
137
+ continue
138
+ edge = {"kind": "flow", "source": source_path, "target": target_path}
139
+ edge_key = (edge["kind"], edge["source"], edge["target"], None)
140
+ if edge_key not in seen:
141
+ seen.add(edge_key)
142
+ edges.append(edge)
143
+
144
+ for source_mod in getattr(target_mod, "_data_dependencies", []):
145
+ source_path = id_to_path.get(id(source_mod))
146
+ if source_path is None:
147
+ continue
148
+ edge = {"kind": "data", "source": source_path, "target": target_path}
149
+ edge_key = (edge["kind"], edge["source"], edge["target"], None)
150
+ if edge_key not in seen:
151
+ seen.add(edge_key)
152
+ edges.append(edge)
153
+
154
+ return {"schema_version": 1, "edges": edges}
155
+
156
+
157
+ def _clear_dependency_registries(model: Module) -> None:
158
+ for _, mod in model.named_modules():
159
+ mod._dependencies = []
160
+ mod._dep_seen = set()
161
+ mod._flow_dependencies = []
162
+ mod._flow_dep_seen = set()
163
+ mod._data_dependencies = []
164
+ mod._data_dep_seen = set()
165
+
166
+
167
+ def _restore_dependency_topology(
168
+ model: Module, topology: Optional[dict[str, Any]]
169
+ ) -> None:
170
+ if not topology:
171
+ return
172
+ name_to_mod = {name: mod for name, mod in model.named_modules()}
173
+ _clear_dependency_registries(model)
174
+ for edge in topology.get("edges", []):
175
+ source_mod = name_to_mod.get(edge.get("source"))
176
+ target_mod = name_to_mod.get(edge.get("target"))
177
+ if source_mod is None or target_mod is None:
178
+ continue
179
+ kind = edge.get("kind")
180
+ if kind == "read":
181
+ variable_name = edge.get("variable")
182
+ variable = source_mod._variables.get(variable_name)
183
+ if variable is not None:
184
+ target_mod._record_incoming_edge(variable)
185
+ elif kind == "flow":
186
+ target_mod._record_flow_edge(source_mod)
187
+ elif kind == "data":
188
+ target_mod._record_data_edge(source_mod)
189
+
190
+
191
+ def save_checkpoint(engine: Any, filepath: str) -> None:
192
+ model = engine.model
193
+ id_to_name = {id(mod): name for name, mod in model.named_modules()}
194
+
195
+ variables_state = {}
196
+ for name, mod in model.named_modules():
197
+ for var_name, var in mod._variables.items():
198
+ var_path = f"{name}.{var_name}" if name else var_name
199
+ rate_set_by = None
200
+ if getattr(var, "_rate_set_by", None) is not None:
201
+ rate_set_by = id_to_name.get(id(var._rate_set_by))
202
+ var_state = {
203
+ "value": serialize_val(var._value),
204
+ "rate": serialize_val(var.rate) if hasattr(var, "rate") else None,
205
+ "upper_threshold": (
206
+ serialize_val(var.upper_threshold)
207
+ if hasattr(var, "upper_threshold")
208
+ else None
209
+ ),
210
+ "lower_threshold": (
211
+ serialize_val(var.lower_threshold)
212
+ if hasattr(var, "lower_threshold")
213
+ else None
214
+ ),
215
+ "_rate_set_by": rate_set_by,
216
+ }
217
+ variables_state[var_path] = var_state
218
+
219
+ python_rng = list(random.getstate())
220
+ python_rng[1] = list(python_rng[1])
221
+
222
+ numpy_rng = None
223
+ try:
224
+ import numpy as np
225
+
226
+ np_state = np.random.get_state()
227
+ numpy_rng = [
228
+ np_state[0],
229
+ np_state[1].tolist(),
230
+ np_state[2],
231
+ np_state[3],
232
+ np_state[4],
233
+ ]
234
+ except ImportError:
235
+ pass
236
+
237
+ telemetry_data = None
238
+ if engine.telemetry is not None:
239
+ serialized_history = []
240
+ for entry in engine.telemetry.history:
241
+ new_entry = {}
242
+ for k, v in entry.items():
243
+ new_entry[k] = serialize_val(v)
244
+ serialized_history.append(new_entry)
245
+
246
+ serialized_events = []
247
+ for e in engine.telemetry.events:
248
+ new_details = {}
249
+ for k, v in e.details.items():
250
+ new_details[k] = serialize_val(v)
251
+ serialized_events.append(
252
+ {
253
+ "time": e.time,
254
+ "event_type": e.event_type,
255
+ "source": e.source,
256
+ "details": new_details,
257
+ }
258
+ )
259
+ telemetry_data = {
260
+ "history": serialized_history,
261
+ "events": serialized_events,
262
+ "event_log_cursor": len(engine.telemetry.events),
263
+ "history_cursor": len(engine.telemetry.history),
264
+ }
265
+
266
+ checkpoint = {
267
+ "drs_version": "1.0",
268
+ "engine": {
269
+ "current_time": engine.current_time,
270
+ "step_count": getattr(engine, "step_count", 0),
271
+ "_consecutive_zero_dt_count": getattr(
272
+ engine, "_consecutive_zero_dt_count", 0
273
+ ),
274
+ "rng": {"python": python_rng, "numpy": numpy_rng},
275
+ "telemetry": telemetry_data,
276
+ },
277
+ "model_structure": _serialize_module_structure(model),
278
+ "topology": _serialize_dependency_topology(model),
279
+ "variables_state": variables_state,
280
+ }
281
+
282
+ with open(filepath, "w") as f:
283
+ json.dump(checkpoint, f, indent=2)
284
+
285
+
286
+ def load_checkpoint(engine: Any, filepath: str) -> None:
287
+ with open(filepath, "r") as f:
288
+ checkpoint = json.load(f)
289
+
290
+ model = engine.model
291
+
292
+ current_structure = _serialize_module_structure(model)
293
+ _validate_structure(current_structure, checkpoint["model_structure"])
294
+
295
+ _restore_module_attributes(model, checkpoint["model_structure"])
296
+
297
+ name_to_mod = {name: mod for name, mod in model.named_modules()}
298
+ variables_state = checkpoint["variables_state"]
299
+ for var_path, var_state in variables_state.items():
300
+ parts = var_path.split(".")
301
+ var_name = parts[-1]
302
+ mod_path = ".".join(parts[:-1])
303
+ mod = name_to_mod.get(mod_path)
304
+ if mod is None:
305
+ continue
306
+ var = mod._variables.get(var_name)
307
+ if var is None:
308
+ continue
309
+
310
+ restored_val = deserialize_val(var_state["value"])
311
+ if isinstance(restored_val, dict) and "__type__" in restored_val:
312
+ obj_name = restored_val.get("name")
313
+ reconstructed = False
314
+ if (
315
+ var._value is not None
316
+ and hasattr(var._value, "name")
317
+ and hasattr(var._value, "id")
318
+ ):
319
+ try:
320
+ var._value = type(var._value)(obj_name)
321
+ reconstructed = True
322
+ except Exception:
323
+ pass
324
+ if not reconstructed:
325
+ type_name = restored_val["__type__"]
326
+ import sys
327
+
328
+ for module in list(sys.modules.values()):
329
+ if module and hasattr(module, type_name):
330
+ try:
331
+ var._value = getattr(module, type_name)(obj_name)
332
+ reconstructed = True
333
+ break
334
+ except Exception:
335
+ continue
336
+ if not reconstructed:
337
+ var._value = obj_name if obj_name is not None else restored_val
338
+ else:
339
+ var._value = restored_val
340
+ if hasattr(var, "rate") and var_state["rate"] is not None:
341
+ var._rate = deserialize_val(var_state["rate"])
342
+ if hasattr(var, "upper_threshold") and var_state["upper_threshold"] is not None:
343
+ var.upper_threshold = deserialize_val(var_state["upper_threshold"])
344
+ if hasattr(var, "lower_threshold") and var_state["lower_threshold"] is not None:
345
+ var.lower_threshold = deserialize_val(var_state["lower_threshold"])
346
+
347
+ rate_set_by_name = var_state.get("_rate_set_by")
348
+ if rate_set_by_name is not None:
349
+ var._rate_set_by = name_to_mod.get(rate_set_by_name)
350
+ else:
351
+ if hasattr(var, "_rate_set_by"):
352
+ var._rate_set_by = None
353
+
354
+ engine_data = checkpoint["engine"]
355
+ engine.current_time = engine_data["current_time"]
356
+ engine.step_count = engine_data["step_count"]
357
+ engine._consecutive_zero_dt_count = engine_data["_consecutive_zero_dt_count"]
358
+ engine._resuming = True
359
+
360
+ rng_data = engine_data["rng"]
361
+ if rng_data.get("python") is not None:
362
+ p_rng = rng_data["python"]
363
+ p_rng_state = (p_rng[0], tuple(p_rng[1]), p_rng[2])
364
+ random.setstate(p_rng_state)
365
+ if rng_data.get("numpy") is not None:
366
+ try:
367
+ import numpy as np
368
+
369
+ np_list = rng_data["numpy"]
370
+ np_state = (
371
+ np_list[0],
372
+ np.array(np_list[1], dtype=np.uint32),
373
+ np_list[2],
374
+ np_list[3],
375
+ np_list[4],
376
+ )
377
+ np.random.set_state(np_state)
378
+ except ImportError:
379
+ pass
380
+
381
+ telemetry_data = engine_data.get("telemetry")
382
+ if telemetry_data is not None and engine.telemetry is not None:
383
+ from .telemetry import Event
384
+
385
+ deserialized_history = []
386
+ for entry in telemetry_data["history"]:
387
+ new_entry = {}
388
+ for k, v in entry.items():
389
+ new_entry[k] = deserialize_val(v)
390
+ deserialized_history.append(new_entry)
391
+ engine.telemetry.history = deserialized_history
392
+
393
+ deserialized_events = []
394
+ for e in telemetry_data["events"]:
395
+ new_details = {}
396
+ for k, v in e["details"].items():
397
+ new_details[k] = deserialize_val(v)
398
+ deserialized_events.append(
399
+ Event(
400
+ time=e["time"],
401
+ event_type=e["event_type"],
402
+ source=e["source"],
403
+ details=new_details,
404
+ )
405
+ )
406
+ engine.telemetry.events = deserialized_events
407
+
408
+ _restore_dependency_topology(model, checkpoint.get("topology"))