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/variables.py ADDED
@@ -0,0 +1,547 @@
1
+ import math
2
+ import enum
3
+ from typing import Any, Union
4
+ from ._execution_context import ExecutionContext
5
+ from .exceptions import StateMutationError
6
+
7
+
8
+ def serialize_val(val: Any) -> Any:
9
+ if isinstance(val, enum.Enum):
10
+ return val.name
11
+ if (
12
+ hasattr(val, "name")
13
+ and hasattr(val, "id")
14
+ and not isinstance(val, (int, float, str, bool))
15
+ ):
16
+ return {"__type__": type(val).__name__, "name": val.name}
17
+ if isinstance(val, float):
18
+ if math.isinf(val):
19
+ return "Infinity" if val > 0 else "-Infinity"
20
+ elif math.isnan(val):
21
+ return "NaN"
22
+ return val
23
+
24
+
25
+ def deserialize_val(val: Any) -> Any:
26
+ if val == "Infinity":
27
+ return math.inf
28
+ elif val == "-Infinity":
29
+ return -math.inf
30
+ elif val == "NaN":
31
+ return math.nan
32
+ return val
33
+
34
+
35
+ class Expression:
36
+ """AST node for tracking mathematical dependencies between Variables."""
37
+
38
+ def __init__(self, op: str, left: Any, right: Any):
39
+ self.op = op
40
+ self.left = left
41
+ self.right = right
42
+
43
+ def evaluate(self) -> float:
44
+ def get_val(node):
45
+ if isinstance(node, Expression):
46
+ return node.evaluate()
47
+ if hasattr(node, "_sim_value"):
48
+ return node._sim_value()
49
+ return node
50
+
51
+ l_val = get_val(self.left)
52
+ r_val = get_val(self.right)
53
+
54
+ if self.op == "neg":
55
+ return -l_val
56
+ if self.op == "pos":
57
+ return +l_val
58
+ if self.op == "abs":
59
+ return abs(l_val)
60
+ if self.op == "add":
61
+ return l_val + r_val
62
+ if self.op == "sub":
63
+ return l_val - r_val
64
+ if self.op == "mul":
65
+ return l_val * r_val
66
+ if self.op == "div":
67
+ return l_val / r_val if r_val != 0 else 0.0
68
+ if self.op == "gt":
69
+ return l_val > r_val
70
+ if self.op == "lt":
71
+ return l_val < r_val
72
+ if self.op == "ge":
73
+ return l_val >= r_val
74
+ if self.op == "le":
75
+ return l_val <= r_val
76
+ if self.op == "eq":
77
+ return l_val == r_val
78
+ if self.op == "ne":
79
+ return l_val != r_val
80
+ if self.op == "pow":
81
+ return l_val**r_val
82
+ return 0.0
83
+
84
+ def get_sources(self) -> list:
85
+ sources = set()
86
+ for side in (self.left, self.right):
87
+ if hasattr(side, "get_sources"):
88
+ sources.update(side.get_sources())
89
+ elif isinstance(side, Variable):
90
+ sources.add(side)
91
+ return list(sources)
92
+
93
+ def get_equation(self) -> str:
94
+ op_chars = {
95
+ "add": "+",
96
+ "sub": "-",
97
+ "mul": "*",
98
+ "div": "/",
99
+ "gt": ">",
100
+ "lt": "<",
101
+ "ge": ">=",
102
+ "le": "<=",
103
+ "eq": "==",
104
+ "ne": "!=",
105
+ "pow": "**",
106
+ }
107
+ unary_chars = {"neg": "-", "pos": "+", "abs": "|"}
108
+
109
+ def format_node(node):
110
+ if isinstance(node, Expression):
111
+ return node.get_equation()
112
+ if hasattr(node, "name"):
113
+ mod = getattr(node, "_owner", None)
114
+ if mod and hasattr(mod, "name"):
115
+ return f"{mod.name}.{node.name}"
116
+ elif mod:
117
+ return f"{type(mod).__name__}.{node.name}"
118
+ return node.name
119
+ return str(node)
120
+
121
+ if self.op in unary_chars:
122
+ l = unary_chars[self.op]
123
+ r = unary_chars[self.op] if self.op == "abs" else ""
124
+ return f"({l}{format_node(self.left)}{r})"
125
+ return f"({format_node(self.left)} {op_chars.get(self.op, '?')} {format_node(self.right)})"
126
+
127
+ def __bool__(self):
128
+ raise TypeError(
129
+ f"Cannot use Expression ('{self.get_equation()}') as a boolean. "
130
+ f"Use `.value` for immediate evaluation or `drs.Where()` for symbolic branching."
131
+ )
132
+
133
+ def __neg__(self):
134
+ return Expression("neg", self, None)
135
+
136
+ def __pos__(self):
137
+ return Expression("pos", self, None)
138
+
139
+ def __abs__(self):
140
+ return Expression("abs", self, None)
141
+
142
+ def __add__(self, other):
143
+ return Expression("add", self, other)
144
+
145
+ def __sub__(self, other):
146
+ return Expression("sub", self, other)
147
+
148
+ def __mul__(self, other):
149
+ return Expression("mul", self, other)
150
+
151
+ def __truediv__(self, other):
152
+ return Expression("div", self, other)
153
+
154
+ def __radd__(self, other):
155
+ return Expression("add", other, self)
156
+
157
+ def __rsub__(self, other):
158
+ return Expression("sub", other, self)
159
+
160
+ def __rmul__(self, other):
161
+ return Expression("mul", other, self)
162
+
163
+ def __rtruediv__(self, other):
164
+ return Expression("div", other, self)
165
+
166
+ def __gt__(self, other):
167
+ return Expression("gt", self, other)
168
+
169
+ def __lt__(self, other):
170
+ return Expression("lt", self, other)
171
+
172
+ def __ge__(self, other):
173
+ return Expression("ge", self, other)
174
+
175
+ def __le__(self, other):
176
+ return Expression("le", self, other)
177
+
178
+ def __eq__(self, other):
179
+ return Expression("eq", self, other)
180
+
181
+ def __ne__(self, other):
182
+ return Expression("ne", self, other)
183
+
184
+ def __pow__(self, other):
185
+ return Expression("pow", self, other)
186
+
187
+ def __rpow__(self, other):
188
+ return Expression("pow", other, self)
189
+
190
+
191
+ class Variable:
192
+ """Base class for all domain variables.
193
+
194
+ Variables hold named state and belong to a specific `Module` owner. They ensure
195
+ that state is tracked properly through the execution context and prevent
196
+ cross-module mutation.
197
+
198
+ Attributes:
199
+ name (str): The unique name of the variable.
200
+ """
201
+
202
+ def __init__(self, name: str, initial_value: Any = 0.0) -> None:
203
+ """
204
+ Initialize a new Variable.
205
+
206
+ Args:
207
+ name: The unique name of the variable.
208
+ initial_value: The starting value (default: 0.0).
209
+ """
210
+ self.name = name
211
+ self._value = initial_value
212
+ self._owner = None
213
+
214
+ def _sim_value(self) -> Any:
215
+ if isinstance(self._value, Expression):
216
+ return self._value.evaluate()
217
+ return self._value
218
+
219
+ def get_sources(self) -> list:
220
+ return [self]
221
+
222
+ def _record_read_dependency(self) -> None:
223
+ """
224
+ [INTERNAL] Record that the current executing module has read this variable.
225
+
226
+ Power User Note: This is called automatically by the `value` getter. It
227
+ interfaces with the ExecutionContext to build the dependency graph.
228
+ """
229
+ current = ExecutionContext.get_current()
230
+ if current is not None and current is not self._owner:
231
+ current._record_incoming_edge(self)
232
+
233
+ @property
234
+ def value(self) -> Any:
235
+ """
236
+ Get the current value of the variable.
237
+
238
+ Reading this automatically records a dependency edge in the execution context,
239
+ linking the module that read it to the module that owns it.
240
+
241
+ Returns:
242
+ Any: The underlying value of the variable.
243
+ """
244
+ self._record_read_dependency()
245
+ if ExecutionContext.is_tracing():
246
+ return self
247
+ return self._sim_value()
248
+
249
+ @value.setter
250
+ def value(self, val: Any) -> None:
251
+ """
252
+ Set the value of the variable.
253
+
254
+ Args:
255
+ val (Any): The new value to set.
256
+
257
+ Raises:
258
+ RuntimeError: If a module attempts to mutate a variable it does not own.
259
+ """
260
+ current = ExecutionContext.get_current()
261
+ if current is not None and current is not self._owner:
262
+ raise StateMutationError(
263
+ f"Illegal Mutation: {type(current).__name__} tried to mutate "
264
+ f"'{self.name}' owned by {type(self._owner).__name__}. "
265
+ f"Modules must communicate by passing Flows. Do not mutate state directly!"
266
+ )
267
+
268
+ if self._value != val:
269
+ engine = ExecutionContext.get_engine()
270
+ if engine and getattr(engine, "telemetry", None):
271
+ engine.telemetry.log_event(
272
+ time=engine.current_time,
273
+ event_type="STATE_CHANGE",
274
+ source=type(current).__name__ if current else "External",
275
+ details={
276
+ "variable": self.name,
277
+ "old_value": self._value,
278
+ "new_value": val,
279
+ },
280
+ )
281
+ self._value = val
282
+
283
+ @property
284
+ def rate(self) -> float:
285
+ raise AttributeError(
286
+ f"'{type(self).__name__}' has no attribute 'rate'. "
287
+ f"Only drs.Level supports .rate. Use drs.Level() for quantities that flow."
288
+ )
289
+
290
+ @rate.setter
291
+ def rate(self, val: Union[float, tuple[float, float, float]]) -> None:
292
+ raise AttributeError(
293
+ f"Cannot set .rate on '{type(self).__name__}'. "
294
+ f"Only drs.Level supports .rate."
295
+ )
296
+
297
+ def _unary(self, op: str):
298
+ self._record_read_dependency()
299
+ if ExecutionContext.is_tracing():
300
+ return Expression(op, self, None)
301
+ l_val = self._sim_value()
302
+ if op == "neg":
303
+ return -l_val
304
+ if op == "pos":
305
+ return +l_val
306
+ if op == "abs":
307
+ return abs(l_val)
308
+ return NotImplemented
309
+
310
+ def _op(self, op: str, other):
311
+ self._record_read_dependency()
312
+ if isinstance(other, Variable):
313
+ other._record_read_dependency()
314
+ if ExecutionContext.is_tracing():
315
+ return Expression(op, self, other)
316
+ r_val = other._sim_value() if isinstance(other, Variable) else other
317
+ if isinstance(r_val, Expression):
318
+ r_val = r_val.evaluate()
319
+ l_val = self._sim_value()
320
+ if op == "add":
321
+ return l_val + r_val
322
+ if op == "sub":
323
+ return l_val - r_val
324
+ if op == "mul":
325
+ return l_val * r_val
326
+ if op == "div":
327
+ return l_val / r_val if r_val != 0 else 0.0
328
+ if op == "gt":
329
+ return l_val > r_val
330
+ if op == "lt":
331
+ return l_val < r_val
332
+ if op == "ge":
333
+ return l_val >= r_val
334
+ if op == "le":
335
+ return l_val <= r_val
336
+ if op == "eq":
337
+ return l_val == r_val
338
+ if op == "ne":
339
+ return l_val != r_val
340
+ if op == "pow":
341
+ return l_val**r_val
342
+ return NotImplemented
343
+
344
+ def _rop(self, op: str, other):
345
+ self._record_read_dependency()
346
+ if isinstance(other, Variable):
347
+ other._record_read_dependency()
348
+ if ExecutionContext.is_tracing():
349
+ return Expression(op, other, self)
350
+ l_val = other._sim_value() if isinstance(other, Variable) else other
351
+ if isinstance(l_val, Expression):
352
+ l_val = l_val.evaluate()
353
+ r_val = self._sim_value()
354
+ if op == "add":
355
+ return l_val + r_val
356
+ if op == "sub":
357
+ return l_val - r_val
358
+ if op == "mul":
359
+ return l_val * r_val
360
+ if op == "div":
361
+ return l_val / r_val if r_val != 0 else 0.0
362
+ if op == "pow":
363
+ return l_val**r_val
364
+ return NotImplemented
365
+
366
+ def __neg__(self):
367
+ return self._unary("neg")
368
+
369
+ def __pos__(self):
370
+ return self._unary("pos")
371
+
372
+ def __abs__(self):
373
+ return self._unary("abs")
374
+
375
+ def __add__(self, other):
376
+ return self._op("add", other)
377
+
378
+ def __sub__(self, other):
379
+ return self._op("sub", other)
380
+
381
+ def __mul__(self, other):
382
+ return self._op("mul", other)
383
+
384
+ def __truediv__(self, other):
385
+ return self._op("div", other)
386
+
387
+ def __radd__(self, other):
388
+ return self._rop("add", other)
389
+
390
+ def __rsub__(self, other):
391
+ return self._rop("sub", other)
392
+
393
+ def __rmul__(self, other):
394
+ return self._rop("mul", other)
395
+
396
+ def __rtruediv__(self, other):
397
+ return self._rop("div", other)
398
+
399
+ def __gt__(self, other):
400
+ return self._op("gt", other)
401
+
402
+ def __lt__(self, other):
403
+ return self._op("lt", other)
404
+
405
+ def __ge__(self, other):
406
+ return self._op("ge", other)
407
+
408
+ def __le__(self, other):
409
+ return self._op("le", other)
410
+
411
+ def __eq__(self, other):
412
+ return self._op("eq", other)
413
+
414
+ def __ne__(self, other):
415
+ return self._op("ne", other)
416
+
417
+ def __pow__(self, other):
418
+ return self._op("pow", other)
419
+
420
+ def __rpow__(self, other):
421
+ return self._rop("pow", other)
422
+
423
+ def __hash__(self) -> int:
424
+ return id(self)
425
+
426
+
427
+ class Level(Variable):
428
+ """A variable that accumulates over time based on a rate.
429
+
430
+ Levels are the primary way to model physical quantities that flow or change
431
+ continuously over time (e.g., mass in a stockpile, energy in a battery).
432
+
433
+ Attributes:
434
+ upper_threshold (float): The maximum limit for the level. The engine will
435
+ stop exactly at this boundary. Defaults to math.inf.
436
+ lower_threshold (float): The minimum limit for the level. Defaults to -math.inf.
437
+ """
438
+
439
+ def __init__(
440
+ self, name: str, initial_value: float = 0.0, rate: float = 0.0
441
+ ) -> None:
442
+ """
443
+ Initialize a new Level.
444
+
445
+ Args:
446
+ name: The unique name of the level.
447
+ initial_value: The starting value (default: 0.0).
448
+ rate: The initial rate of change (default: 0.0).
449
+ """
450
+ super().__init__(name, initial_value)
451
+ self._rate = rate
452
+ self.upper_threshold = math.inf
453
+ self.lower_threshold = -math.inf
454
+ self._rate_set_by = None
455
+
456
+ @property
457
+ def rate(self) -> float:
458
+ """
459
+ Get the current rate of change.
460
+
461
+ Returns:
462
+ float: The rate at which the level is currently accumulating per time unit.
463
+ """
464
+ self._record_read_dependency()
465
+ if ExecutionContext.is_tracing():
466
+ return self._rate
467
+ if isinstance(self._rate, Expression):
468
+ return self._rate.evaluate()
469
+ return self._rate
470
+
471
+ @rate.setter
472
+ def rate(self, val: Union[float, tuple[float, float, float]]) -> None:
473
+ """
474
+ Set the rate of change.
475
+
476
+ Args:
477
+ val (Any): Can be a single float representing the new rate, or a tuple
478
+ of `(rate, lower_threshold, upper_threshold)`.
479
+
480
+ Raises:
481
+ ValueError: If a tuple is provided but it does not have exactly 3 elements.
482
+ """
483
+ current_actor = ExecutionContext.get_current()
484
+ if current_actor is not None and current_actor is not self._owner:
485
+ if hasattr(current_actor, "_record_incoming_edge"):
486
+ current_actor._record_incoming_edge(self)
487
+
488
+ # Rate override guardrail
489
+ if (
490
+ current_actor is not None
491
+ and self._rate_set_by is not None
492
+ and self._rate_set_by is not current_actor
493
+ ):
494
+ raise StateMutationError(
495
+ f"Rate Conflict: '{type(current_actor).__name__}' attempted to set the rate of "
496
+ f"'{self.name}', but it was already set by '{type(self._rate_set_by).__name__}' "
497
+ f"during this time step. Multiple modules cannot control the rate of the same Level."
498
+ )
499
+ self._rate_set_by = current_actor
500
+
501
+ if isinstance(val, tuple):
502
+ if len(val) == 3:
503
+ self._rate, self.lower_threshold, self.upper_threshold = val
504
+ else:
505
+ raise ValueError(f"Rate tuple must be (rate, lower, upper), got {val}")
506
+ else:
507
+ self._rate = val
508
+
509
+ def _update(self, dt: float) -> None:
510
+ """
511
+ [INTERNAL] Step the level forward in time based on its current rate.
512
+
513
+ Power User Note: This is called automatically by the DRSEngine. Do not call this
514
+ manually unless you are implementing a custom time-stepping loop.
515
+
516
+ Args:
517
+ dt (float): The amount of time to simulate.
518
+ """
519
+ self.value += self.rate * dt
520
+
521
+
522
+ class Timer(Level):
523
+ """A specialized level used to track time.
524
+
525
+ Timers are simply Levels that accumulate at a default rate of 1.0 (or -1.0 for countdowns).
526
+ """
527
+
528
+ def __init__(
529
+ self, name: str, initial_value: float = 0.0, rate: float = 1.0
530
+ ) -> None:
531
+ """
532
+ Initialize a Timer.
533
+
534
+ Args:
535
+ name: The unique name of the timer.
536
+ initial_value: The starting time value (default: 0.0).
537
+ rate: The speed of time (default: 1.0).
538
+ """
539
+ super().__init__(name, initial_value, rate)
540
+
541
+ def reset(self) -> None:
542
+ """
543
+ Reset the timer value back to 0.0.
544
+
545
+ This sets the absolute value of the timer to 0, but does not modify the rate.
546
+ """
547
+ self.value = 0.0
@@ -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,18 @@
1
+ drs/__init__.py,sha256=UkCagrq2kLPVFgVawNy9fo04hc-0Jt_z7pQZKms3ADk,1178
2
+ drs/_execution_context.py,sha256=wJCq3P8e9ETx_GyP7Cg_s8KRqzDZxd14PnMD3O8x4ho,2696
3
+ drs/callbacks.py,sha256=W-oAj36F6emr9QPxrh7Gl3niJECmrQ2AEyf3DjGzPkI,3604
4
+ drs/config.py,sha256=AetXaQRpHftPZLfY8SLpIdGBVjARKKNsSKUs935iR0s,768
5
+ drs/data_source.py,sha256=SoVPWCr3hzdEKkqFpcov5amTaA4dVAWf_qBqKgdAByk,1612
6
+ drs/engine.py,sha256=nt9b-zhsdXTvfaIao8rlSk8ObF4M9cVdh3apzKGIG0Y,16362
7
+ drs/exceptions.py,sha256=bKO054dsU5vUVw5Hv6jG6Y7DJ8j-SfYpTnbBqUB8HH8,1015
8
+ drs/flow.py,sha256=xBTzj4txCf9lUqvJyko0i2ZrWuR-hrpNp3-MHaxOZGY,882
9
+ drs/module.py,sha256=5_Z7Vw12sqUKxBOYLPxF1PoxXtIQQivDDk_eWENiKZs,18317
10
+ drs/plot.py,sha256=tYZ9Rv3N-f-CRFIVKc5yXXf4FmaF4Uu7LIVD3ShJ418,6938
11
+ drs/serialize.py,sha256=XcuEhOGZCoBjM6FVDM-5kfoOb0LJIWbACkOczL-cX4s,14764
12
+ drs/telemetry.py,sha256=1XuUpiqO9jL1ZqMGgUgEz8P57nFg1YlNqKJkDW_qLfI,6336
13
+ drs/variables.py,sha256=w9KI0Z8KtMpefBUlBjaddpaQaoFqXjF_9xQoJlio-zc,16834
14
+ python_drs-0.1.0.dist-info/licenses/LICENSE,sha256=tPAvFtSTwLleSk2ZL8TnhfG-XUSOO3T98VnS3xZgwP0,1082
15
+ python_drs-0.1.0.dist-info/METADATA,sha256=E1jSOV488U-mi01u_MbCZ9NTuG5EiPq8d9Wrg5t47uU,4333
16
+ python_drs-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
17
+ python_drs-0.1.0.dist-info/top_level.txt,sha256=adIZh9e3GYNwobkEd7ymv5xbZR-L54aXZYZSx0rW7Cs,4
18
+ python_drs-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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 @@
1
+ drs