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/__init__.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.0"
|
|
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, DataSource
|
|
10
|
+
from .engine import DRSEngine, SimulationResult
|
|
11
|
+
from .variables import Variable, Level, Timer, Expression
|
|
12
|
+
from .data_source import DataPoint
|
|
13
|
+
from .flow import Flow
|
|
14
|
+
from .telemetry import Telemetry
|
|
15
|
+
from .exceptions import StateMutationError, DeadlockError
|
|
16
|
+
from .config import DRSConfig, EngineConfig
|
|
17
|
+
from .callbacks import Callback, ProgressBarCallback
|
|
18
|
+
from .serialize import (
|
|
19
|
+
save_state,
|
|
20
|
+
load_state,
|
|
21
|
+
export_architecture,
|
|
22
|
+
save_checkpoint,
|
|
23
|
+
load_checkpoint,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"DRSEngine",
|
|
28
|
+
"SimulationResult",
|
|
29
|
+
"Callback",
|
|
30
|
+
"ProgressBarCallback",
|
|
31
|
+
"Variable",
|
|
32
|
+
"Level",
|
|
33
|
+
"Timer",
|
|
34
|
+
"Expression",
|
|
35
|
+
"DataPoint",
|
|
36
|
+
"DataSource",
|
|
37
|
+
"Module",
|
|
38
|
+
"Flow",
|
|
39
|
+
"Telemetry",
|
|
40
|
+
"StateMutationError",
|
|
41
|
+
"DeadlockError",
|
|
42
|
+
"DRSConfig",
|
|
43
|
+
"EngineConfig",
|
|
44
|
+
"save_state",
|
|
45
|
+
"load_state",
|
|
46
|
+
"export_architecture",
|
|
47
|
+
"save_checkpoint",
|
|
48
|
+
"load_checkpoint",
|
|
49
|
+
]
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ExecutionContext:
|
|
5
|
+
"""
|
|
6
|
+
[INTERNAL] Thread-local context tracking active modules and engines during evaluation.
|
|
7
|
+
|
|
8
|
+
Power User Note: Enforces state ownership and builds the dependency graph implicitly
|
|
9
|
+
as the simulation steps.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
_local = threading.local()
|
|
13
|
+
|
|
14
|
+
@classmethod
|
|
15
|
+
def push(cls, module):
|
|
16
|
+
"""
|
|
17
|
+
[INTERNAL] Push a module onto the active execution stack.
|
|
18
|
+
|
|
19
|
+
Power User Note: Used when evaluating a module's forward pass to record active scope.
|
|
20
|
+
"""
|
|
21
|
+
if not hasattr(cls._local, "stack"):
|
|
22
|
+
cls._local.stack = []
|
|
23
|
+
cls._local.flow_edges = []
|
|
24
|
+
cls._local.stack.append(module)
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def set_tracing(cls, enabled: bool = True):
|
|
28
|
+
"""
|
|
29
|
+
[INTERNAL] Toggle symbolic tracing mode.
|
|
30
|
+
|
|
31
|
+
Power User Note: Enables AST expression recording instead of eager evaluation.
|
|
32
|
+
"""
|
|
33
|
+
cls._local.tracing = enabled
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def is_tracing(cls) -> bool:
|
|
37
|
+
"""
|
|
38
|
+
[INTERNAL] Check if symbolic tracing mode is active.
|
|
39
|
+
|
|
40
|
+
Power User Note: Used by Variable to decide whether to return expressions.
|
|
41
|
+
"""
|
|
42
|
+
return getattr(cls._local, "tracing", False)
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def pop(cls):
|
|
46
|
+
"""
|
|
47
|
+
[INTERNAL] Pop the top module from the execution stack.
|
|
48
|
+
|
|
49
|
+
Power User Note: Restores the caller's context after a module's execution completes.
|
|
50
|
+
"""
|
|
51
|
+
cls._local.stack.pop()
|
|
52
|
+
|
|
53
|
+
@classmethod
|
|
54
|
+
def get_current(cls):
|
|
55
|
+
"""
|
|
56
|
+
[INTERNAL] Retrieve the currently executing module.
|
|
57
|
+
|
|
58
|
+
Power User Note: Returns the module at the top of the stack, or None if executing externally.
|
|
59
|
+
"""
|
|
60
|
+
stack = getattr(cls._local, "stack", [])
|
|
61
|
+
return stack[-1] if stack else None
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
def set_engine(cls, engine):
|
|
65
|
+
"""
|
|
66
|
+
[INTERNAL] Bind the current DRSEngine to the context.
|
|
67
|
+
|
|
68
|
+
Power User Note: Used to provide modules access to simulation clock and configs.
|
|
69
|
+
"""
|
|
70
|
+
cls._local.engine = engine
|
|
71
|
+
|
|
72
|
+
@classmethod
|
|
73
|
+
def get_engine(cls):
|
|
74
|
+
"""
|
|
75
|
+
[INTERNAL] Retrieve the currently active DRSEngine.
|
|
76
|
+
|
|
77
|
+
Power User Note: Accesses thread-local storage to retrieve the running engine instance.
|
|
78
|
+
"""
|
|
79
|
+
return getattr(cls._local, "engine", None)
|
|
80
|
+
|
|
81
|
+
@classmethod
|
|
82
|
+
def record_flow_edge(cls, source, target):
|
|
83
|
+
"""
|
|
84
|
+
[INTERNAL] Record a transient flow edge between a source and target module.
|
|
85
|
+
"""
|
|
86
|
+
if source is not None:
|
|
87
|
+
if not hasattr(cls._local, "flow_edges"):
|
|
88
|
+
cls._local.flow_edges = []
|
|
89
|
+
cls._local.flow_edges.append((source, target))
|
drs/callbacks.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING, Optional
|
|
2
|
+
import time
|
|
3
|
+
|
|
4
|
+
if TYPE_CHECKING:
|
|
5
|
+
from .engine import DRSEngine, SimulationResult
|
|
6
|
+
from .variables import Variable
|
|
7
|
+
|
|
8
|
+
# TODO: Should this file be a part of the public api?
|
|
9
|
+
# TODO: Should we replace the "forward" system with a callback system on the levels?
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Callback:
|
|
13
|
+
"""Base class for DRS Engine callbacks.
|
|
14
|
+
|
|
15
|
+
Subclass this to hook into the simulation lifecycle.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def on_simulation_start(self, engine: "DRSEngine") -> None:
|
|
19
|
+
"""Called before the simulation loop begins."""
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
def on_step_start(self, engine: "DRSEngine") -> None:
|
|
23
|
+
"""Called at the beginning of each simulation step, after rates are zeroed and models evaluated."""
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
def on_threshold(
|
|
27
|
+
self, engine: "DRSEngine", trigger_var: "Variable", is_upper: bool
|
|
28
|
+
) -> None:
|
|
29
|
+
"""Called when a variable's threshold is the trigger for the next time step."""
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
def on_deadlock(self, engine: "DRSEngine") -> None:
|
|
33
|
+
"""Called immediately before a DeadlockError is raised."""
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
def on_complete(self, engine: "DRSEngine", result: "SimulationResult") -> None:
|
|
37
|
+
"""Called when the simulation loop has completely finished."""
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ProgressBarCallback(Callback):
|
|
42
|
+
"""A built-in callback that uses rich to display a progress bar for the simulation."""
|
|
43
|
+
|
|
44
|
+
def __init__(self):
|
|
45
|
+
try:
|
|
46
|
+
from rich.progress import (
|
|
47
|
+
Progress,
|
|
48
|
+
TextColumn,
|
|
49
|
+
BarColumn,
|
|
50
|
+
TaskProgressColumn,
|
|
51
|
+
TimeElapsedColumn,
|
|
52
|
+
TimeRemainingColumn,
|
|
53
|
+
)
|
|
54
|
+
except ImportError:
|
|
55
|
+
raise ImportError(
|
|
56
|
+
"The 'rich' package is required for the ProgressBarCallback. Install it with 'pip install rich'."
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
self.progress = Progress(
|
|
60
|
+
TextColumn("[progress.description]{task.description}"),
|
|
61
|
+
BarColumn(),
|
|
62
|
+
TaskProgressColumn(),
|
|
63
|
+
TimeElapsedColumn(),
|
|
64
|
+
TimeRemainingColumn(),
|
|
65
|
+
redirect_stdout=False,
|
|
66
|
+
redirect_stderr=False,
|
|
67
|
+
)
|
|
68
|
+
self.task_id = None
|
|
69
|
+
self.max_time = None
|
|
70
|
+
|
|
71
|
+
def on_simulation_start(self, engine: "DRSEngine") -> None:
|
|
72
|
+
self.progress.start()
|
|
73
|
+
# the engine doesn't technically know its max_time until run() is called,
|
|
74
|
+
# but max_time is passed to run(). We can extract it if we attach it to the engine temporarily,
|
|
75
|
+
# or we just assume we'll get it from engine if we store it.
|
|
76
|
+
# Wait, DRSEngine.run(max_time) takes max_time. So it's not known here unless we store it on the engine.
|
|
77
|
+
# Let's assume engine._current_max_time is set before calling this.
|
|
78
|
+
self.max_time = getattr(engine, "_current_max_time", None)
|
|
79
|
+
|
|
80
|
+
if self.max_time:
|
|
81
|
+
self.task_id = self.progress.add_task(
|
|
82
|
+
"[cyan]Simulating...", total=self.max_time
|
|
83
|
+
)
|
|
84
|
+
else:
|
|
85
|
+
self.task_id = self.progress.add_task("[cyan]Simulating...", total=None)
|
|
86
|
+
|
|
87
|
+
def on_step_start(self, engine: "DRSEngine") -> None:
|
|
88
|
+
if self.task_id is not None and self.max_time:
|
|
89
|
+
self.progress.update(self.task_id, completed=engine.current_time)
|
|
90
|
+
|
|
91
|
+
def on_complete(self, engine: "DRSEngine", result: "SimulationResult") -> None:
|
|
92
|
+
if self.task_id is not None and self.max_time:
|
|
93
|
+
self.progress.update(self.task_id, completed=self.max_time)
|
|
94
|
+
self.progress.stop()
|
|
95
|
+
|
|
96
|
+
def on_deadlock(self, engine: "DRSEngine") -> None:
|
|
97
|
+
self.progress.stop()
|
drs/config.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import Any, Dict
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class DRSConfig:
|
|
7
|
+
"""Base configuration class for DRS Modules.
|
|
8
|
+
|
|
9
|
+
Subclass this using @dataclass to define strictly-typed configuration
|
|
10
|
+
blocks for your models, allowing IDE autocomplete and clear parameterization.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class EngineConfig(DRSConfig):
|
|
18
|
+
"""Configuration for the DRS Engine."""
|
|
19
|
+
|
|
20
|
+
max_step_size: float = (
|
|
21
|
+
0.5 # TODO: should change this to float("inf") but for parity dont, it seems to cause behaviour changes, theory is that its because of precision propogation over longer time horizons. Why do we even need a max step size?
|
|
22
|
+
)
|
|
23
|
+
max_deadlock_steps: int = 20
|
|
24
|
+
max_time: float = None
|
|
25
|
+
strict_mode: bool = False
|
drs/data_source.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Generic data point for data yielded by DataSource.
|
|
5
|
+
|
|
6
|
+
Note on alternatives:
|
|
7
|
+
If you prefer standard Python semantics, you could potentially bypass `DataSource` entirely
|
|
8
|
+
and use a plain Python generator (e.g. `yield DataPoint(...)`). However, the `DataSource`
|
|
9
|
+
class is retained to seamlessly integrate with `Module`, making it a first-class citizen
|
|
10
|
+
in the simulation graph and tracking execution contexts and telemetry.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DataPoint:
|
|
15
|
+
"""A single batch of data yielded by a DataSource.
|
|
16
|
+
|
|
17
|
+
Fields are accessed as attributes. Any keyword argument passed to
|
|
18
|
+
``__init__`` is stored and accessible via ``.name``::
|
|
19
|
+
|
|
20
|
+
point = DataPoint(weight=40000.0, priority=1)
|
|
21
|
+
point.weight # → 40000.0
|
|
22
|
+
point.priority # → 1
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, **kwargs: Any):
|
|
26
|
+
"""
|
|
27
|
+
Initialize a new DataPoint with arbitrary attributes.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
**kwargs: Arbitrary keyword arguments that will be stored and exposed
|
|
31
|
+
as attributes on the DataPoint instance.
|
|
32
|
+
"""
|
|
33
|
+
self._data = dict(kwargs)
|
|
34
|
+
self._source = None
|
|
35
|
+
|
|
36
|
+
def __getattr__(self, name):
|
|
37
|
+
if name.startswith("_"):
|
|
38
|
+
raise AttributeError(name)
|
|
39
|
+
try:
|
|
40
|
+
return self._data[name]
|
|
41
|
+
except KeyError:
|
|
42
|
+
raise AttributeError(
|
|
43
|
+
f"'{type(self).__name__}' has no field '{name}'. "
|
|
44
|
+
f"Available fields: {list(self._data)}"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def __repr__(self):
|
|
48
|
+
items = ", ".join(f"{k}={v}" for k, v in self._data.items())
|
|
49
|
+
return f"DataPoint({items})"
|