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/__init__.py +49 -0
- drs/_execution_context.py +89 -0
- drs/callbacks.py +97 -0
- drs/config.py +25 -0
- drs/data_source.py +49 -0
- drs/engine.py +447 -0
- drs/exceptions.py +33 -0
- drs/flow.py +24 -0
- drs/module.py +497 -0
- drs/plot.py +254 -0
- drs/serialize.py +408 -0
- drs/telemetry.py +172 -0
- drs/variables.py +547 -0
- python_drs-0.1.0.dist-info/METADATA +106 -0
- python_drs-0.1.0.dist-info/RECORD +18 -0
- python_drs-0.1.0.dist-info/WHEEL +5 -0
- python_drs-0.1.0.dist-info/licenses/LICENSE +21 -0
- python_drs-0.1.0.dist-info/top_level.txt +1 -0
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"))
|