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/module.py
ADDED
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
import math
|
|
2
|
+
from typing import Iterator, Any, Optional
|
|
3
|
+
from .variables import Variable, Level, Timer
|
|
4
|
+
from ._execution_context import ExecutionContext
|
|
5
|
+
from .data_source import DataPoint
|
|
6
|
+
from .flow import Flow
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Module:
|
|
10
|
+
"""Base class for all DRS models and sub-components.
|
|
11
|
+
|
|
12
|
+
Modules are the fundamental building blocks of a simulation. They automatically
|
|
13
|
+
register any `Variable` or `Module` assigned as an attribute, mimicking the
|
|
14
|
+
behavior of PyTorch's `nn.Module`.
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
parent (Optional[Module]): The parent module that owns this module.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self) -> None:
|
|
21
|
+
"""Initialize the module and its internal registries."""
|
|
22
|
+
self._variables = {}
|
|
23
|
+
self._modules = {}
|
|
24
|
+
self.parent = None
|
|
25
|
+
self._post_step_hooks = []
|
|
26
|
+
self._dependencies = []
|
|
27
|
+
self._dep_seen = set()
|
|
28
|
+
self._flow_dependencies = []
|
|
29
|
+
self._flow_dep_seen = set()
|
|
30
|
+
self._data_dependencies = []
|
|
31
|
+
self._data_dep_seen = set()
|
|
32
|
+
|
|
33
|
+
def __call__(self, *args: Any, **kwargs: Any) -> Any:
|
|
34
|
+
"""
|
|
35
|
+
Execute the forward pass while managing the ExecutionContext.
|
|
36
|
+
|
|
37
|
+
This method acts as a wrapper around `forward()`. It pushes this module
|
|
38
|
+
onto the execution stack, validates that inter-module communication uses
|
|
39
|
+
only `drs.Flow` or `drs.DataPoint`, records dependency edges, and then
|
|
40
|
+
pops the execution stack.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
*args: Positional arguments passed to `forward()`.
|
|
44
|
+
**kwargs: Keyword arguments passed to `forward()`.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Any: The result of the `forward()` pass.
|
|
48
|
+
|
|
49
|
+
Raises:
|
|
50
|
+
RuntimeError: If invalid types are passed or returned.
|
|
51
|
+
"""
|
|
52
|
+
caller = ExecutionContext.get_current()
|
|
53
|
+
ExecutionContext.push(self)
|
|
54
|
+
try:
|
|
55
|
+
if caller is not None and caller is not self:
|
|
56
|
+
|
|
57
|
+
def validate_drs_type(arg, arg_name):
|
|
58
|
+
if arg is None:
|
|
59
|
+
return
|
|
60
|
+
if isinstance(arg, (tuple, list)):
|
|
61
|
+
for item in arg:
|
|
62
|
+
validate_drs_type(item, arg_name)
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
if not isinstance(arg, (Flow, Variable, DataPoint)):
|
|
66
|
+
raise RuntimeError(
|
|
67
|
+
f"Hidden Dependency Error: '{type(caller).__name__}' passed an untracked type "
|
|
68
|
+
f"'{type(arg).__name__}' to '{type(self).__name__}' for {arg_name}. "
|
|
69
|
+
f"Inter-module arguments MUST be drs.Flow (physics) or drs.Variable (control)."
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
for i, arg in enumerate(args):
|
|
73
|
+
validate_drs_type(arg, f"positional arg {i}")
|
|
74
|
+
for key, val in kwargs.items():
|
|
75
|
+
validate_drs_type(val, f"keyword arg '{key}'")
|
|
76
|
+
|
|
77
|
+
for arg in args:
|
|
78
|
+
if isinstance(arg, Flow) and arg._source is not None:
|
|
79
|
+
ExecutionContext.record_flow_edge(arg._source, self)
|
|
80
|
+
self._record_flow_edge(arg._source)
|
|
81
|
+
elif isinstance(arg, DataPoint) and arg._source is not None:
|
|
82
|
+
self._record_data_edge(arg._source)
|
|
83
|
+
for v in kwargs.values():
|
|
84
|
+
if isinstance(v, Flow) and v._source is not None:
|
|
85
|
+
ExecutionContext.record_flow_edge(v._source, self)
|
|
86
|
+
self._record_flow_edge(v._source)
|
|
87
|
+
elif isinstance(v, DataPoint) and v._source is not None:
|
|
88
|
+
self._record_data_edge(v._source)
|
|
89
|
+
|
|
90
|
+
result = self.forward(*args, **kwargs)
|
|
91
|
+
|
|
92
|
+
if isinstance(result, tuple):
|
|
93
|
+
for res in result:
|
|
94
|
+
if not isinstance(res, (Flow, DataPoint)):
|
|
95
|
+
raise RuntimeError(
|
|
96
|
+
f"Tuple returned from '{type(self).__name__}.forward()' "
|
|
97
|
+
f"must contain only drs.Flow or drs.DataPoint objects."
|
|
98
|
+
)
|
|
99
|
+
res._source = self
|
|
100
|
+
return result
|
|
101
|
+
|
|
102
|
+
if isinstance(self, DataSource):
|
|
103
|
+
if result is None:
|
|
104
|
+
raise RuntimeError(
|
|
105
|
+
f"'{type(self).__name__}.forward()' must return drs.DataPoint or drs.Flow. "
|
|
106
|
+
f"DataSource subclasses cannot return None."
|
|
107
|
+
)
|
|
108
|
+
if not isinstance(result, (Flow, DataPoint)):
|
|
109
|
+
raise RuntimeError(
|
|
110
|
+
f"'{type(self).__name__}.forward()' returned "
|
|
111
|
+
f"'{type(result).__name__}', not drs.Flow or drs.DataPoint."
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
if result is not None and not isinstance(result, (Flow, DataPoint)):
|
|
115
|
+
raise RuntimeError(
|
|
116
|
+
f"'{type(self).__name__}.forward()' returned "
|
|
117
|
+
f"'{type(result).__name__}', not a drs.Flow or drs.DataPoint. "
|
|
118
|
+
f"Inter-module communication must use drs.Flow or drs.DataPoint."
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
if isinstance(result, (Flow, DataPoint)):
|
|
122
|
+
result._source = self
|
|
123
|
+
|
|
124
|
+
return result
|
|
125
|
+
finally:
|
|
126
|
+
ExecutionContext.pop()
|
|
127
|
+
|
|
128
|
+
def forward(self, *args: Any, **kwargs: Any) -> Any:
|
|
129
|
+
"""
|
|
130
|
+
Define the physics and logic of the module.
|
|
131
|
+
|
|
132
|
+
This method must be implemented by all subclasses. It is called on every
|
|
133
|
+
time step to update rates and determine flows.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
*args: Positional arguments.
|
|
137
|
+
**kwargs: Keyword arguments.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
Any: Typically a `drs.Flow`, `drs.DataPoint`, or a tuple of them.
|
|
141
|
+
"""
|
|
142
|
+
raise NotImplementedError("Module subclasses must implement forward()")
|
|
143
|
+
|
|
144
|
+
# TODO: add @property def current_time(self) -> float
|
|
145
|
+
# This would implicitly grab ExecutionContext.get_engine().current_time.
|
|
146
|
+
# Benefit: Allows modules/controllers to read simulation time natively
|
|
147
|
+
# (e.g. `if self.current_time > 10.0:`) without needing to explicitly instantiate and track a Timer variable.
|
|
148
|
+
|
|
149
|
+
def __setattr__(self, name: str, value: Any) -> None:
|
|
150
|
+
if name.startswith("_"):
|
|
151
|
+
super().__setattr__(name, value)
|
|
152
|
+
return
|
|
153
|
+
|
|
154
|
+
if hasattr(self, "_variables"):
|
|
155
|
+
self._variables.pop(name, None)
|
|
156
|
+
if hasattr(self, "_modules") and name != "parent":
|
|
157
|
+
self._modules.pop(name, None)
|
|
158
|
+
|
|
159
|
+
if isinstance(value, Variable):
|
|
160
|
+
if not hasattr(self, "_variables"):
|
|
161
|
+
raise AttributeError(
|
|
162
|
+
"Cannot assign variable before Module.__init__() call"
|
|
163
|
+
)
|
|
164
|
+
self._variables[name] = value
|
|
165
|
+
value._owner = self
|
|
166
|
+
elif isinstance(value, Module) and name != "parent":
|
|
167
|
+
if not hasattr(self, "_modules"):
|
|
168
|
+
raise AttributeError(
|
|
169
|
+
"Cannot assign module before Module.__init__() call"
|
|
170
|
+
)
|
|
171
|
+
self._modules[name] = value
|
|
172
|
+
if not getattr(value, "parent", None):
|
|
173
|
+
value.parent = self
|
|
174
|
+
|
|
175
|
+
super().__setattr__(name, value)
|
|
176
|
+
|
|
177
|
+
def _record_incoming_edge(self, variable: Variable) -> None:
|
|
178
|
+
"""
|
|
179
|
+
[INTERNAL] Record that this module reads 'variable' (owned by another module).
|
|
180
|
+
|
|
181
|
+
Power User Note: Automatically called by the ExecutionContext to build graphs.
|
|
182
|
+
"""
|
|
183
|
+
if variable._owner is not None and variable._owner is not self:
|
|
184
|
+
key = (id(variable._owner), id(variable))
|
|
185
|
+
if key not in self._dep_seen:
|
|
186
|
+
self._dep_seen.add(key)
|
|
187
|
+
self._dependencies.append((variable._owner, variable))
|
|
188
|
+
|
|
189
|
+
def _record_flow_edge(self, source_module: "Module") -> None:
|
|
190
|
+
"""
|
|
191
|
+
[INTERNAL] Record that this module received a Flow from source_module.
|
|
192
|
+
|
|
193
|
+
Power User Note: Automatically called by the ExecutionContext to build flow graphs.
|
|
194
|
+
"""
|
|
195
|
+
if source_module is not None and source_module is not self:
|
|
196
|
+
key = id(source_module)
|
|
197
|
+
if key not in self._flow_dep_seen:
|
|
198
|
+
self._flow_dep_seen.add(key)
|
|
199
|
+
self._flow_dependencies.append(source_module)
|
|
200
|
+
|
|
201
|
+
def _record_data_edge(self, source_module: "Module") -> None:
|
|
202
|
+
"""
|
|
203
|
+
[INTERNAL] Record that this module received a DataPoint from source_module.
|
|
204
|
+
|
|
205
|
+
Power User Note: Automatically called by the ExecutionContext to build data graphs.
|
|
206
|
+
"""
|
|
207
|
+
if source_module is not None and source_module is not self:
|
|
208
|
+
key = id(source_module)
|
|
209
|
+
if key not in self._data_dep_seen:
|
|
210
|
+
self._data_dep_seen.add(key)
|
|
211
|
+
self._data_dependencies.append(source_module)
|
|
212
|
+
|
|
213
|
+
def variables(self) -> Iterator[Variable]:
|
|
214
|
+
"""
|
|
215
|
+
Recursively yield all variables owned by this module and its sub-modules.
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
Iterator[Variable]: An iterator over all unique variables in the hierarchy.
|
|
219
|
+
"""
|
|
220
|
+
seen = set()
|
|
221
|
+
|
|
222
|
+
def _get_vars(module):
|
|
223
|
+
for var in module._variables.values():
|
|
224
|
+
var_id = id(var)
|
|
225
|
+
if var_id not in seen:
|
|
226
|
+
seen.add(var_id)
|
|
227
|
+
yield var
|
|
228
|
+
for mod in module._modules.values():
|
|
229
|
+
yield from _get_vars(mod)
|
|
230
|
+
|
|
231
|
+
yield from _get_vars(self)
|
|
232
|
+
|
|
233
|
+
def modules(self) -> Iterator["Module"]:
|
|
234
|
+
"""
|
|
235
|
+
Recursively yield this module and all nested sub-modules.
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
Iterator[Module]: An iterator over the module hierarchy.
|
|
239
|
+
"""
|
|
240
|
+
for _, module in self.named_modules():
|
|
241
|
+
yield module
|
|
242
|
+
|
|
243
|
+
def named_modules(self, prefix: str = "") -> Iterator[tuple[str, "Module"]]:
|
|
244
|
+
"""
|
|
245
|
+
Recursively yield `(path, module)` pairs using PyTorch-style names.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
prefix (str): The prefix to prepend to the paths.
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
Iterator[tuple[str, Module]]: An iterator of `(path, module)` tuples.
|
|
252
|
+
"""
|
|
253
|
+
seen = set()
|
|
254
|
+
|
|
255
|
+
def _get_modules(module, module_prefix):
|
|
256
|
+
module_id = id(module)
|
|
257
|
+
if module_id in seen:
|
|
258
|
+
return
|
|
259
|
+
seen.add(module_id)
|
|
260
|
+
yield module_prefix, module
|
|
261
|
+
|
|
262
|
+
for name, sub_mod in module._modules.items():
|
|
263
|
+
sub_prefix = name if not module_prefix else f"{module_prefix}.{name}"
|
|
264
|
+
yield from _get_modules(sub_mod, sub_prefix)
|
|
265
|
+
|
|
266
|
+
yield from _get_modules(self, prefix)
|
|
267
|
+
|
|
268
|
+
def _zero_rates(self) -> None:
|
|
269
|
+
"""
|
|
270
|
+
[INTERNAL] Zero out rates and remove thresholds for all Levels before the next rate update.
|
|
271
|
+
"""
|
|
272
|
+
for var in self.variables():
|
|
273
|
+
if isinstance(var, Level):
|
|
274
|
+
var._rate = 0.0
|
|
275
|
+
var.upper_threshold = math.inf
|
|
276
|
+
var.lower_threshold = -math.inf
|
|
277
|
+
|
|
278
|
+
def initialize_state(self) -> None:
|
|
279
|
+
"""
|
|
280
|
+
Override this to set up initial state before the simulation starts.
|
|
281
|
+
|
|
282
|
+
This is called once by the engine before the first time step.
|
|
283
|
+
"""
|
|
284
|
+
pass
|
|
285
|
+
|
|
286
|
+
def is_terminating_condition_met(self) -> bool:
|
|
287
|
+
"""
|
|
288
|
+
Override this to define custom stopping conditions.
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
bool: True if the simulation should stop, False otherwise.
|
|
292
|
+
"""
|
|
293
|
+
return False
|
|
294
|
+
|
|
295
|
+
def state_dict(self, prefix: str = "") -> dict[str, Any]:
|
|
296
|
+
"""
|
|
297
|
+
Returns a dictionary containing the entire state of the module.
|
|
298
|
+
Keys are dotted paths to variable values (e.g., 'submodule.variable.value').
|
|
299
|
+
"""
|
|
300
|
+
state = {}
|
|
301
|
+
for name, var in self._variables.items():
|
|
302
|
+
key_base = f"{prefix}.{name}" if prefix else name
|
|
303
|
+
state[f"{key_base}.value"] = var.value
|
|
304
|
+
|
|
305
|
+
for name, module in self._modules.items():
|
|
306
|
+
module_prefix = f"{prefix}.{name}" if prefix else name
|
|
307
|
+
state.update(module.state_dict(prefix=module_prefix))
|
|
308
|
+
|
|
309
|
+
return state
|
|
310
|
+
|
|
311
|
+
def load_state_dict(self, state_dict: dict[str, Any]) -> None:
|
|
312
|
+
"""
|
|
313
|
+
Copies state from state_dict into this module and its descendants.
|
|
314
|
+
"""
|
|
315
|
+
for key, value in state_dict.items():
|
|
316
|
+
if not key.endswith(".value"):
|
|
317
|
+
continue
|
|
318
|
+
key_base = key[:-6] # remove .value
|
|
319
|
+
parts = key_base.split(".")
|
|
320
|
+
var_name = parts[-1]
|
|
321
|
+
mod_path = parts[:-1]
|
|
322
|
+
|
|
323
|
+
# Navigate to the correct module
|
|
324
|
+
current_mod = self
|
|
325
|
+
try:
|
|
326
|
+
for part in mod_path:
|
|
327
|
+
current_mod = current_mod._modules[part]
|
|
328
|
+
current_mod._variables[var_name].value = value
|
|
329
|
+
except KeyError:
|
|
330
|
+
import logging
|
|
331
|
+
|
|
332
|
+
logging.getLogger(__name__).warning(
|
|
333
|
+
f"Key '{key}' found in state_dict but not in module hierarchy."
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
def to_dict(self, root: Optional["Module"] = None) -> dict[str, Any]:
|
|
337
|
+
"""
|
|
338
|
+
Returns a structural JSON-serializable representation of the module architecture.
|
|
339
|
+
"""
|
|
340
|
+
from .variables import Level, Expression, serialize_val
|
|
341
|
+
|
|
342
|
+
if root is None:
|
|
343
|
+
root = self
|
|
344
|
+
|
|
345
|
+
# TODO: Why is expresison not included in serialize_val?
|
|
346
|
+
def _to_dict_serialize_val(val: Any) -> Any:
|
|
347
|
+
if isinstance(val, Expression):
|
|
348
|
+
return {"equation": val.get_equation()}
|
|
349
|
+
return serialize_val(val)
|
|
350
|
+
|
|
351
|
+
def get_module_path(rt: Module, target: Module) -> str:
|
|
352
|
+
if target is rt:
|
|
353
|
+
return ""
|
|
354
|
+
for path, mod in rt.named_modules():
|
|
355
|
+
if mod is target:
|
|
356
|
+
return path
|
|
357
|
+
return getattr(target, "name", type(target).__name__)
|
|
358
|
+
|
|
359
|
+
children = {}
|
|
360
|
+
for name, mod in self._modules.items():
|
|
361
|
+
children[name] = mod.to_dict(root)
|
|
362
|
+
|
|
363
|
+
variables = {}
|
|
364
|
+
for name, var in self._variables.items():
|
|
365
|
+
var_info = {
|
|
366
|
+
"class": type(var).__name__,
|
|
367
|
+
"value": _to_dict_serialize_val(var._value),
|
|
368
|
+
}
|
|
369
|
+
if isinstance(var, Level):
|
|
370
|
+
var_info["rate"] = _to_dict_serialize_val(var._rate)
|
|
371
|
+
var_info["lower_threshold"] = _to_dict_serialize_val(
|
|
372
|
+
var.lower_threshold
|
|
373
|
+
)
|
|
374
|
+
var_info["upper_threshold"] = _to_dict_serialize_val(
|
|
375
|
+
var.upper_threshold
|
|
376
|
+
)
|
|
377
|
+
variables[name] = var_info
|
|
378
|
+
|
|
379
|
+
layout = getattr(self, "layout", getattr(self, "metadata", {}))
|
|
380
|
+
if not isinstance(layout, dict):
|
|
381
|
+
layout = {"value": str(layout)}
|
|
382
|
+
|
|
383
|
+
attributes = {}
|
|
384
|
+
import json
|
|
385
|
+
from .variables import Variable
|
|
386
|
+
|
|
387
|
+
# Exclude reserved framework attributes
|
|
388
|
+
RESERVED_KEYS = {
|
|
389
|
+
"parent",
|
|
390
|
+
"config",
|
|
391
|
+
"telemetry",
|
|
392
|
+
"layout",
|
|
393
|
+
"global_time",
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
for k, v in self.__dict__.items():
|
|
397
|
+
if k.startswith("_"):
|
|
398
|
+
continue
|
|
399
|
+
if k in RESERVED_KEYS:
|
|
400
|
+
continue
|
|
401
|
+
if isinstance(v, (Module, Variable)):
|
|
402
|
+
continue
|
|
403
|
+
if isinstance(v, (int, float, str, bool, list, dict)) or v is None:
|
|
404
|
+
try:
|
|
405
|
+
json.dumps(v)
|
|
406
|
+
attributes[k] = v
|
|
407
|
+
except Exception:
|
|
408
|
+
pass
|
|
409
|
+
|
|
410
|
+
flow_inputs = []
|
|
411
|
+
for src in self._flow_dependencies:
|
|
412
|
+
flow_inputs.append(get_module_path(root, src))
|
|
413
|
+
|
|
414
|
+
data_inputs = []
|
|
415
|
+
for src in self._data_dependencies:
|
|
416
|
+
data_inputs.append(get_module_path(root, src))
|
|
417
|
+
|
|
418
|
+
variable_reads = []
|
|
419
|
+
for src_mod, var in self._dependencies:
|
|
420
|
+
variable_reads.append(
|
|
421
|
+
{"module": get_module_path(root, src_mod), "variable": var.name}
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
connections = {
|
|
425
|
+
"flow_inputs": flow_inputs,
|
|
426
|
+
"data_inputs": data_inputs,
|
|
427
|
+
"variable_reads": variable_reads,
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return {
|
|
431
|
+
"class": type(self).__name__,
|
|
432
|
+
"layout": layout,
|
|
433
|
+
"variables": variables,
|
|
434
|
+
"attributes": attributes,
|
|
435
|
+
"children": children,
|
|
436
|
+
"connections": connections,
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
def register_post_step_hook(self, hook_fn: Any) -> None:
|
|
440
|
+
"""Registers a callback to be run after every engine step."""
|
|
441
|
+
self._post_step_hooks.append(hook_fn)
|
|
442
|
+
|
|
443
|
+
def _run_post_step_hooks(self, current_time: float) -> None:
|
|
444
|
+
"""
|
|
445
|
+
[INTERNAL] Execute all registered post-step callback functions.
|
|
446
|
+
|
|
447
|
+
Power User Note: Called automatically by the DRSEngine after time integration.
|
|
448
|
+
"""
|
|
449
|
+
for hook in self._post_step_hooks:
|
|
450
|
+
hook(current_time)
|
|
451
|
+
|
|
452
|
+
def get_dependency_graph(self) -> list:
|
|
453
|
+
"""
|
|
454
|
+
Get all recorded read dependencies from this module and all sub-modules.
|
|
455
|
+
|
|
456
|
+
Returns:
|
|
457
|
+
list[tuple[Module, Variable]]: A list of `(source_module, variable)` pairs
|
|
458
|
+
representing cross-module reads.
|
|
459
|
+
"""
|
|
460
|
+
result = []
|
|
461
|
+
for mod in self.modules():
|
|
462
|
+
result.extend(mod._dependencies)
|
|
463
|
+
return result
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
# NOTE: could remove and use just modules and Flow instead of DataSource and DataPoint. May be better.
|
|
467
|
+
class DataSource(Module):
|
|
468
|
+
"""Yields ``DataPoint`` batches one at a time.
|
|
469
|
+
|
|
470
|
+
Subclass and implement ``__next__`` to define the data stream.
|
|
471
|
+
Raise ``StopIteration`` when the stream is exhausted::
|
|
472
|
+
|
|
473
|
+
class MySource(DataSource):
|
|
474
|
+
def __init__(self):
|
|
475
|
+
super().__init__()
|
|
476
|
+
self._data = [DataPoint(x=1), DataPoint(x=2)]
|
|
477
|
+
self._index = 0
|
|
478
|
+
|
|
479
|
+
def __next__(self) -> DataPoint:
|
|
480
|
+
if self._index >= len(self._data):
|
|
481
|
+
raise StopIteration
|
|
482
|
+
point = self._data[self._index]
|
|
483
|
+
self._index += 1
|
|
484
|
+
return point
|
|
485
|
+
"""
|
|
486
|
+
|
|
487
|
+
def __init__(self) -> None:
|
|
488
|
+
"""Initialize the DataSource."""
|
|
489
|
+
super().__init__()
|
|
490
|
+
|
|
491
|
+
def __iter__(self) -> Iterator[DataPoint]:
|
|
492
|
+
"""Return the iterator object itself."""
|
|
493
|
+
return self
|
|
494
|
+
|
|
495
|
+
def __next__(self) -> DataPoint:
|
|
496
|
+
"""Yield the next DataPoint in the sequence."""
|
|
497
|
+
raise StopIteration
|