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/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)