MATE-contract 1.0.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.
- MATE_contract/__init__.py +52 -0
- MATE_contract/_base.py +292 -0
- MATE_contract/_context.py +215 -0
- MATE_contract/_declarations.py +278 -0
- MATE_contract/_domains.py +774 -0
- MATE_contract/_exchange.py +372 -0
- MATE_contract/_interfaces.py +596 -0
- MATE_contract/_keys.py +226 -0
- MATE_contract/_phenotype.py +285 -0
- MATE_contract/_specs.py +953 -0
- mate_contract-1.0.0.dist-info/METADATA +51 -0
- mate_contract-1.0.0.dist-info/RECORD +15 -0
- mate_contract-1.0.0.dist-info/WHEEL +4 -0
- mate_contract-1.0.0.dist-info/licenses/COPYING +674 -0
- mate_contract-1.0.0.dist-info/licenses/COPYING.LESSER +165 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""MATE contract: the exchange types and interfaces shared by all MATE modules.
|
|
2
|
+
|
|
3
|
+
A system and a method depend only on this distribution (and NumPy). Every public name is
|
|
4
|
+
imported from here; the submodules are private.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from importlib.metadata import version as _distribution_version
|
|
8
|
+
|
|
9
|
+
from ._context import Context, EventSink, Logger
|
|
10
|
+
from ._declarations import Capabilities, Device, Requirements
|
|
11
|
+
from ._domains import Categorical, Domain, Integer, Log, Ordinal, Real
|
|
12
|
+
from ._exchange import Budget, Candidate, Environment, Invalid, Operation, Partial, Performance
|
|
13
|
+
from ._interfaces import Genome, Method, System
|
|
14
|
+
from ._keys import Key
|
|
15
|
+
from ._phenotype import Network, Params, Phenotype, Program
|
|
16
|
+
from ._specs import (
|
|
17
|
+
EnvironmentSpec,
|
|
18
|
+
EvaluationSpec,
|
|
19
|
+
FieldArray,
|
|
20
|
+
NetworkSpec,
|
|
21
|
+
ObservableSpec,
|
|
22
|
+
ParamsSpec,
|
|
23
|
+
PerformanceSpec,
|
|
24
|
+
PhenotypeSpec,
|
|
25
|
+
ProgramSpec,
|
|
26
|
+
Role,
|
|
27
|
+
Setting,
|
|
28
|
+
TraceSpec,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
__version__ = _distribution_version("MATE-contract")
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
# _domains
|
|
35
|
+
"Domain", "Real", "Log", "Integer", "Ordinal", "Categorical",
|
|
36
|
+
# _keys
|
|
37
|
+
"Key",
|
|
38
|
+
# _specs
|
|
39
|
+
"FieldArray", "ParamsSpec", "NetworkSpec", "ProgramSpec", "PhenotypeSpec",
|
|
40
|
+
"EnvironmentSpec", "TraceSpec", "PerformanceSpec", "ObservableSpec",
|
|
41
|
+
"Role", "EvaluationSpec", "Setting",
|
|
42
|
+
# _phenotype
|
|
43
|
+
"Params", "Network", "Program", "Phenotype",
|
|
44
|
+
# _exchange
|
|
45
|
+
"Budget", "Partial", "Performance", "Operation", "Invalid", "Candidate", "Environment",
|
|
46
|
+
# _declarations
|
|
47
|
+
"Requirements", "Capabilities", "Device",
|
|
48
|
+
# _context
|
|
49
|
+
"Context", "Logger", "EventSink",
|
|
50
|
+
# _interfaces
|
|
51
|
+
"System", "Method", "Genome",
|
|
52
|
+
] # fmt: skip
|
MATE_contract/_base.py
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"""Shared helpers of the contract types: closed families, read-only storage, value equality, names.
|
|
2
|
+
|
|
3
|
+
Everything here is private to ``MATE_contract``. The public behaviours it implements are those of
|
|
4
|
+
the contract specification: closed families ([K] §3 "Enforcement", P1b), arrays and mappings
|
|
5
|
+
stored as private read-only copies ([K] §1 "Immutability"), value equality of array-holding
|
|
6
|
+
types ([K] §1.1) and the checks on names ([K] OB3, NS2).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import dataclasses
|
|
10
|
+
import numbers
|
|
11
|
+
from collections.abc import Hashable, ItemsView, Iterable, Iterator, KeysView, Mapping, ValuesView
|
|
12
|
+
from typing import Any, ClassVar
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
from numpy.typing import ArrayLike, DTypeLike
|
|
16
|
+
|
|
17
|
+
_PACKAGE = "MATE_contract"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _in_contract(module: str) -> bool:
|
|
21
|
+
"""Tell whether a module name belongs to the contract distribution."""
|
|
22
|
+
return module == _PACKAGE or module.startswith(_PACKAGE + ".")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Closed:
|
|
26
|
+
"""Mixin making a class family closed to classes defined outside ``MATE_contract``.
|
|
27
|
+
|
|
28
|
+
A direct subclass of ``Closed`` is the root of a family. Any class of the family, the root or
|
|
29
|
+
a member, can be subclassed only by a class whose module belongs to ``MATE_contract``.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init_subclass__(cls, **kwargs: Any) -> None:
|
|
33
|
+
super().__init_subclass__(**kwargs)
|
|
34
|
+
root = _family_root(cls)
|
|
35
|
+
if root is not cls and not _in_contract(cls.__module__):
|
|
36
|
+
raise TypeError(
|
|
37
|
+
f"{root.__name__} is a closed family: {cls.__qualname__} cannot subclass it; "
|
|
38
|
+
f"see {_PACKAGE}.{root.__name__}"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _family_root(cls: type) -> type:
|
|
43
|
+
"""Return the root of the closed family ``cls`` belongs to (the class listing ``Closed``)."""
|
|
44
|
+
for klass in cls.__mro__:
|
|
45
|
+
if Closed in klass.__bases__:
|
|
46
|
+
return klass
|
|
47
|
+
return cls
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def check_exact_type(value: object, types: tuple[type, ...], where: str) -> None:
|
|
51
|
+
"""Raise ``TypeError`` unless ``type(value)`` is exactly one of ``types``.
|
|
52
|
+
|
|
53
|
+
The membership test of closed families is by exact type ([K] §3, P1b), never by
|
|
54
|
+
``isinstance``.
|
|
55
|
+
"""
|
|
56
|
+
if type(value) not in types:
|
|
57
|
+
names = ", ".join(t.__name__ for t in types)
|
|
58
|
+
raise TypeError(f"{where}: {type(value).__name__} is not one of {names}")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def readonly(a: ArrayLike, dtype: DTypeLike | None = None) -> np.ndarray:
|
|
62
|
+
"""Return a private read-only copy of ``np.asarray(a, dtype)``.
|
|
63
|
+
|
|
64
|
+
The copy is made even when ``a`` is already an array of that dtype: a caller who later
|
|
65
|
+
writes into its own array (a buffer reused across genomes, say) does not change the object
|
|
66
|
+
that stores the copy ([K] §1 "Immutability").
|
|
67
|
+
"""
|
|
68
|
+
arr = np.array(a, dtype=dtype, copy=True)
|
|
69
|
+
arr.flags.writeable = False
|
|
70
|
+
return arr
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class ReadOnlyMapping(Mapping[Any, Any]):
|
|
74
|
+
"""Read-only mapping holding its own copy of the items it is built from.
|
|
75
|
+
|
|
76
|
+
It takes the place of ``types.MappingProxyType``, which cannot be pickled, so that a module
|
|
77
|
+
keeping a contract mapping (``self.fields = spec.fields``) can still be sent to a worker
|
|
78
|
+
process. It pickles and deep-copies through :meth:`__reduce__`, compares equal to any
|
|
79
|
+
mapping with the same items, is unhashable, prints as a ``dict`` and keeps the read
|
|
80
|
+
methods of ``MappingProxyType``: ``copy()`` and ``|`` return a ``dict``.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
__slots__ = ("_items",)
|
|
84
|
+
|
|
85
|
+
_items: dict[Any, Any]
|
|
86
|
+
|
|
87
|
+
def __init__(self, items: Mapping[Any, Any] | Iterable[tuple[Any, Any]] = ()) -> None:
|
|
88
|
+
object.__setattr__(self, "_items", dict(items))
|
|
89
|
+
|
|
90
|
+
def __getitem__(self, key: Any) -> Any:
|
|
91
|
+
return self._items[key]
|
|
92
|
+
|
|
93
|
+
def __iter__(self) -> Iterator[Any]:
|
|
94
|
+
return iter(self._items)
|
|
95
|
+
|
|
96
|
+
def __len__(self) -> int:
|
|
97
|
+
return len(self._items)
|
|
98
|
+
|
|
99
|
+
def __contains__(self, key: object) -> bool:
|
|
100
|
+
return key in self._items
|
|
101
|
+
|
|
102
|
+
def __reversed__(self) -> Iterator[Any]:
|
|
103
|
+
return reversed(self._items)
|
|
104
|
+
|
|
105
|
+
def get(self, key: Any, default: Any = None) -> Any:
|
|
106
|
+
return self._items.get(key, default)
|
|
107
|
+
|
|
108
|
+
def keys(self) -> KeysView[Any]:
|
|
109
|
+
return self._items.keys()
|
|
110
|
+
|
|
111
|
+
def values(self) -> ValuesView[Any]:
|
|
112
|
+
return self._items.values()
|
|
113
|
+
|
|
114
|
+
def items(self) -> ItemsView[Any, Any]:
|
|
115
|
+
return self._items.items()
|
|
116
|
+
|
|
117
|
+
def copy(self) -> dict[Any, Any]:
|
|
118
|
+
"""Return the items as a new ``dict``."""
|
|
119
|
+
return dict(self._items)
|
|
120
|
+
|
|
121
|
+
def __or__(self, other: object) -> dict[Any, Any]:
|
|
122
|
+
if not isinstance(other, Mapping):
|
|
123
|
+
return NotImplemented
|
|
124
|
+
return {**self._items, **other}
|
|
125
|
+
|
|
126
|
+
def __ror__(self, other: object) -> dict[Any, Any]:
|
|
127
|
+
if not isinstance(other, Mapping):
|
|
128
|
+
return NotImplemented
|
|
129
|
+
return {**other, **self._items}
|
|
130
|
+
|
|
131
|
+
def __eq__(self, other: object) -> bool:
|
|
132
|
+
if isinstance(other, ReadOnlyMapping):
|
|
133
|
+
other = other._items
|
|
134
|
+
elif not isinstance(other, Mapping):
|
|
135
|
+
return NotImplemented
|
|
136
|
+
elif not isinstance(other, dict):
|
|
137
|
+
other = dict(other.items())
|
|
138
|
+
return self._items == other
|
|
139
|
+
|
|
140
|
+
__hash__ = None # type: ignore[assignment]
|
|
141
|
+
|
|
142
|
+
def __repr__(self) -> str:
|
|
143
|
+
return repr(self._items)
|
|
144
|
+
|
|
145
|
+
def __reduce__(self) -> tuple[Any, ...]:
|
|
146
|
+
return (ReadOnlyMapping, (self._items,))
|
|
147
|
+
|
|
148
|
+
def __setattr__(self, name: str, value: object) -> None:
|
|
149
|
+
raise AttributeError(f"{type(self).__name__} is read-only")
|
|
150
|
+
|
|
151
|
+
def __delattr__(self, name: str) -> None:
|
|
152
|
+
raise AttributeError(f"{type(self).__name__} is read-only")
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def frozen_mapping(mapping: Mapping[str, Any], where: str) -> ReadOnlyMapping:
|
|
156
|
+
"""Return a read-only copy of ``mapping`` whose keys are checked names.
|
|
157
|
+
|
|
158
|
+
Raises
|
|
159
|
+
------
|
|
160
|
+
TypeError
|
|
161
|
+
If ``mapping`` is not a mapping, or a key is not a ``str``.
|
|
162
|
+
ValueError
|
|
163
|
+
If a key is the empty string.
|
|
164
|
+
"""
|
|
165
|
+
if not isinstance(mapping, Mapping):
|
|
166
|
+
raise TypeError(f"{where}: expected a mapping, got {type(mapping).__name__}")
|
|
167
|
+
for key in mapping:
|
|
168
|
+
check_name(key, where)
|
|
169
|
+
return ReadOnlyMapping(mapping)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def check_name(name: object, where: str) -> str:
|
|
173
|
+
"""Return ``name`` if it is a non-empty ``str``.
|
|
174
|
+
|
|
175
|
+
Raises
|
|
176
|
+
------
|
|
177
|
+
TypeError
|
|
178
|
+
If ``name`` is not a ``str``.
|
|
179
|
+
ValueError
|
|
180
|
+
If ``name`` is empty.
|
|
181
|
+
"""
|
|
182
|
+
if not isinstance(name, str):
|
|
183
|
+
raise TypeError(f"{where}: a name must be a str, got {type(name).__name__} {name!r}")
|
|
184
|
+
if not name:
|
|
185
|
+
raise ValueError(f"{where}: a name must be a non-empty str")
|
|
186
|
+
return name
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def check_names(names: Iterable[object], where: str) -> tuple[str, ...]:
|
|
190
|
+
"""Return ``names`` as a tuple of distinct non-empty strings.
|
|
191
|
+
|
|
192
|
+
Raises
|
|
193
|
+
------
|
|
194
|
+
TypeError
|
|
195
|
+
If ``names`` is a single ``str`` or not iterable, or an element is not a ``str``.
|
|
196
|
+
ValueError
|
|
197
|
+
If an element is empty or repeated.
|
|
198
|
+
"""
|
|
199
|
+
result = tuple(check_name(name, where) for name in as_tuple(names, where))
|
|
200
|
+
seen: set[str] = set()
|
|
201
|
+
for name in result:
|
|
202
|
+
if name in seen:
|
|
203
|
+
raise ValueError(
|
|
204
|
+
f"{where}: name '{name}' appears more than once; names must be distinct"
|
|
205
|
+
)
|
|
206
|
+
seen.add(name)
|
|
207
|
+
return result
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def as_tuple(values: Iterable[Any], where: str) -> tuple[Any, ...]:
|
|
211
|
+
"""Return ``values`` as a tuple; a ``str`` or ``bytes`` is refused, not split into items."""
|
|
212
|
+
if isinstance(values, (str, bytes)):
|
|
213
|
+
raise TypeError(f"{where}: expected a sequence, got {type(values).__name__} {values!r}")
|
|
214
|
+
try:
|
|
215
|
+
return tuple(values)
|
|
216
|
+
except TypeError:
|
|
217
|
+
raise TypeError(f"{where}: expected a sequence, got {type(values).__name__}") from None
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def check_hashable(value: object, where: str) -> Hashable:
|
|
221
|
+
"""Return ``value`` if it is hashable, else raise ``TypeError``."""
|
|
222
|
+
try:
|
|
223
|
+
hash(value)
|
|
224
|
+
except TypeError:
|
|
225
|
+
raise TypeError(f"{where}: {type(value).__name__} {value!r} is not hashable") from None
|
|
226
|
+
return value
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def is_bool(value: object) -> bool:
|
|
230
|
+
"""Tell whether ``value`` is a Python or numpy boolean (refused where an int is expected)."""
|
|
231
|
+
return isinstance(value, (bool, np.bool_))
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def as_int(value: object, where: str) -> int:
|
|
235
|
+
"""Return ``value`` as a Python ``int``; booleans and non-integral types raise ``TypeError``."""
|
|
236
|
+
if is_bool(value) or not isinstance(value, numbers.Integral):
|
|
237
|
+
raise TypeError(f"{where}: expected an int, got {type(value).__name__} {value!r}")
|
|
238
|
+
return int(value)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def values_equal(a: object, b: object) -> bool:
|
|
242
|
+
"""Compare two contract values by the value-equality rule of [K] §1.1.
|
|
243
|
+
|
|
244
|
+
Arrays are equal when dtype, shape and bytes are equal (so NaN equals NaN of the same bit
|
|
245
|
+
pattern and ``0.0 != -0.0``); mappings when they have the same keys and equal values; tuples
|
|
246
|
+
and lists elementwise; anything else by ``==``.
|
|
247
|
+
"""
|
|
248
|
+
if isinstance(a, np.ndarray) or isinstance(b, np.ndarray):
|
|
249
|
+
if not (isinstance(a, np.ndarray) and isinstance(b, np.ndarray)):
|
|
250
|
+
return False
|
|
251
|
+
return (
|
|
252
|
+
a.dtype == b.dtype
|
|
253
|
+
and a.shape == b.shape
|
|
254
|
+
and np.ascontiguousarray(a).tobytes() == np.ascontiguousarray(b).tobytes()
|
|
255
|
+
)
|
|
256
|
+
if isinstance(a, Mapping) or isinstance(b, Mapping):
|
|
257
|
+
if not (isinstance(a, Mapping) and isinstance(b, Mapping)):
|
|
258
|
+
return False
|
|
259
|
+
return a.keys() == b.keys() and all(values_equal(a[k], b[k]) for k in a)
|
|
260
|
+
if isinstance(a, (tuple, list)) or isinstance(b, (tuple, list)):
|
|
261
|
+
if type(a) is not type(b) or len(a) != len(b):
|
|
262
|
+
return False
|
|
263
|
+
return all(values_equal(x, y) for x, y in zip(a, b))
|
|
264
|
+
result = a == b
|
|
265
|
+
return isinstance(result, (bool, np.bool_)) and bool(result)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
class ValueEquality:
|
|
269
|
+
"""Mixin giving a dataclass the value equality of [K] §1.1 and making it unhashable.
|
|
270
|
+
|
|
271
|
+
Two instances are equal when they have the same exact type and their comparison keys are
|
|
272
|
+
equal under :func:`values_equal`. The key is the tuple of dataclass field values, minus the
|
|
273
|
+
fields named in ``_eq_ignore``; a class overrides :meth:`_eq_key` to normalise a field (for
|
|
274
|
+
instance an absent ``filled`` mask, equal to an all-true mask).
|
|
275
|
+
"""
|
|
276
|
+
|
|
277
|
+
_eq_ignore: ClassVar[tuple[str, ...]] = ()
|
|
278
|
+
|
|
279
|
+
def _eq_key(self) -> tuple[Any, ...]:
|
|
280
|
+
"""Return the values compared by ``==``."""
|
|
281
|
+
return tuple(
|
|
282
|
+
getattr(self, f.name)
|
|
283
|
+
for f in dataclasses.fields(self) # type: ignore[arg-type]
|
|
284
|
+
if f.compare and f.name not in self._eq_ignore
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
def __eq__(self, other: object) -> bool:
|
|
288
|
+
if type(other) is not type(self):
|
|
289
|
+
return NotImplemented
|
|
290
|
+
return values_equal(self._eq_key(), other._eq_key()) # type: ignore[attr-defined]
|
|
291
|
+
|
|
292
|
+
__hash__ = None # type: ignore[assignment]
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""The execution context of a module call, and its two output streams ([K] §8.2-8.3).
|
|
2
|
+
|
|
3
|
+
Every module call receives a :class:`Context`: its random stream and key, its logger (messages
|
|
4
|
+
for humans), its event sink (structured events for machines), its configuration block and the
|
|
5
|
+
run's budget. The runtime passes its own subclass, which extends the log path in
|
|
6
|
+
:meth:`Context.spawn` and implements the checkpoint hooks of a system that owns its loop; a
|
|
7
|
+
module author can build a plain :class:`Context` to test a module without the runtime.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import dataclasses
|
|
11
|
+
from collections.abc import Callable, Mapping
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Protocol
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
|
|
18
|
+
from ._declarations import Device
|
|
19
|
+
from ._exchange import Budget
|
|
20
|
+
from ._keys import Key
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Logger(Protocol):
|
|
24
|
+
"""Journal of messages for humans, filtered by a verbosity level.
|
|
25
|
+
|
|
26
|
+
Any object with these four methods is a logger. A message is formatted as ``msg % args``
|
|
27
|
+
only after the level test, so that a discarded message costs one comparison.
|
|
28
|
+
|
|
29
|
+
Notes
|
|
30
|
+
-----
|
|
31
|
+
Rule LG1.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def debug(self, msg: str, /, *args: object) -> None:
|
|
35
|
+
"""Log a debugging message.
|
|
36
|
+
|
|
37
|
+
Parameters
|
|
38
|
+
----------
|
|
39
|
+
msg : str
|
|
40
|
+
Message, a ``%``-format string when ``args`` are given.
|
|
41
|
+
*args : object
|
|
42
|
+
Values formatted into ``msg`` after the level test.
|
|
43
|
+
"""
|
|
44
|
+
...
|
|
45
|
+
|
|
46
|
+
def info(self, msg: str, /, *args: object) -> None:
|
|
47
|
+
"""Log an informative message.
|
|
48
|
+
|
|
49
|
+
Parameters
|
|
50
|
+
----------
|
|
51
|
+
msg : str
|
|
52
|
+
Message, a ``%``-format string when ``args`` are given.
|
|
53
|
+
*args : object
|
|
54
|
+
Values formatted into ``msg`` after the level test.
|
|
55
|
+
"""
|
|
56
|
+
...
|
|
57
|
+
|
|
58
|
+
def warning(self, msg: str, /, *args: object) -> None:
|
|
59
|
+
"""Log a warning.
|
|
60
|
+
|
|
61
|
+
Parameters
|
|
62
|
+
----------
|
|
63
|
+
msg : str
|
|
64
|
+
Message, a ``%``-format string when ``args`` are given.
|
|
65
|
+
*args : object
|
|
66
|
+
Values formatted into ``msg`` after the level test.
|
|
67
|
+
"""
|
|
68
|
+
...
|
|
69
|
+
|
|
70
|
+
def error(self, msg: str, /, *args: object) -> None:
|
|
71
|
+
"""Log an error.
|
|
72
|
+
|
|
73
|
+
Parameters
|
|
74
|
+
----------
|
|
75
|
+
msg : str
|
|
76
|
+
Message, a ``%``-format string when ``args`` are given.
|
|
77
|
+
*args : object
|
|
78
|
+
Values formatted into ``msg`` after the level test.
|
|
79
|
+
"""
|
|
80
|
+
...
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class EventSink(Protocol):
|
|
84
|
+
"""Receiver of structured events: a name and typed fields.
|
|
85
|
+
|
|
86
|
+
``ctx.emit(name, value=v)`` publishes a value of a declared series or summary;
|
|
87
|
+
``ctx.emit(name, **fields)`` for a declared trace publishes one record; any other name is
|
|
88
|
+
a free event. Field values are plain data.
|
|
89
|
+
|
|
90
|
+
Notes
|
|
91
|
+
-----
|
|
92
|
+
Rules LG2, LG3. A method emits the names of its declaration; MATE publishes them under
|
|
93
|
+
``method.``.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
def __call__(self, name: str, /, **fields: object) -> None:
|
|
97
|
+
"""Publish one event.
|
|
98
|
+
|
|
99
|
+
Parameters
|
|
100
|
+
----------
|
|
101
|
+
name : str
|
|
102
|
+
Event name, as declared by the module.
|
|
103
|
+
**fields : object
|
|
104
|
+
Event fields, plain data.
|
|
105
|
+
"""
|
|
106
|
+
...
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass
|
|
110
|
+
class Context:
|
|
111
|
+
"""What a module call receives from MATE besides its arguments.
|
|
112
|
+
|
|
113
|
+
Parameters
|
|
114
|
+
----------
|
|
115
|
+
rng : numpy.random.Generator
|
|
116
|
+
Random stream of this component, ``key.rng()``.
|
|
117
|
+
key : Key
|
|
118
|
+
Its key in the seed tree.
|
|
119
|
+
log : Logger
|
|
120
|
+
Journal of messages.
|
|
121
|
+
emit : EventSink
|
|
122
|
+
Events, series and traces.
|
|
123
|
+
config : Mapping
|
|
124
|
+
Its block of the resolved blueprint, read-only.
|
|
125
|
+
workdir : pathlib.Path
|
|
126
|
+
The run's directory, for resolving module-relative resources; a module writes nothing
|
|
127
|
+
there.
|
|
128
|
+
stopping : callable
|
|
129
|
+
``stopping()`` is true when a cooperative stop is requested.
|
|
130
|
+
device : Device, default Device()
|
|
131
|
+
Device designated by the executor.
|
|
132
|
+
ceiling : Budget or None, default None
|
|
133
|
+
The run's total budget.
|
|
134
|
+
spent : Budget, default Budget(0, 0)
|
|
135
|
+
Consumption up to the last result returned.
|
|
136
|
+
|
|
137
|
+
Notes
|
|
138
|
+
-----
|
|
139
|
+
Rules CT1-CT8. A module never keeps a generator in its state: it draws from the context of
|
|
140
|
+
each call. Outside a run, a plain ``Context`` serves standalone tests of a module:
|
|
141
|
+
``Context(rng=key.rng(), key=key, log=logger, emit=sink, config={}, workdir=Path.cwd(),
|
|
142
|
+
stopping=lambda: False)``.
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
rng: np.random.Generator
|
|
146
|
+
key: Key
|
|
147
|
+
log: Logger
|
|
148
|
+
emit: EventSink
|
|
149
|
+
config: Mapping[str, Any]
|
|
150
|
+
workdir: Path
|
|
151
|
+
stopping: Callable[[], bool]
|
|
152
|
+
device: Device = Device()
|
|
153
|
+
ceiling: Budget | None = None
|
|
154
|
+
spent: Budget = Budget(0, 0)
|
|
155
|
+
|
|
156
|
+
def spawn(self, label: str) -> Context:
|
|
157
|
+
"""Return the context of a child component.
|
|
158
|
+
|
|
159
|
+
Parameters
|
|
160
|
+
----------
|
|
161
|
+
label : str
|
|
162
|
+
Name of the child; it extends the seed-tree branch (and, in a run, the log path).
|
|
163
|
+
|
|
164
|
+
Returns
|
|
165
|
+
-------
|
|
166
|
+
Context
|
|
167
|
+
A context of the same type with ``key = self.key.child(label)`` and a fresh
|
|
168
|
+
``rng = key.rng()``; the other fields, ``log`` and ``emit`` included, are shared.
|
|
169
|
+
|
|
170
|
+
Notes
|
|
171
|
+
-----
|
|
172
|
+
Rule CT4. The runtime's subclass also extends the logger's path.
|
|
173
|
+
"""
|
|
174
|
+
key = self.key.child(label)
|
|
175
|
+
return dataclasses.replace(self, key=key, rng=key.rng())
|
|
176
|
+
|
|
177
|
+
def checkpoint_due(self, date: int | float) -> bool:
|
|
178
|
+
"""Tell a system that owns its loop whether a checkpoint is due at ``date``.
|
|
179
|
+
|
|
180
|
+
Parameters
|
|
181
|
+
----------
|
|
182
|
+
date : int or float
|
|
183
|
+
Current date, in the system's clock unit.
|
|
184
|
+
|
|
185
|
+
Returns
|
|
186
|
+
-------
|
|
187
|
+
bool
|
|
188
|
+
Always ``False`` outside a run.
|
|
189
|
+
|
|
190
|
+
Notes
|
|
191
|
+
-----
|
|
192
|
+
Rule CT7.
|
|
193
|
+
"""
|
|
194
|
+
return False
|
|
195
|
+
|
|
196
|
+
def checkpoint(self, date: int | float, state: bytes) -> None:
|
|
197
|
+
"""Hand MATE a checkpoint of a system that owns its loop.
|
|
198
|
+
|
|
199
|
+
Parameters
|
|
200
|
+
----------
|
|
201
|
+
date : int or float
|
|
202
|
+
Date of the state, in the system's clock unit.
|
|
203
|
+
state : bytes
|
|
204
|
+
The system's serialised state, in its own format.
|
|
205
|
+
|
|
206
|
+
Raises
|
|
207
|
+
------
|
|
208
|
+
RuntimeError
|
|
209
|
+
Always outside a run.
|
|
210
|
+
|
|
211
|
+
Notes
|
|
212
|
+
-----
|
|
213
|
+
Rule CT7.
|
|
214
|
+
"""
|
|
215
|
+
raise RuntimeError("no checkpoint outside a run")
|