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 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})"