python-drs 0.1.0__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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jonathan Lamontagne Kratz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,106 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-drs
3
+ Version: 0.1.0
4
+ Summary: A PyTorch-inspired, event-driven Discrete Rate Simulation framework.
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://github.com/epicgamer17/python-drs/tree/main/docs
31
+ Keywords: simulation,discrete-rate,drs,discrete-event,modeling,mining,continuous-flow
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
+ Requires-Python: >=3.9
42
+ Description-Content-Type: text/markdown
43
+ License-File: LICENSE
44
+ Requires-Dist: numpy>=1.21
45
+ Requires-Dist: pandas>=1.3
46
+ Requires-Dist: matplotlib>=3.4
47
+ Requires-Dist: seaborn>=0.12
48
+ Provides-Extra: progress
49
+ Requires-Dist: rich>=13.0; extra == "progress"
50
+ Provides-Extra: dev
51
+ Requires-Dist: build; extra == "dev"
52
+ Requires-Dist: twine; extra == "dev"
53
+ Requires-Dist: pytest; extra == "dev"
54
+ Dynamic: license-file
55
+
56
+ # python-drs
57
+
58
+ A PyTorch-inspired, event-driven **Discrete Rate Simulation (DRS)** framework for modeling systems where material flows continuously over time.
59
+
60
+ Instead of ticking through time at fixed intervals, DRS calculates *exactly* when the next limit (threshold) will be hit, jumps the simulation clock to that precise moment, and triggers the matching state transition. The result is a simulation that runs in a fraction of the time of fixed-step models — and never misses a limit.
61
+
62
+ ## Features
63
+
64
+ - **Event-driven time stepping** — simulate years of operation in seconds
65
+ - **PyTorch-style architecture** — `Module`, `Variable`, and `Level` compose into hierarchies with automatic dependency tracking
66
+ - **Built-in telemetry** — every state change is logged and plotted without custom tracking code
67
+ - **Fail-fast guardrails** — the engine stops you from breaking the physics of your model (e.g. draining an empty stockpile)
68
+ - **Applicable to any continuous-flow system** — mining supply chains, water pipes, electrical grids, traffic, and more
69
+
70
+ ## Installation
71
+
72
+ ```bash
73
+ pip install python-drs
74
+ ```
75
+
76
+ Optional extras:
77
+
78
+ ```bash
79
+ pip install python-drs[progress] # Rich progress bar
80
+ ```
81
+
82
+ ## Quickstart
83
+
84
+ ```python
85
+ from drs import DRSEngine, Module, Level
86
+
87
+ class Stockpile(Module):
88
+ def forward(self):
89
+ # Fill at 50 units per time step
90
+ self.ore.rate = 50.0
91
+
92
+ model = Stockpile()
93
+ model.ore = Level("Ore", initial_value=100.0)
94
+
95
+ engine = DRSEngine(model)
96
+ result = engine.run(max_time=20.0)
97
+ print(result.summary())
98
+ ```
99
+
100
+ ## Documentation
101
+
102
+ Full guides, tutorials, and API reference live in the [`docs/`](https://github.com/epicgamer17/python-drs/tree/main/docs) directory.
103
+
104
+ ## License
105
+
106
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,51 @@
1
+ # python-drs
2
+
3
+ A PyTorch-inspired, event-driven **Discrete Rate Simulation (DRS)** framework for modeling systems where material flows continuously over time.
4
+
5
+ Instead of ticking through time at fixed intervals, DRS calculates *exactly* when the next limit (threshold) will be hit, jumps the simulation clock to that precise moment, and triggers the matching state transition. The result is a simulation that runs in a fraction of the time of fixed-step models — and never misses a limit.
6
+
7
+ ## Features
8
+
9
+ - **Event-driven time stepping** — simulate years of operation in seconds
10
+ - **PyTorch-style architecture** — `Module`, `Variable`, and `Level` compose into hierarchies with automatic dependency tracking
11
+ - **Built-in telemetry** — every state change is logged and plotted without custom tracking code
12
+ - **Fail-fast guardrails** — the engine stops you from breaking the physics of your model (e.g. draining an empty stockpile)
13
+ - **Applicable to any continuous-flow system** — mining supply chains, water pipes, electrical grids, traffic, and more
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ pip install python-drs
19
+ ```
20
+
21
+ Optional extras:
22
+
23
+ ```bash
24
+ pip install python-drs[progress] # Rich progress bar
25
+ ```
26
+
27
+ ## Quickstart
28
+
29
+ ```python
30
+ from drs import DRSEngine, Module, Level
31
+
32
+ class Stockpile(Module):
33
+ def forward(self):
34
+ # Fill at 50 units per time step
35
+ self.ore.rate = 50.0
36
+
37
+ model = Stockpile()
38
+ model.ore = Level("Ore", initial_value=100.0)
39
+
40
+ engine = DRSEngine(model)
41
+ result = engine.run(max_time=20.0)
42
+ print(result.summary())
43
+ ```
44
+
45
+ ## Documentation
46
+
47
+ Full guides, tutorials, and API reference live in the [`docs/`](https://github.com/epicgamer17/python-drs/tree/main/docs) directory.
48
+
49
+ ## License
50
+
51
+ MIT — see [LICENSE](LICENSE).
@@ -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))
@@ -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()
@@ -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
@@ -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})"