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/telemetry.py
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
from typing import Callable, Dict, Any, Optional
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# TODO: are we able to autopopulate and give more detail on things like thresholds or why mode changes are happening. i notice my logs in my examples look like:
|
|
7
|
+
# --- Mode Transition Log ---
|
|
8
|
+
# Time: 17.97 | Transition: MODE_A -> MODE_A_CONTINGENCY
|
|
9
|
+
# Time: 18.97 | Transition: MODE_A_CONTINGENCY -> MODE_A
|
|
10
|
+
# Time: 19.60 | Transition: MODE_A -> MODE_A_CONTINGENCY
|
|
11
|
+
@dataclass
|
|
12
|
+
class Event:
|
|
13
|
+
"""Represents a discrete semantic event in the simulation."""
|
|
14
|
+
|
|
15
|
+
time: float
|
|
16
|
+
event_type: str
|
|
17
|
+
source: str
|
|
18
|
+
details: dict[str, Any] = field(default_factory=dict)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
from .module import Module
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Telemetry:
|
|
25
|
+
"""Automates the recording of all simulation variables over time.
|
|
26
|
+
|
|
27
|
+
Provides methods to export the recorded history into analysis-ready formats.
|
|
28
|
+
|
|
29
|
+
NOTE: this is probably what we should be using to "make" our observations.
|
|
30
|
+
The telemetry data or sensor data in a way. For MDP just track all variables.
|
|
31
|
+
For POMDP only some of them.
|
|
32
|
+
|
|
33
|
+
Attributes:
|
|
34
|
+
model (Module): The root module being tracked.
|
|
35
|
+
history (list[dict]): The recorded history of states.
|
|
36
|
+
tracked_vars (Optional[list[str]]): The names of variables being tracked.
|
|
37
|
+
snapshot_condition (Optional[Callable[[float], bool]]): Condition to take a snapshot.
|
|
38
|
+
on_snapshot (Optional[Callable[[dict[str, Any]], None]]): Callback on snapshot.
|
|
39
|
+
group (Optional[str]): Categorize telemetry channels.
|
|
40
|
+
derived_metrics (Dict[str, Callable]): Custom metrics calculated at each step.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
model: Module,
|
|
46
|
+
tracked_vars: Optional[list[str]] = None,
|
|
47
|
+
snapshot_condition: Optional[Callable[[float], bool]] = None,
|
|
48
|
+
on_snapshot: Optional[Callable[[dict[str, Any]], None]] = None,
|
|
49
|
+
group: Optional[str] = None,
|
|
50
|
+
) -> None:
|
|
51
|
+
"""
|
|
52
|
+
Initializes the telemetry system attached to a specific model.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
model (Module): The root Module of your simulation.
|
|
56
|
+
tracked_vars (Optional[list[str]]): Filter which variables to track. If None, track all.
|
|
57
|
+
snapshot_condition (Optional[Callable[[float], bool]]): Lambda returning bool, snapshot only if True.
|
|
58
|
+
on_snapshot (Optional[Callable[[dict[str, Any]], None]]): Streaming hook for live dashboards.
|
|
59
|
+
group (Optional[str]): Categorize telemetry channels (e.g. "sensors").
|
|
60
|
+
"""
|
|
61
|
+
self.model = model
|
|
62
|
+
self.history: list[dict[str, Any]] = []
|
|
63
|
+
self.events: list[Event] = []
|
|
64
|
+
self.tracked_vars = tracked_vars
|
|
65
|
+
self.snapshot_condition = snapshot_condition
|
|
66
|
+
self.on_snapshot = on_snapshot
|
|
67
|
+
self.group = group
|
|
68
|
+
self.derived_metrics: Dict[
|
|
69
|
+
str, Callable[[float, Module, dict[str, Any], list[dict[str, Any]]], float]
|
|
70
|
+
] = {}
|
|
71
|
+
|
|
72
|
+
def register_metric(
|
|
73
|
+
self, name: str, calc_fn: Callable[[float, Module, dict, list], float]
|
|
74
|
+
):
|
|
75
|
+
"""Register a custom metric calculated dynamically at each time step.
|
|
76
|
+
|
|
77
|
+
Useful for tracking derived metrics like NPV, utilization, or efficiency.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
name (str): The name of the metric.
|
|
81
|
+
calc_fn (Callable): The metric function.
|
|
82
|
+
Signature: `calc_fn(current_time, model, state, history) -> float`
|
|
83
|
+
"""
|
|
84
|
+
self.derived_metrics[name] = calc_fn
|
|
85
|
+
|
|
86
|
+
def snapshot(self, current_time: float):
|
|
87
|
+
"""
|
|
88
|
+
Called automatically at the end of every simulation tick to record the state.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
current_time (float): The current simulation time.
|
|
92
|
+
"""
|
|
93
|
+
if self.snapshot_condition is not None and not self.snapshot_condition(
|
|
94
|
+
current_time
|
|
95
|
+
):
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
state = {"time": current_time}
|
|
99
|
+
|
|
100
|
+
for variable in self.model.variables():
|
|
101
|
+
if self.tracked_vars is None or variable.name in self.tracked_vars:
|
|
102
|
+
state[variable.name] = variable.value
|
|
103
|
+
|
|
104
|
+
for name, func in self.derived_metrics.items():
|
|
105
|
+
state[name] = func(current_time, self.model, state, self.history)
|
|
106
|
+
|
|
107
|
+
self.history.append(state)
|
|
108
|
+
|
|
109
|
+
if self.on_snapshot is not None:
|
|
110
|
+
self.on_snapshot(state)
|
|
111
|
+
|
|
112
|
+
def to_dataframe(self) -> pd.DataFrame:
|
|
113
|
+
"""
|
|
114
|
+
Converts the entire simulation history into a Pandas DataFrame.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
pd.DataFrame: A DataFrame where each row is a time step and columns
|
|
118
|
+
are tracked variables and derived metrics.
|
|
119
|
+
"""
|
|
120
|
+
return pd.DataFrame(self.history)
|
|
121
|
+
|
|
122
|
+
def log_event(
|
|
123
|
+
self, time: float, event_type: str, source: str, details: dict = None, **kwargs
|
|
124
|
+
) -> None:
|
|
125
|
+
"""
|
|
126
|
+
Log a discrete semantic event to the simulation's audit trail.
|
|
127
|
+
"""
|
|
128
|
+
_details = details or {}
|
|
129
|
+
_details.update(kwargs)
|
|
130
|
+
self.events.append(
|
|
131
|
+
Event(time=time, event_type=event_type, source=source, details=_details)
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
def filter_events(
|
|
135
|
+
self, type: Optional[str] = None, source: Optional[str] = None
|
|
136
|
+
) -> list[Event]:
|
|
137
|
+
"""
|
|
138
|
+
Return a filtered list of events.
|
|
139
|
+
"""
|
|
140
|
+
results = self.events
|
|
141
|
+
if type is not None:
|
|
142
|
+
results = [e for e in results if e.event_type == type]
|
|
143
|
+
if source is not None:
|
|
144
|
+
results = [e for e in results if e.source == source]
|
|
145
|
+
return results
|
|
146
|
+
|
|
147
|
+
def event_timeline(self) -> str:
|
|
148
|
+
"""
|
|
149
|
+
Returns a formatted string representation of the causal chain of events.
|
|
150
|
+
"""
|
|
151
|
+
lines = []
|
|
152
|
+
for e in self.events:
|
|
153
|
+
details_str = ", ".join(f"{k}={v}" for k, v in e.details.items())
|
|
154
|
+
lines.append(
|
|
155
|
+
f"t={e.time:<6.2f} | {e.event_type:<15} | [{e.source}] {details_str}"
|
|
156
|
+
)
|
|
157
|
+
return "\n".join(lines)
|
|
158
|
+
|
|
159
|
+
def export_events_csv(self, path: str) -> None:
|
|
160
|
+
"""
|
|
161
|
+
Converts the events list to a DataFrame and saves it to a CSV file.
|
|
162
|
+
"""
|
|
163
|
+
if not self.events:
|
|
164
|
+
return
|
|
165
|
+
|
|
166
|
+
data = []
|
|
167
|
+
for e in self.events:
|
|
168
|
+
row = {"time": e.time, "event_type": e.event_type, "source": e.source}
|
|
169
|
+
row.update(e.details)
|
|
170
|
+
data.append(row)
|
|
171
|
+
|
|
172
|
+
pd.DataFrame(data).to_csv(path, index=False)
|