numbox 0.2.17__py3-none-any.whl → 0.3.1__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.

Potentially problematic release.


This version of numbox might be problematic. Click here for more details.

numbox/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = '0.2.17'
1
+ __version__ = '0.3.1'
File without changes
@@ -0,0 +1,428 @@
1
+ import warnings
2
+
3
+ from collections import defaultdict
4
+ from dataclasses import dataclass, field
5
+ from typing import (
6
+ Any, Callable, Dict, List, Mapping, Set, Tuple, TypeAlias, Union
7
+ )
8
+
9
+
10
+ Namespace: TypeAlias = Union['External', 'Variables']
11
+ VarSpec: TypeAlias = Dict[str, Callable | Dict[str, str] | str]
12
+ VarValue: TypeAlias = Any
13
+
14
+
15
+ class _Null:
16
+ """ Value of `Variable` that has not been calculated. """
17
+ pass
18
+
19
+
20
+ _null = _Null()
21
+
22
+
23
+ QUAL_SEP = "."
24
+
25
+
26
+ def _make_qual_name(namespace_name: str, var_name: str) -> str:
27
+ """ Each `Variable` instance is best contained in a
28
+ mapping namespace, with the given `namespace_name`.
29
+ This function thereby returns qualified name of the
30
+ `Variable` instance. """
31
+ return f"{namespace_name}{QUAL_SEP}{var_name}"
32
+
33
+
34
+ class External:
35
+ """
36
+ A dictionary that facilitates discovery of required names.
37
+ When requested a `Variable` with the given name via a
38
+ typical `__getitem__` call, if the `Variable` is not
39
+ found, it will be created and added to this dictionary.
40
+ This way the user will be able to infer which variables
41
+ are required from the external source abstracted by this
42
+ dictionary.
43
+ """
44
+ def __init__(self, name: str):
45
+ self.name = name
46
+ self._vars = {}
47
+
48
+ def __getitem__(self, name):
49
+ variable = self._vars.get(name)
50
+ if variable is None:
51
+ variable = Variable(
52
+ name=name,
53
+ source=self.name
54
+ )
55
+ self._vars[name] = variable
56
+ return variable
57
+
58
+
59
+ @dataclass(frozen=True)
60
+ class Variable:
61
+ """
62
+ An instance of `Variable` is anything that can be calculated
63
+ from the values of the given input dependencies using the
64
+ provided formula (i.e., a function).
65
+
66
+ Calculated value can be `None`, that is why a non-calculated
67
+ value is designated with `_null`.
68
+
69
+ An instance of `Variable` is best created directly within
70
+ the given `Namespace`, that is, when it is instantiated upon
71
+ initialization of that `Namespace` and made then available
72
+ through a `__getitem__` call to either a collection of
73
+ variables created as an instance of `Variables` (see below)
74
+ or a non-`Variables` mapping, referred to (or abstracted by),
75
+ in general, as an `External` collection. The namespace is
76
+ also referred to as the 'source' of the `Variable`.
77
+
78
+ Qualified name of a `Variable` incorporates both the name
79
+ of the `Variable` and the name of its source / namespace.
80
+
81
+ It is therefore recommended to create `Variable` only in
82
+ the `External` or `Variables` containers rather than in
83
+ isolation.
84
+
85
+ :param name: name of the `Variable` instance.
86
+ :param source: name of the `Variables` or `External` instance
87
+ which is namespaces / source of this `Variable`.
88
+ :param inputs: (optional) map from names of the `Variable`
89
+ inputs (which are names of other `Variable` instances) to
90
+ the names of their sources, i.e., names of either
91
+ `Variables` or 'External' mapping collections referencing
92
+ these variables.
93
+ :param formula: (optional) function that calculates value
94
+ of this `Variable` from its sources.
95
+ :param cacheable: (default `False`) when `True`, the
96
+ corresponding `Value` (see below) will be cached during
97
+ calculation by the `id` of the corresponding Python object
98
+ containing that value. When attempted to recompute with
99
+ the same inputs, cached value will be returned instead.
100
+ """
101
+ name: str
102
+ source: str = field(default="")
103
+ inputs: Mapping[str, str] = field(default_factory=lambda: {})
104
+ formula: Callable = field(default=None)
105
+ cacheable: bool = field(default=False)
106
+
107
+ def __hash__(self):
108
+ return hash((self.source, self.name))
109
+
110
+ def __eq__(self, other):
111
+ return isinstance(other, Variable) and self.name == other.name and self.source == other.source
112
+
113
+ def qual_name(self) -> str:
114
+ return _make_qual_name(self.source, self.name)
115
+
116
+
117
+ class Variables:
118
+ def __init__(
119
+ self,
120
+ name: str,
121
+ variables: List[VarSpec],
122
+ ):
123
+ """
124
+ Namespace of `Variable` instances.
125
+
126
+ :param name: name of the `Variables` instance namespace.
127
+ :param variables: initializer list of `VarSpec` specs that can be used
128
+ to create instances of `Variable` to be stored in this namespace.
129
+ """
130
+ self.name = name
131
+ self.variables = {variable["name"]: Variable(source=self.name, **variable) for variable in variables}
132
+
133
+ def __getitem__(self, variable_name: str) -> Variable:
134
+ """
135
+ :param variable_name: name of the `Variable` to retrieve,
136
+ it is expected that `Variables` and `External` all expose
137
+ this method that returns an instance of the `Variable` with
138
+ the given name in either a `Variables` namespace or an
139
+ `External` namespace, respectively.
140
+ """
141
+ return self.variables[variable_name]
142
+
143
+
144
+ @dataclass
145
+ class Value:
146
+ """
147
+ Value of the corresponding `Variable`.
148
+ """
149
+ variable: Variable
150
+ value: VarValue | _Null = field(default_factory=lambda: _null)
151
+
152
+
153
+ class Values:
154
+ """ Values of all `Variable`s, computed and external,
155
+ will be held here. """
156
+ def __init__(self):
157
+ self.values: Dict[Variable, Value] = {}
158
+ self.cache: Dict[tuple, VarValue] = {}
159
+
160
+ def get(self, variable: Variable) -> Value:
161
+ if variable not in self.values:
162
+ self.values[variable] = Value(variable=variable)
163
+ return self.values[variable]
164
+
165
+
166
+ @dataclass(frozen=True)
167
+ class CompiledNode:
168
+ variable: Variable
169
+ inputs: List[Variable]
170
+
171
+ def __post_init__(self):
172
+ if self.variable.formula is None and self.inputs:
173
+ raise RuntimeError(f"{self.variable} contains inputs but no formula, how come?")
174
+
175
+ def __hash__(self):
176
+ return hash((self.variable.source, self.variable.name))
177
+
178
+ def __eq__(self, other):
179
+ return (
180
+ isinstance(other, CompiledNode) and
181
+ self.variable.name == other.variable.name and
182
+ self.variable.source == other.variable.source
183
+ )
184
+
185
+
186
+ @dataclass(frozen=True)
187
+ class CompiledGraph:
188
+ ordered_nodes: List[CompiledNode]
189
+ required_external_variables: Dict[str, Dict[str, Variable]]
190
+ dependents: Dict[Variable, List[CompiledNode]] = field(default_factory=lambda: defaultdict(list))
191
+
192
+ def __post_init__(self):
193
+ for node in self.ordered_nodes:
194
+ for inp in node.inputs:
195
+ self.dependents[inp].append(node)
196
+
197
+ def execute(
198
+ self,
199
+ external_values: Dict[str, Dict[str, VarValue]],
200
+ values: Values,
201
+ ):
202
+ """
203
+ Main entry point to calculation of compiled graph.
204
+ Calculation requires the following inputs:
205
+
206
+ :param external_values: actual values of all required external
207
+ variables, this can be a superset of what is really needed for
208
+ the calculation. The map is first from the name of the external
209
+ source and then from the name of the variable within that
210
+ source to the variable's actual value.
211
+ :param values: runtime storage of all values, instance of `Values`.
212
+ :return:
213
+ """
214
+ self._assign_external_values(external_values, values)
215
+ self._calculate(self.ordered_nodes, values)
216
+
217
+ def _assign_external_values(
218
+ self,
219
+ external_values: Dict[str, Dict[str, VarValue]],
220
+ values: Values
221
+ ):
222
+ """
223
+ For the external variables required for this calculation,
224
+ populate their values into the `Values` container.
225
+
226
+ :param external_values: mapping from names of external sources
227
+ to dictionary from names of external `Variable`s to their instances,
228
+ that are needed for the given calculation.
229
+ :param external_values: dictionary of `External` name to a mapping
230
+ of `Variable` names to their values.
231
+ :param values: an instance of `Values` storage of all calculated
232
+ values.
233
+ """
234
+ for source_name, variables in self.required_external_variables.items():
235
+ provided = external_values.get(source_name)
236
+ if provided is None:
237
+ raise KeyError(f"Missing external source '{source_name}'")
238
+ for var_name, variable in variables.items():
239
+ if var_name not in provided:
240
+ raise KeyError(
241
+ f"Missing value for external variable '{source_name}.{var_name}'"
242
+ )
243
+ values.get(variable).value = provided[var_name]
244
+
245
+ def _collect_affected(self, changed_vars: Set[Variable]) -> List[CompiledNode]:
246
+ """
247
+ Return subset of `self.ordered_nodes` consisting of nodes
248
+ affected by `changed_vars`, in the same order as in the original.
249
+ """
250
+ affected = set()
251
+ stack = list(changed_vars)
252
+ while stack:
253
+ v = stack.pop()
254
+ for node in self.dependents.get(v, []):
255
+ if node not in affected:
256
+ affected.add(node)
257
+ stack.append(node.variable)
258
+ return [node for node in self.ordered_nodes if node in affected]
259
+
260
+ @staticmethod
261
+ def _calculate(nodes: List[CompiledNode], values: Values):
262
+ """
263
+ Calculate the values of the `Variable`s using their own callables
264
+ by evaluating them as functions of the values of the specified
265
+ inputs.
266
+
267
+ All inputs need to be calculated first (i.e., to be non-`_null`)
268
+ before the value of the given `Variable` can be `calculate`d.
269
+
270
+ This is possible because the `Variable`s are supplied as a
271
+ topologically ordered list `ordered_variables`.
272
+ """
273
+ for node in nodes:
274
+ if node.variable.formula is None:
275
+ continue
276
+ args = tuple(values.get(input_).value for input_ in node.inputs)
277
+ # assert _null not in args, f"Uninitialized input for {node.variable}, args = {args}"
278
+ cache_key = (node.variable, args)
279
+ if node.variable.cacheable:
280
+ if cache_key in values.cache:
281
+ values.get(node.variable).value = values.cache[cache_key]
282
+ continue
283
+ result = node.variable.formula(*args)
284
+ if node.variable.cacheable:
285
+ values.cache[cache_key] = result
286
+ values.get(node.variable).value = result
287
+
288
+ def recompute(self, changed: Dict[str, Dict[str, VarValue]], values: Values):
289
+ """
290
+ :param changed: dict of sources to names to new values of changed `Variable`s.
291
+ :param values: storage of all the `Variable` values.
292
+ """
293
+ changed_vars = set()
294
+ for src, vals in changed.items():
295
+ for name, val in vals.items():
296
+ variable = self.required_external_variables.get(src, {}).get(name)
297
+ qual = _make_qual_name(src, name)
298
+ if variable is None:
299
+ try:
300
+ variable = next(n.variable for n in self.ordered_nodes if n.variable.qual_name() == qual)
301
+ except StopIteration:
302
+ warnings.warn(f"{qual} is not in the calculation path, update has no effect.")
303
+ continue
304
+ values.get(variable).value = val
305
+ changed_vars.add(variable)
306
+ affected_nodes = self._collect_affected(changed_vars)
307
+ for node in affected_nodes:
308
+ values.get(node.variable).value = _null
309
+ self._calculate(affected_nodes, values)
310
+
311
+
312
+ class Graph:
313
+ def __init__(
314
+ self,
315
+ variables_lists: Dict[str, List[VarSpec]],
316
+ external_source_names: List[str]
317
+ ):
318
+ """
319
+ :param variables_lists: mapping of names of `Variables`
320
+ namespace to the list of `Variable` instances to be added
321
+ to that namespace.
322
+ :param external_source_names: list of names of possible
323
+ `External` sources from which `Variable` inputs might
324
+ be coming from.
325
+ """
326
+ self.external_source_names = external_source_names
327
+ self.registry = {}
328
+ self.external = {
329
+ external_source_name: External(external_source_name) for external_source_name in external_source_names
330
+ }
331
+ for variables_name, variables_list in variables_lists.items():
332
+ assert variables_name not in self.registry, (
333
+ f"Variables instance {variables_name} has already been created in this registry"
334
+ )
335
+ variables = Variables(
336
+ name=variables_name,
337
+ variables=variables_list
338
+ )
339
+ self.registry[variables_name] = variables
340
+ for external_name, external_ in self.external.items():
341
+ registered_external = self.registry.get(external_name)
342
+ if registered_external is not None:
343
+ assert registered_external == external_, (
344
+ f"{external_name} external already registered as {registered_external}"
345
+ )
346
+ else:
347
+ self.registry[external_name] = external_
348
+ self.compiled_graphs = {}
349
+
350
+ def compile(self, required: List[str] | str) -> CompiledGraph:
351
+ if isinstance(required, str):
352
+ required = [required]
353
+ required_tup = tuple(sorted(required))
354
+ compiled_graph = self.compiled_graphs.get(required_tup)
355
+ if compiled_graph is not None:
356
+ return compiled_graph
357
+ ordered_variables, used_external_vars = self._topological_order(required)
358
+ ordered_nodes = [
359
+ CompiledNode(
360
+ variable=var,
361
+ inputs=[self.registry[var.inputs[input_name]][input_name] for input_name in var.inputs.keys()]
362
+ ) for var in ordered_variables
363
+ ]
364
+ required_external_variables = self._required_external_variables(used_external_vars)
365
+ compiled = CompiledGraph(
366
+ ordered_nodes=ordered_nodes,
367
+ required_external_variables=required_external_variables,
368
+ )
369
+ self.compiled_graphs[required_tup] = compiled
370
+ return compiled
371
+
372
+ def _get_source(self, source_name: str) -> Namespace:
373
+ """
374
+ :param source_name: name of the source (either an instance
375
+ of `Variables` or an `External` source) that is requested.
376
+ """
377
+ variables_source = self.registry.get(source_name)
378
+ if variables_source is not None:
379
+ return variables_source
380
+ raise KeyError(f"Unknown source {source_name}")
381
+
382
+ def _topological_order(self, required: List | Tuple | str):
383
+ """
384
+ :param required: qualified name(s) of `Variable` instance(s)
385
+ for which a topological ordering of a DAG is to be determined.
386
+ """
387
+ if isinstance(required, str):
388
+ required = [required]
389
+
390
+ visited = set()
391
+ visiting = set()
392
+ ordered_variables = []
393
+
394
+ used_external_vars: Set[Variable] = set()
395
+
396
+ def visit(qual_name: str):
397
+ if qual_name in visited:
398
+ return
399
+ if qual_name in visiting:
400
+ raise RuntimeError(f"Cycle detected at {qual_name}")
401
+ visiting.add(qual_name)
402
+ source_name, variable_name = qual_name.split(QUAL_SEP)
403
+ source = self._get_source(source_name)
404
+ variable = source[variable_name]
405
+ if isinstance(source, External):
406
+ used_external_vars.add(variable)
407
+ for input_name, input_source in variable.inputs.items():
408
+ visit(_make_qual_name(input_source, input_name))
409
+ visiting.remove(qual_name)
410
+ visited.add(qual_name)
411
+ ordered_variables.append(variable)
412
+
413
+ for r in required:
414
+ visit(r)
415
+ return ordered_variables, used_external_vars
416
+
417
+ @staticmethod
418
+ def _required_external_variables(used_external_vars: Set[Variable]) -> Dict[str, Dict[str, Variable]]:
419
+ """
420
+ For requested `External` sources, return the list of required
421
+ external `Variable` instances.
422
+ """
423
+ required_external_variables = defaultdict(dict)
424
+ for variable in used_external_vars:
425
+ variable_name = variable.name
426
+ variable_source = variable.source
427
+ required_external_variables[variable_source][variable_name] = variable
428
+ return required_external_variables
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: numbox
3
- Version: 0.2.17
3
+ Version: 0.3.1
4
4
  Author: Mikhail Goykhman
5
5
  License: MIT License (with Citation Clause)
6
6
 
@@ -50,8 +50,9 @@ Documentation is available at [numbox](https://goykhman.github.io/numbox).
50
50
 
51
51
  - **Any**: A lightweight structure that wraps any value into the same type leveraging a variant of the type erasure technique.
52
52
  - **Bindings**: JIT-wrappers of functions imported from dynamically linked libraries.
53
- - **Node** A lightweight JIT-compatible graph node with type-erased dependencies in a uniform dynamically-sized vector (numba List).
53
+ - **Node**: A lightweight JIT-compatible graph node with type-erased dependencies in a uniform dynamically-sized vector (numba List).
54
54
  - **Proxy**: Create a proxy for a decorated function with specified signatures, enabling efficient JIT compilation and caching.
55
+ - **Variable**: Pure Python framework of a graph calculation. JIT-compiled dispatchers can be invoked by it.
55
56
  - **Work**: JIT-compatible unit of calculation work with dependencies, inner states, and custom calculation.
56
57
 
57
58
  ## Installation
@@ -1,4 +1,4 @@
1
- numbox/__init__.py,sha256=iGaTwv2hqOZnwUd0PLrIfw3ILV1NHyCT3vlxnvDunLw,23
1
+ numbox/__init__.py,sha256=TZkGuMIRSRmUY3XCIs5owt2o60vXyqYMHWIkhx65uYE,22
2
2
  numbox/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  numbox/core/configurations.py,sha256=0bCmxXL-QMwtvyIDhpXLeT-1KJMf_QpH0wLuEvYLGxQ,68
4
4
  numbox/core/any/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -14,6 +14,8 @@ numbox/core/bindings/signatures.py,sha256=OcSBDpJ422eoWkJXxHPEanMNbVB7bq9f5bRq5L
14
14
  numbox/core/bindings/utils.py,sha256=OATfF4k8e5oPa9_wlHHkLQhY_DhrPNKYdeeGu9Nj5yg,1180
15
15
  numbox/core/proxy/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
16
  numbox/core/proxy/proxy.py,sha256=Wt7yzswDmeQXt0yjcTcnLi2coneowSHWXy_IFpZZJMU,3612
17
+ numbox/core/variable/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
+ numbox/core/variable/variable.py,sha256=GoCsB1hUDJJzZByK1OK13RZn4M-maBDjXfnbvRFjcS8,16371
17
19
  numbox/core/work/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
20
  numbox/core/work/builder.py,sha256=d0DRwJoyskp-6tYQyV1VE-v9eX99qJbQJ_FdAFrjuiE,6273
19
21
  numbox/core/work/builder_utils.py,sha256=z8au1x10AwnzQ0_MAbQ6DnKTp3u9HeYZ1jyfkUMYjVg,1213
@@ -33,8 +35,8 @@ numbox/utils/meminfo.py,sha256=ykFi8Vt0WcHI3ztgMwvpn6NqaflDSQGL8tjI01jrzm0,1759
33
35
  numbox/utils/standard.py,sha256=SPsQcyLZw21RaNCdfkIGE_QBaVnMtZjJY4F40_GGuak,347
34
36
  numbox/utils/timer.py,sha256=5_d690Fb3L2axJBRxtoB0qe23exBosNR4qu6cno4QfY,641
35
37
  numbox/utils/void_type.py,sha256=IkZsjNeAIShYJtvWbvERdHnl_mbF1rCRWiM3gp6II8U,404
36
- numbox-0.2.17.dist-info/LICENSE,sha256=YYgNvjH_p6-1NsdrIqGJnr1GUbZzA_8DxsP6vVfM6nY,1446
37
- numbox-0.2.17.dist-info/METADATA,sha256=SVbtLGXFHz4gsHrrgt0lCkSDImYIrWH1A9RGfjK2MeM,2885
38
- numbox-0.2.17.dist-info/WHEEL,sha256=WnJ8fYhv8N4SYVK2lLYNI6N0kVATA7b0piVUNvqIIJE,91
39
- numbox-0.2.17.dist-info/top_level.txt,sha256=A67jOkfqidCSYYm6ifjN_WZyIiR1B27fjxv6nNbPvjc,7
40
- numbox-0.2.17.dist-info/RECORD,,
38
+ numbox-0.3.1.dist-info/LICENSE,sha256=YYgNvjH_p6-1NsdrIqGJnr1GUbZzA_8DxsP6vVfM6nY,1446
39
+ numbox-0.3.1.dist-info/METADATA,sha256=L2Ol4D5QRR_K8j31xvFgbdJtxFiFLDJG2MPWrmS81Zk,2996
40
+ numbox-0.3.1.dist-info/WHEEL,sha256=WnJ8fYhv8N4SYVK2lLYNI6N0kVATA7b0piVUNvqIIJE,91
41
+ numbox-0.3.1.dist-info/top_level.txt,sha256=A67jOkfqidCSYYm6ifjN_WZyIiR1B27fjxv6nNbPvjc,7
42
+ numbox-0.3.1.dist-info/RECORD,,