python-drs 0.1.1__tar.gz → 0.1.2__tar.gz

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.
Files changed (31) hide show
  1. python_drs-0.1.2/PKG-INFO +182 -0
  2. python_drs-0.1.2/README.md +125 -0
  3. python_drs-0.1.2/drs/__init__.py +40 -0
  4. python_drs-0.1.2/drs/engine.py +128 -0
  5. python_drs-0.1.2/drs/module.py +106 -0
  6. python_drs-0.1.2/drs/plot.py +366 -0
  7. {python_drs-0.1.1 → python_drs-0.1.2}/drs/telemetry.py +12 -79
  8. python_drs-0.1.2/drs/variables.py +254 -0
  9. {python_drs-0.1.1 → python_drs-0.1.2}/pyproject.toml +18 -6
  10. python_drs-0.1.2/python_drs.egg-info/PKG-INFO +182 -0
  11. {python_drs-0.1.1 → python_drs-0.1.2}/python_drs.egg-info/SOURCES.txt +0 -7
  12. python_drs-0.1.1/PKG-INFO +0 -106
  13. python_drs-0.1.1/README.md +0 -51
  14. python_drs-0.1.1/drs/__init__.py +0 -49
  15. python_drs-0.1.1/drs/_execution_context.py +0 -89
  16. python_drs-0.1.1/drs/callbacks.py +0 -97
  17. python_drs-0.1.1/drs/config.py +0 -25
  18. python_drs-0.1.1/drs/data_source.py +0 -49
  19. python_drs-0.1.1/drs/engine.py +0 -447
  20. python_drs-0.1.1/drs/exceptions.py +0 -33
  21. python_drs-0.1.1/drs/flow.py +0 -24
  22. python_drs-0.1.1/drs/module.py +0 -497
  23. python_drs-0.1.1/drs/plot.py +0 -254
  24. python_drs-0.1.1/drs/serialize.py +0 -408
  25. python_drs-0.1.1/drs/variables.py +0 -547
  26. python_drs-0.1.1/python_drs.egg-info/PKG-INFO +0 -106
  27. {python_drs-0.1.1 → python_drs-0.1.2}/LICENSE +0 -0
  28. {python_drs-0.1.1 → python_drs-0.1.2}/python_drs.egg-info/dependency_links.txt +0 -0
  29. {python_drs-0.1.1 → python_drs-0.1.2}/python_drs.egg-info/requires.txt +0 -0
  30. {python_drs-0.1.1 → python_drs-0.1.2}/python_drs.egg-info/top_level.txt +0 -0
  31. {python_drs-0.1.1 → python_drs-0.1.2}/setup.cfg +0 -0
@@ -0,0 +1,182 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-drs
3
+ Version: 0.1.2
4
+ Summary: PyTorch-inspired, event-driven Discrete Rate Simulation (DRS) framework for fast, exact continuous-flow modeling in Python.
5
+ Author: Jonathan Lamontagne Kratz
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Jonathan Lamontagne Kratz
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/epicgamer17/python-drs
29
+ Project-URL: Repository, https://github.com/epicgamer17/python-drs
30
+ Project-URL: Documentation, https://python-drs.readthedocs.io/
31
+ Keywords: simulation,discrete-rate-simulation,drs,discrete-event-simulation,continuous-flow,event-driven,modeling,simulation-framework,python-simulation,system-dynamics,hybrid-simulation,process-simulation,power-systems,scientific-computing,pytorch,engineering
32
+ Classifier: Development Status :: 3 - Alpha
33
+ Classifier: Intended Audience :: Science/Research
34
+ Classifier: License :: OSI Approved :: MIT License
35
+ Classifier: Programming Language :: Python :: 3
36
+ Classifier: Programming Language :: Python :: 3.9
37
+ Classifier: Programming Language :: Python :: 3.10
38
+ Classifier: Programming Language :: Python :: 3.11
39
+ Classifier: Programming Language :: Python :: 3.12
40
+ Classifier: Topic :: Scientific/Engineering
41
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
42
+ Classifier: Topic :: Scientific/Engineering :: Physics
43
+ Requires-Python: >=3.9
44
+ Description-Content-Type: text/markdown
45
+ License-File: LICENSE
46
+ Requires-Dist: numpy>=1.21
47
+ Requires-Dist: pandas>=1.3
48
+ Requires-Dist: matplotlib>=3.4
49
+ Requires-Dist: seaborn>=0.12
50
+ Provides-Extra: progress
51
+ Requires-Dist: rich>=13.0; extra == "progress"
52
+ Provides-Extra: dev
53
+ Requires-Dist: build; extra == "dev"
54
+ Requires-Dist: twine; extra == "dev"
55
+ Requires-Dist: pytest; extra == "dev"
56
+ Dynamic: license-file
57
+
58
+ # python-drs — Discrete Rate Simulation (DRS) Framework for Python
59
+
60
+ [![PyPI version](https://img.shields.io/pypi/v/python-drs.svg)](https://pypi.org/project/python-drs/)
61
+ [![Python versions](https://img.shields.io/pypi/pyversions/python-drs.svg)](https://pypi.org/project/python-drs/)
62
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
63
+
64
+ **python-drs** is an open-source, PyTorch-inspired, **event-driven Discrete Rate Simulation (DRS)** framework for Python. It models systems where quantities flow continuously over time — water networks, chemical processing, electrical grids, energy storage, traffic, and supply chains — dramatically faster than traditional fixed-step simulation.
65
+
66
+ Discrete Rate Simulation is a hybrid of discrete-event simulation and continuous simulation: instead of ticking through time at fixed intervals, the engine calculates *exactly* when the next limit (threshold) will be reached, jumps the simulation clock to that precise moment, and triggers the matching state transition. The result is a Python simulation library that runs years of operation in a fraction of a second — and **never misses a limit**.
67
+
68
+ ---
69
+
70
+ ## Why Discrete Rate Simulation?
71
+
72
+ | Fixed-step simulation | Discrete Rate Simulation (DRS) |
73
+ |-----------------------|-------------------------------|
74
+ | Checks the system at every interval | Computes *exactly* when the next limit is hit |
75
+ | Can miss events between ticks | Never misses a limit |
76
+ | Slow for tight tolerances | Fast, event-driven time jumping |
77
+ | Threshold logic bolted on | Thresholds are first-class citizens |
78
+
79
+ Discrete Rate Simulation is ideal for hybrid systems where continuous physics (filling, draining, heating, discharging) meets discrete thresholds (full, empty, minimum, maximum, switch points).
80
+
81
+ ---
82
+
83
+ ## Key Features
84
+
85
+ - **Event-driven time stepping** — simulate years of operation in seconds by jumping directly from event to event
86
+ - **PyTorch-style architecture** — every model is a `Module` that owns `Variable`, `Level`, and `Timer` state
87
+ - **`@engine.on_step` control policies** — hook control logic into the loop to drive rates and thresholds every step
88
+ - **Continuous-flow modeling** — `Level`s accumulate quantity over time (like an integral in `dt`) and leap straight to their thresholds
89
+ - **Built-in telemetry** — every state is recorded as a pandas `DataFrame` and plotted without custom tracking code
90
+ - **Built-in components** — `Storage` and `Processor` give you ready-made tanks, stockpiles, and processing units
91
+ - **Door-to-door stream integration** — feed discrete data streams and iterators into continuous dynamics
92
+ - **NumPy/Pandas ecosystem** — first-class integration with the Python scientific stack
93
+ - **Pure Python** — works on Python 3.9+, no external solver required
94
+
95
+ ---
96
+
97
+ ## Installation
98
+
99
+ Install from PyPI:
100
+
101
+ ```bash
102
+ pip install python-drs
103
+ ```
104
+
105
+ ---
106
+
107
+ ## Quickstart
108
+
109
+ Model a tank that fills at a constant rate and watch the engine jump to the exact moment it overflows:
110
+
111
+ ```python
112
+ from drs import DRSEngine, Module, Level
113
+
114
+ class Tank(Module):
115
+ pass
116
+
117
+ model = Tank()
118
+ model.volume = Level("Volume", initial_value=100.0)
119
+
120
+ engine = DRSEngine()
121
+ engine.register(model)
122
+
123
+ @engine.on_step
124
+ def fill(policy_time):
125
+ # Fill at 50 units per time step
126
+ model.volume.rate = 50.0
127
+
128
+ result = engine.run(max_time=20.0)
129
+ print(result.summary())
130
+ ```
131
+
132
+ Learn the core concepts step by step: [Tutorial 1: Introduction to DRS](docs/tutorials/01_introduction.md).
133
+
134
+ ---
135
+
136
+ ## How the Engine Works
137
+
138
+ Every simulation follows the same repeating loop:
139
+
140
+ 1. **Run control policies** — each registered `@engine.on_step` handler computes the instantaneous rates and thresholds of the system.
141
+ 2. **Find the next event** — the engine calculates how long until any `Level` crosses one of its thresholds.
142
+ 3. **Jump time** — the simulation clock advances by exactly that amount, and all levels are integrated forward.
143
+ 4. **Repeat.**
144
+
145
+ Because time jumps from event to event rather than advancing at fixed steps, python-drs scales to long-horizon problems that are intractable with naive fixed-step solvers.
146
+
147
+ ---
148
+
149
+ ## Use Cases
150
+
151
+ - **Water networks & hydraulics** — storage tanks, reservoirs, pumping stations, pipe flow
152
+ - **Chemical & process engineering** — reactors, tanks, batch processes, separations
153
+ - **Electrical grids & energy storage** — charge/discharge cycles, grid balancing, batteries
154
+ - **Supply chains & logistics** — inventory, buffer stock, material flow, demand shocks
155
+ - **Manufacturing** — production lines, work-in-progress, equipment states
156
+ - **Traffic & transportation** — queue accumulation, congestion thresholds
157
+
158
+ If your system is best described by **continuous flow crossing discrete thresholds**, it is a Discrete Rate Simulation — and python-drs is the Python library built for it.
159
+
160
+ ---
161
+
162
+ ## Documentation
163
+
164
+ [![Documentation](https://readthedocs.org/projects/python-drs/badge/?version=latest)](https://python-drs.readthedocs.io/)
165
+
166
+ Full guides, tutorials, and API reference are available at [https://python-drs.readthedocs.io/](https://python-drs.readthedocs.io/):
167
+
168
+ - [Tutorial 1: Introduction to DRS](docs/tutorials/01_introduction.md)
169
+ - [Tutorial 2: Event-Driven Time Jumping](docs/tutorials/02_advanced_dynamics.md)
170
+ - [Tutorial 3: Streaming Inputs & Data Sources](docs/tutorials/03_data_streams.md)
171
+ - [Tutorial 4: Telemetry & Control Policies](docs/tutorials/05_telemetry_callbacks.md)
172
+ - [Tutorial 5: Design Patterns: Operating Modes](docs/tutorials/06_operating_modes.md)
173
+
174
+ ---
175
+
176
+ ## Related
177
+
178
+ Looking for a Python alternative to discrete-rate simulation approaches in Simulink® or Modelica? python-drs brings PyTorch-like ergonomics to **event-driven, continuous-flow simulation** and lives on [PyPI](https://pypi.org/project/python-drs/).
179
+
180
+ ## License
181
+
182
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,125 @@
1
+ # python-drs — Discrete Rate Simulation (DRS) Framework for Python
2
+
3
+ [![PyPI version](https://img.shields.io/pypi/v/python-drs.svg)](https://pypi.org/project/python-drs/)
4
+ [![Python versions](https://img.shields.io/pypi/pyversions/python-drs.svg)](https://pypi.org/project/python-drs/)
5
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
6
+
7
+ **python-drs** is an open-source, PyTorch-inspired, **event-driven Discrete Rate Simulation (DRS)** framework for Python. It models systems where quantities flow continuously over time — water networks, chemical processing, electrical grids, energy storage, traffic, and supply chains — dramatically faster than traditional fixed-step simulation.
8
+
9
+ Discrete Rate Simulation is a hybrid of discrete-event simulation and continuous simulation: instead of ticking through time at fixed intervals, the engine calculates *exactly* when the next limit (threshold) will be reached, jumps the simulation clock to that precise moment, and triggers the matching state transition. The result is a Python simulation library that runs years of operation in a fraction of a second — and **never misses a limit**.
10
+
11
+ ---
12
+
13
+ ## Why Discrete Rate Simulation?
14
+
15
+ | Fixed-step simulation | Discrete Rate Simulation (DRS) |
16
+ |-----------------------|-------------------------------|
17
+ | Checks the system at every interval | Computes *exactly* when the next limit is hit |
18
+ | Can miss events between ticks | Never misses a limit |
19
+ | Slow for tight tolerances | Fast, event-driven time jumping |
20
+ | Threshold logic bolted on | Thresholds are first-class citizens |
21
+
22
+ Discrete Rate Simulation is ideal for hybrid systems where continuous physics (filling, draining, heating, discharging) meets discrete thresholds (full, empty, minimum, maximum, switch points).
23
+
24
+ ---
25
+
26
+ ## Key Features
27
+
28
+ - **Event-driven time stepping** — simulate years of operation in seconds by jumping directly from event to event
29
+ - **PyTorch-style architecture** — every model is a `Module` that owns `Variable`, `Level`, and `Timer` state
30
+ - **`@engine.on_step` control policies** — hook control logic into the loop to drive rates and thresholds every step
31
+ - **Continuous-flow modeling** — `Level`s accumulate quantity over time (like an integral in `dt`) and leap straight to their thresholds
32
+ - **Built-in telemetry** — every state is recorded as a pandas `DataFrame` and plotted without custom tracking code
33
+ - **Built-in components** — `Storage` and `Processor` give you ready-made tanks, stockpiles, and processing units
34
+ - **Door-to-door stream integration** — feed discrete data streams and iterators into continuous dynamics
35
+ - **NumPy/Pandas ecosystem** — first-class integration with the Python scientific stack
36
+ - **Pure Python** — works on Python 3.9+, no external solver required
37
+
38
+ ---
39
+
40
+ ## Installation
41
+
42
+ Install from PyPI:
43
+
44
+ ```bash
45
+ pip install python-drs
46
+ ```
47
+
48
+ ---
49
+
50
+ ## Quickstart
51
+
52
+ Model a tank that fills at a constant rate and watch the engine jump to the exact moment it overflows:
53
+
54
+ ```python
55
+ from drs import DRSEngine, Module, Level
56
+
57
+ class Tank(Module):
58
+ pass
59
+
60
+ model = Tank()
61
+ model.volume = Level("Volume", initial_value=100.0)
62
+
63
+ engine = DRSEngine()
64
+ engine.register(model)
65
+
66
+ @engine.on_step
67
+ def fill(policy_time):
68
+ # Fill at 50 units per time step
69
+ model.volume.rate = 50.0
70
+
71
+ result = engine.run(max_time=20.0)
72
+ print(result.summary())
73
+ ```
74
+
75
+ Learn the core concepts step by step: [Tutorial 1: Introduction to DRS](docs/tutorials/01_introduction.md).
76
+
77
+ ---
78
+
79
+ ## How the Engine Works
80
+
81
+ Every simulation follows the same repeating loop:
82
+
83
+ 1. **Run control policies** — each registered `@engine.on_step` handler computes the instantaneous rates and thresholds of the system.
84
+ 2. **Find the next event** — the engine calculates how long until any `Level` crosses one of its thresholds.
85
+ 3. **Jump time** — the simulation clock advances by exactly that amount, and all levels are integrated forward.
86
+ 4. **Repeat.**
87
+
88
+ Because time jumps from event to event rather than advancing at fixed steps, python-drs scales to long-horizon problems that are intractable with naive fixed-step solvers.
89
+
90
+ ---
91
+
92
+ ## Use Cases
93
+
94
+ - **Water networks & hydraulics** — storage tanks, reservoirs, pumping stations, pipe flow
95
+ - **Chemical & process engineering** — reactors, tanks, batch processes, separations
96
+ - **Electrical grids & energy storage** — charge/discharge cycles, grid balancing, batteries
97
+ - **Supply chains & logistics** — inventory, buffer stock, material flow, demand shocks
98
+ - **Manufacturing** — production lines, work-in-progress, equipment states
99
+ - **Traffic & transportation** — queue accumulation, congestion thresholds
100
+
101
+ If your system is best described by **continuous flow crossing discrete thresholds**, it is a Discrete Rate Simulation — and python-drs is the Python library built for it.
102
+
103
+ ---
104
+
105
+ ## Documentation
106
+
107
+ [![Documentation](https://readthedocs.org/projects/python-drs/badge/?version=latest)](https://python-drs.readthedocs.io/)
108
+
109
+ Full guides, tutorials, and API reference are available at [https://python-drs.readthedocs.io/](https://python-drs.readthedocs.io/):
110
+
111
+ - [Tutorial 1: Introduction to DRS](docs/tutorials/01_introduction.md)
112
+ - [Tutorial 2: Event-Driven Time Jumping](docs/tutorials/02_advanced_dynamics.md)
113
+ - [Tutorial 3: Streaming Inputs & Data Sources](docs/tutorials/03_data_streams.md)
114
+ - [Tutorial 4: Telemetry & Control Policies](docs/tutorials/05_telemetry_callbacks.md)
115
+ - [Tutorial 5: Design Patterns: Operating Modes](docs/tutorials/06_operating_modes.md)
116
+
117
+ ---
118
+
119
+ ## Related
120
+
121
+ Looking for a Python alternative to discrete-rate simulation approaches in Simulink® or Modelica? python-drs brings PyTorch-like ergonomics to **event-driven, continuous-flow simulation** and lives on [PyPI](https://pypi.org/project/python-drs/).
122
+
123
+ ## License
124
+
125
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,40 @@
1
+ import logging
2
+
3
+ __version__ = "0.1.2"
4
+
5
+ # Configure a NullHandler to prevent "No handler found" warnings
6
+ # Users of the library can configure their own logging handlers
7
+ logging.getLogger(__name__).addHandler(logging.NullHandler())
8
+
9
+ from .module import Module
10
+ from .components import Storage, Processor
11
+ from .engine import DRSEngine, SimResult
12
+ from .variables import Variable, Level, Timer
13
+ from .telemetry import Telemetry
14
+ from .plot import (
15
+ Dashboard,
16
+ plot_series,
17
+ plot_time_series,
18
+ plot_safety_margin,
19
+ plot_dual_axis_step,
20
+ apply_plot_style,
21
+ )
22
+
23
+ __all__ = [
24
+ "DRSEngine",
25
+ "SimResult",
26
+ "Variable",
27
+ "Level",
28
+ "Timer",
29
+ "Module",
30
+ "Storage",
31
+ "Processor",
32
+ "Telemetry",
33
+ "Dashboard",
34
+ "plot_series",
35
+ "plot_time_series",
36
+ "plot_safety_margin",
37
+ "plot_dual_axis_step",
38
+ "apply_plot_style",
39
+ ]
40
+
@@ -0,0 +1,128 @@
1
+ from dataclasses import dataclass
2
+ from typing import Any, Callable, List, Optional
3
+
4
+
5
+ @dataclass
6
+ class SimResult:
7
+ """Encapsulates the final results of a simulation run."""
8
+
9
+ steps: int
10
+ duration: float
11
+ history: Optional[Any] = None
12
+ terminated_reason: str = "until_reached"
13
+
14
+ def summary(self) -> str:
15
+ """Returns a string summary of the simulation run."""
16
+ lines = [
17
+ "--- Simulation Summary ---",
18
+ f"Termination Reason : {self.terminated_reason}",
19
+ f"Simulated Time : {self.duration:.2f}",
20
+ f"Engine Steps : {self.steps:,}",
21
+ ]
22
+ if self.history is not None:
23
+ lines.append(f"Telemetry Records : {len(self.history):,}")
24
+ return "\n".join(lines)
25
+
26
+
27
+ class DRSEngine:
28
+
29
+ def __init__(self, max_step_size: float = 1.0):
30
+ self.max_step_size = float(max_step_size)
31
+ self.components: List[Any] = []
32
+ self._step_handlers: List[Callable[[float], None]] = []
33
+ self.telemetry: Optional[Any] = None
34
+ self.current_time: float = 0.0
35
+
36
+ def register(self, *components: Any) -> None:
37
+ """Registers components to be simulated."""
38
+ for comp in components:
39
+ if comp not in self.components:
40
+ self.components.append(comp)
41
+
42
+ def attach_telemetry(self, telemetry: Any) -> None:
43
+ """Attach a Telemetry object to the engine."""
44
+ self.telemetry = telemetry
45
+
46
+ def variables(self):
47
+ """Yield every variable owned by the registered components.
48
+
49
+ Lets ``Telemetry(model=engine)`` record the full state of the flat
50
+ leaf components without passing a wrapper module.
51
+ """
52
+ seen = set()
53
+ for comp in self.components:
54
+ for variable in comp.variables():
55
+ if id(variable) not in seen:
56
+ seen.add(id(variable))
57
+ yield variable
58
+
59
+ def on_step(self, fn: Callable[[float], None]) -> Callable[[float], None]:
60
+ """Decorator to register a control policy callback."""
61
+ self._step_handlers.append(fn)
62
+ return fn
63
+
64
+ def run(
65
+ self, until: Optional[float] = None, max_time: Optional[float] = None
66
+ ) -> SimResult:
67
+ if until is None:
68
+ if max_time is not None:
69
+ until = max_time
70
+ else:
71
+ raise ValueError("Must specify 'until' or 'max_time'")
72
+ current_time = 0.0
73
+ self.current_time = current_time
74
+ steps = 0
75
+
76
+ terminated_reason = "until_reached"
77
+
78
+ if self.telemetry is not None:
79
+ self.telemetry.snapshot(current_time)
80
+
81
+ while current_time < until:
82
+ self.current_time = current_time
83
+
84
+ # Check if terminating condition is met
85
+ terminated = False
86
+ for comp in self.components:
87
+ if comp.is_terminating_condition_met():
88
+ terminated = True
89
+ break
90
+ if terminated:
91
+ terminated_reason = "condition_met"
92
+ break
93
+
94
+ # 1. Execute control policies at time t
95
+ for handler in self._step_handlers:
96
+ handler(current_time)
97
+
98
+ # 2. Determine time to next state boundary across all components
99
+ dt = self.max_step_size
100
+
101
+ # Don't step past simulation end
102
+ if current_time + dt > until:
103
+ dt = until - current_time
104
+
105
+ for comp in self.components:
106
+ event_dt = comp.time_to_event()
107
+ if event_dt >= 0.0:
108
+ dt = min(dt, event_dt)
109
+
110
+ # 3. Tell each component to step forward by dt
111
+ for comp in self.components:
112
+ comp.step(dt)
113
+
114
+ current_time += dt
115
+ self.current_time = current_time
116
+ steps += 1
117
+
118
+ if self.telemetry is not None:
119
+ self.telemetry.snapshot(current_time)
120
+
121
+ df = self.telemetry.to_dataframe() if self.telemetry else None
122
+
123
+ return SimResult(
124
+ steps=steps,
125
+ duration=current_time,
126
+ history=df,
127
+ terminated_reason=terminated_reason,
128
+ )
@@ -0,0 +1,106 @@
1
+ import math
2
+ from typing import Iterator, Any
3
+ from .variables import Variable, Level
4
+
5
+
6
+ class Module:
7
+ """Base class for all DRS models and sub-components.
8
+
9
+ Modules are simple, modular components of a simulation.
10
+ """
11
+
12
+ def __init__(self) -> None:
13
+ """Initialize the module."""
14
+ self.parent = None
15
+
16
+ def __setattr__(self, name: str, value: Any) -> None:
17
+ super().__setattr__(name, value)
18
+ if name != "parent" and isinstance(value, Module):
19
+ value.parent = self
20
+
21
+ def variables(self) -> Iterator[Variable]:
22
+ """Recursively yield all variables owned by this module and its sub-components."""
23
+ seen = set()
24
+
25
+ def _find_vars(obj):
26
+ if id(obj) in seen:
27
+ return
28
+ seen.add(id(obj))
29
+
30
+ if isinstance(obj, Variable):
31
+ yield obj
32
+ return
33
+
34
+ dict_items = obj.__dict__.items()
35
+ for k, v in dict_items:
36
+ if isinstance(v, Variable):
37
+ if id(v) not in seen:
38
+ seen.add(id(v))
39
+ yield v
40
+ elif (
41
+ hasattr(v, "__dict__")
42
+ and not k.startswith("_")
43
+ and k != "parent"
44
+ and not isinstance(
45
+ v, (int, float, str, bool, list, dict, set, tuple)
46
+ )
47
+ ):
48
+ yield from _find_vars(v)
49
+
50
+ yield from _find_vars(self)
51
+
52
+ def modules(self) -> Iterator["Module"]:
53
+ """Recursively yield this module and all nested sub-modules."""
54
+ for _, module in self.named_modules():
55
+ yield module
56
+
57
+ def named_modules(self, prefix: str = "") -> Iterator[tuple[str, "Module"]]:
58
+ """Recursively yield (path, module) pairs using dot-separated attribute paths."""
59
+ seen = set()
60
+
61
+ def _get_modules(module, module_prefix):
62
+ module_id = id(module)
63
+ if module_id in seen:
64
+ return
65
+ seen.add(module_id)
66
+ yield module_prefix, module
67
+
68
+ for k, v in module.__dict__.items():
69
+ if k.startswith("_") or k == "parent":
70
+ continue
71
+ if isinstance(v, Module):
72
+ sub_prefix = k if not module_prefix else f"{module_prefix}.{k}"
73
+ yield from _get_modules(v, sub_prefix)
74
+
75
+ yield from _get_modules(self, prefix)
76
+
77
+ def _owned_levels(self) -> Iterator[Level]:
78
+ """Yield Level instances held directly by this module (non-recursive).
79
+
80
+ Unlike :meth:`variables`, this does not descend into nested modules, so it
81
+ is safe to use for registering the stateful leaves owned by a component.
82
+ """
83
+ seen = set()
84
+ for v in self.__dict__.values():
85
+ if isinstance(v, Level) and id(v) not in seen:
86
+ seen.add(id(v))
87
+ yield v
88
+
89
+ def time_to_event(self) -> float:
90
+ """Determines time to next state boundary across all variables in this module based on current rates."""
91
+ min_dt = math.inf
92
+ for var in self.variables():
93
+ dt_for_var = var.time_to_event()
94
+ if 0.0 <= dt_for_var < min_dt:
95
+ min_dt = dt_for_var
96
+
97
+ return min_dt
98
+
99
+ def step(self, dt: float) -> None:
100
+ """Steps all variables in this module forward by dt."""
101
+ for var in self.variables():
102
+ var.step(dt)
103
+
104
+ def is_terminating_condition_met(self) -> bool:
105
+ """Override this to define custom stopping conditions."""
106
+ return False