MATE-runtime 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/__init__.py +22 -0
- MATE/__main__.py +7 -0
- MATE/cli/__init__.py +181 -0
- MATE/cli/commands.py +573 -0
- MATE/cli/workdir.py +571 -0
- MATE/compat/__init__.py +1 -0
- MATE/compat/interface.py +248 -0
- MATE/compat/sizes.py +144 -0
- MATE/config/__init__.py +1 -0
- MATE/config/axes.py +310 -0
- MATE/config/builders.py +498 -0
- MATE/config/calendar.py +371 -0
- MATE/config/canonical.py +543 -0
- MATE/config/criteria.py +396 -0
- MATE/config/events.py +183 -0
- MATE/config/invariance.py +227 -0
- MATE/config/loader.py +498 -0
- MATE/config/model.py +883 -0
- MATE/config/resolve.py +943 -0
- MATE/config/runtable.py +342 -0
- MATE/config/seeds.py +344 -0
- MATE/config/stages.py +632 -0
- MATE/config/structure.py +635 -0
- MATE/config/toml.py +237 -0
- MATE/conformance/__init__.py +14 -0
- MATE/conformance/baseline.py +250 -0
- MATE/conformance/checks.py +1604 -0
- MATE/conformance/sampler.py +179 -0
- MATE/errors.py +236 -0
- MATE/evaluation/__init__.py +1 -0
- MATE/evaluation/budget.py +160 -0
- MATE/evaluation/invalids.py +181 -0
- MATE/evaluation/requests.py +148 -0
- MATE/execution/__init__.py +1 -0
- MATE/execution/accelerator.py +60 -0
- MATE/execution/base.py +643 -0
- MATE/execution/choose.py +152 -0
- MATE/execution/devices.py +108 -0
- MATE/execution/process.py +377 -0
- MATE/execution/serial.py +37 -0
- MATE/execution/thread.py +106 -0
- MATE/execution/vectorised.py +32 -0
- MATE/launch.py +267 -0
- MATE/loop/__init__.py +6 -0
- MATE/loop/continuous.py +172 -0
- MATE/loop/driver.py +783 -0
- MATE/loop/events.py +266 -0
- MATE/loop/generational.py +140 -0
- MATE/loop/owns_loop.py +333 -0
- MATE/loop/subpops.py +508 -0
- MATE/modules/__init__.py +1 -0
- MATE/modules/construct.py +198 -0
- MATE/modules/discovery.py +238 -0
- MATE/modules/registry.py +970 -0
- MATE/modules/software.py +593 -0
- MATE/modules/versions.py +72 -0
- MATE/numerics.py +159 -0
- MATE/observation/__init__.py +10 -0
- MATE/observation/control.py +199 -0
- MATE/observation/hub.py +451 -0
- MATE/observation/records.py +125 -0
- MATE/observation/sinks.py +313 -0
- MATE/provenance.py +396 -0
- MATE/readout/__init__.py +10 -0
- MATE/readout/catalogue.py +282 -0
- MATE/readout/front.py +151 -0
- MATE/readout/readout.py +927 -0
- MATE/readout/series.py +155 -0
- MATE/readout/statistics.py +202 -0
- MATE/run/__init__.py +1 -0
- MATE/run/checkpointing.py +266 -0
- MATE/run/contexts.py +553 -0
- MATE/run/declarations.py +240 -0
- MATE/run/plan.py +464 -0
- MATE/run/preconditions.py +165 -0
- MATE/run/run.py +991 -0
- MATE/run/specs.py +319 -0
- MATE/run/times.py +103 -0
- MATE/scheduling/__init__.py +214 -0
- MATE/scheduling/local.py +119 -0
- MATE/scheduling/slurm.py +380 -0
- MATE/storage/__init__.py +1 -0
- MATE/storage/checkpoint.py +414 -0
- MATE/storage/codec.py +315 -0
- MATE/storage/database.py +359 -0
- MATE/storage/lease.py +397 -0
- MATE/storage/rundir.py +248 -0
- mate_runtime-1.0.0.dist-info/METADATA +58 -0
- mate_runtime-1.0.0.dist-info/RECORD +93 -0
- mate_runtime-1.0.0.dist-info/WHEEL +4 -0
- mate_runtime-1.0.0.dist-info/entry_points.txt +2 -0
- mate_runtime-1.0.0.dist-info/licenses/COPYING +674 -0
- mate_runtime-1.0.0.dist-info/licenses/COPYING.LESSER +165 -0
MATE/config/calendar.py
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
"""The environment calendar: parsing ``[environment]``, building and evaluating it.
|
|
2
|
+
|
|
3
|
+
:class:`Calendar` is the single calendar implementation: the loop evaluates it at each
|
|
4
|
+
request date ([X] §10.1) and a system that owns its loop receives it as a dated function
|
|
5
|
+
([C] R10.6).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from collections.abc import Mapping
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from types import MappingProxyType
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
from MATE_contract import (
|
|
14
|
+
Categorical,
|
|
15
|
+
Domain,
|
|
16
|
+
Environment,
|
|
17
|
+
EnvironmentSpec,
|
|
18
|
+
Integer,
|
|
19
|
+
Log,
|
|
20
|
+
Ordinal,
|
|
21
|
+
Real,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
from MATE.config.model import EnvironmentDecl, Phase, Value
|
|
25
|
+
from MATE.errors import Refusal
|
|
26
|
+
|
|
27
|
+
__all__ = ["parse_environment", "build_calendar", "Calendar"]
|
|
28
|
+
|
|
29
|
+
_UNITS = ("generation", "evaluation")
|
|
30
|
+
_RESERVED = ("unit", "phase")
|
|
31
|
+
_PHASE_KEYS = frozenset({"until", "settings", "interpolation"})
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _is_int(x: object) -> bool:
|
|
35
|
+
"""A TOML integer (booleans excluded)."""
|
|
36
|
+
return isinstance(x, int) and not isinstance(x, bool)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _reactive(value: object) -> bool:
|
|
40
|
+
"""A condition-shaped value: a table with a ``serie`` key (R10.15)."""
|
|
41
|
+
return isinstance(value, Mapping) and "serie" in value
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _reactive_refusal(where: str) -> Refusal:
|
|
45
|
+
"""``P-PHASE-FORM`` for a setting written as a condition on a series."""
|
|
46
|
+
return Refusal("P-PHASE-FORM", where,
|
|
47
|
+
"calendar form not available in MATE 1.0.0: reactive (a setting written "
|
|
48
|
+
"as a condition on a series); reactive calendars are not yet specified")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _freeze(value: object) -> Value:
|
|
52
|
+
"""Arrays as tuples, tables as read-only mappings."""
|
|
53
|
+
if isinstance(value, (list, tuple)):
|
|
54
|
+
return tuple(_freeze(v) for v in value)
|
|
55
|
+
if isinstance(value, Mapping):
|
|
56
|
+
return MappingProxyType({k: _freeze(v) for k, v in value.items()})
|
|
57
|
+
return value
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def parse_environment(table: Mapping) -> tuple[EnvironmentDecl | None, list[Refusal]]:
|
|
61
|
+
"""Parse an ``[environment]`` table.
|
|
62
|
+
|
|
63
|
+
Parameters
|
|
64
|
+
----------
|
|
65
|
+
table : mapping
|
|
66
|
+
The ``[environment]`` table (empty when absent).
|
|
67
|
+
|
|
68
|
+
Returns
|
|
69
|
+
-------
|
|
70
|
+
EnvironmentDecl or None
|
|
71
|
+
Settings as written (labels for ``Ordinal`` and ``Categorical``, linear pairs as
|
|
72
|
+
tuples); ``None`` when refused.
|
|
73
|
+
list of Refusal
|
|
74
|
+
``P-PHASE-ORDER``, ``P-PHASE-UNTIL``, ``P-PHASE-LINEAR``, ``P-PHASE-FORM``,
|
|
75
|
+
``P-UNKNOWN-KEY`` (a phase key, or ``unit`` not a string).
|
|
76
|
+
|
|
77
|
+
Notes
|
|
78
|
+
-----
|
|
79
|
+
[C] R10.7 (keys other than ``unit`` and ``phase`` are settings from date 0), R10.9,
|
|
80
|
+
R10.10 syntax, R10.15. Domains, units and completeness are :func:`build_calendar`'s.
|
|
81
|
+
"""
|
|
82
|
+
if not isinstance(table, Mapping):
|
|
83
|
+
return None, [Refusal("P-UNKNOWN-KEY", "[environment]",
|
|
84
|
+
f"[environment] is a table, got {table!r}")]
|
|
85
|
+
refusals: list[Refusal] = []
|
|
86
|
+
unit = table.get("unit")
|
|
87
|
+
if "unit" in table and (not isinstance(unit, str) or not unit):
|
|
88
|
+
refusals.append(Refusal("P-UNKNOWN-KEY", "[environment] unit",
|
|
89
|
+
f"unit is \"generation\" or \"evaluation\", got {unit!r}"))
|
|
90
|
+
base: dict[str, Value] = {}
|
|
91
|
+
for name, value in table.items():
|
|
92
|
+
if name in _RESERVED:
|
|
93
|
+
continue
|
|
94
|
+
if _reactive(value):
|
|
95
|
+
refusals.append(_reactive_refusal(f"[environment] {name}"))
|
|
96
|
+
base[name] = _freeze(value)
|
|
97
|
+
|
|
98
|
+
raw_phases = table.get("phase", [])
|
|
99
|
+
if not isinstance(raw_phases, (list, tuple)):
|
|
100
|
+
refusals.append(Refusal("P-PHASE-UNTIL", "[environment] phase",
|
|
101
|
+
"phases are an array of tables ([[environment.phase]])"))
|
|
102
|
+
raw_phases = []
|
|
103
|
+
phases: list[Phase] = []
|
|
104
|
+
previous = 0
|
|
105
|
+
last = len(raw_phases) - 1
|
|
106
|
+
for i, raw in enumerate(raw_phases):
|
|
107
|
+
where = f"[[environment.phase]] #{i + 1}"
|
|
108
|
+
if not isinstance(raw, Mapping):
|
|
109
|
+
refusals.append(Refusal("P-PHASE-UNTIL", where, f"a phase is a table, got {raw!r}"))
|
|
110
|
+
continue
|
|
111
|
+
refusals.extend(Refusal("P-UNKNOWN-KEY", f"{where} {k}",
|
|
112
|
+
f"unknown phase key '{k}'; known keys: until, settings, "
|
|
113
|
+
f"interpolation") for k in raw if k not in _PHASE_KEYS)
|
|
114
|
+
until = raw.get("until")
|
|
115
|
+
if until is None:
|
|
116
|
+
if i != last:
|
|
117
|
+
refusals.append(Refusal("P-PHASE-UNTIL", where,
|
|
118
|
+
"only the last phase may omit until"))
|
|
119
|
+
elif not _is_int(until):
|
|
120
|
+
refusals.append(Refusal("P-PHASE-UNTIL", f"{where} until",
|
|
121
|
+
f"until is an integer date, got {until!r}"))
|
|
122
|
+
until = None
|
|
123
|
+
elif until <= previous:
|
|
124
|
+
refusals.append(Refusal(
|
|
125
|
+
"P-PHASE-ORDER", f"{where} until",
|
|
126
|
+
f"until values increase strictly from 0: {until} follows {previous}"))
|
|
127
|
+
else:
|
|
128
|
+
previous = until
|
|
129
|
+
interpolation = raw.get("interpolation")
|
|
130
|
+
if interpolation is not None and interpolation != "linear":
|
|
131
|
+
refusals.append(Refusal(
|
|
132
|
+
"P-PHASE-FORM", f"{where} interpolation",
|
|
133
|
+
f"calendar form not available in MATE 1.0.0: {interpolation}; "
|
|
134
|
+
f"periodic and random calendars are not yet specified"))
|
|
135
|
+
interpolation = None
|
|
136
|
+
settings = raw.get("settings", {})
|
|
137
|
+
if not isinstance(settings, Mapping):
|
|
138
|
+
refusals.append(Refusal("P-UNKNOWN-KEY", f"{where} settings",
|
|
139
|
+
f"settings is a table, got {settings!r}"))
|
|
140
|
+
settings = {}
|
|
141
|
+
frozen: dict[str, Value] = {}
|
|
142
|
+
for name, value in settings.items():
|
|
143
|
+
if _reactive(value):
|
|
144
|
+
refusals.append(_reactive_refusal(f"{where} settings.{name}"))
|
|
145
|
+
elif interpolation == "linear" and (not isinstance(value, (list, tuple))
|
|
146
|
+
or len(value) != 2):
|
|
147
|
+
refusals.append(Refusal(
|
|
148
|
+
"P-PHASE-LINEAR", f"{where} settings.{name}",
|
|
149
|
+
f"a linear phase writes [from, to] pairs, got {value!r}"))
|
|
150
|
+
frozen[name] = _freeze(value)
|
|
151
|
+
if interpolation == "linear" and raw.get("until") is None:
|
|
152
|
+
refusals.append(Refusal("P-PHASE-LINEAR", where, "a linear phase needs until"))
|
|
153
|
+
phases.append(Phase(until, MappingProxyType(frozen), interpolation))
|
|
154
|
+
if refusals:
|
|
155
|
+
return None, refusals
|
|
156
|
+
return EnvironmentDecl(unit, MappingProxyType(base), tuple(phases)), []
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _on_circle(value: Value, domain: Domain) -> Value:
|
|
160
|
+
"""A value of a periodic ``Real``, ``hi`` written as ``lo``: the same point of the circle."""
|
|
161
|
+
if isinstance(domain, Real) and domain.periodic and value == domain.hi:
|
|
162
|
+
return domain.lo # type: ignore[return-value]
|
|
163
|
+
return value
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _normalise(value: object, domain: Domain, *, point: bool = True
|
|
167
|
+
) -> tuple[Value, str | None]:
|
|
168
|
+
"""Normalise one written value in its domain (R6.10); return (value, error).
|
|
169
|
+
|
|
170
|
+
A point of a periodic ``Real`` written at ``hi`` becomes ``lo``. ``point=False`` keeps it
|
|
171
|
+
for an end of a linear pair, which bounds a path: ``[lo, hi]`` is a full turn.
|
|
172
|
+
"""
|
|
173
|
+
if isinstance(domain, (Ordinal, Categorical)):
|
|
174
|
+
for label in domain.values:
|
|
175
|
+
if (isinstance(value, bool) == isinstance(label, bool) and type(value) is not tuple
|
|
176
|
+
and value == label):
|
|
177
|
+
return label, None
|
|
178
|
+
return value, f"{value!r} is not one of {list(domain.values)}"
|
|
179
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
180
|
+
return value, f"{value!r} is not a number"
|
|
181
|
+
if isinstance(domain, Integer):
|
|
182
|
+
if not isinstance(value, int):
|
|
183
|
+
return value, f"{value!r} is not an integer"
|
|
184
|
+
else:
|
|
185
|
+
value = float(value)
|
|
186
|
+
if not bool(domain.contains(np.asarray(value))):
|
|
187
|
+
return value, f"{value!r} is outside {domain!r}"
|
|
188
|
+
return (_on_circle(value, domain) if point else value), None
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def build_calendar(decl: EnvironmentDecl, spec: EnvironmentSpec, *, clock: str | None,
|
|
192
|
+
dated: bool) -> tuple["Calendar | None", list[Refusal]]:
|
|
193
|
+
"""Check a calendar against the system's environment spec and build it.
|
|
194
|
+
|
|
195
|
+
Parameters
|
|
196
|
+
----------
|
|
197
|
+
decl : EnvironmentDecl
|
|
198
|
+
From :func:`parse_environment`.
|
|
199
|
+
spec : EnvironmentSpec
|
|
200
|
+
The system's environment spec.
|
|
201
|
+
clock : str or None
|
|
202
|
+
The system's ``capabilities.clock`` when it owns its loop, else ``None``.
|
|
203
|
+
dated : bool
|
|
204
|
+
Some ``[[event]]`` has an ``at`` date.
|
|
205
|
+
|
|
206
|
+
Returns
|
|
207
|
+
-------
|
|
208
|
+
Calendar or None
|
|
209
|
+
With values normalised (R6.10) and, under a clock, ``unit`` resolved to it;
|
|
210
|
+
``None`` when refused.
|
|
211
|
+
list of Refusal
|
|
212
|
+
``P-ENV-RESERVED`` (R10.7), ``P-ENV-UNKNOWN``, ``P-ENV-DOMAIN`` (R10.8),
|
|
213
|
+
``P-PHASE-LINEAR-DOMAIN`` (R10.10), ``P-ENV-UNSET`` (R10.12), ``P-UNIT-MISSING``,
|
|
214
|
+
``P-UNIT-CLOCK`` (R10.13; also a unit other than ``"generation"`` and
|
|
215
|
+
``"evaluation"`` without a clock).
|
|
216
|
+
"""
|
|
217
|
+
refusals: list[Refusal] = []
|
|
218
|
+
fields = spec.fields
|
|
219
|
+
for name in fields:
|
|
220
|
+
if name in _RESERVED:
|
|
221
|
+
refusals.append(Refusal(
|
|
222
|
+
"P-ENV-RESERVED", f"[environment] {name}",
|
|
223
|
+
f"the environment spec declares a dimension named '{name}', a reserved "
|
|
224
|
+
f"key of [environment]"))
|
|
225
|
+
|
|
226
|
+
unit = decl.unit
|
|
227
|
+
if clock is not None:
|
|
228
|
+
if unit is None:
|
|
229
|
+
unit = clock
|
|
230
|
+
elif unit != clock:
|
|
231
|
+
refusals.append(Refusal(
|
|
232
|
+
"P-UNIT-CLOCK", "[environment] unit",
|
|
233
|
+
f"the system owns its loop and dates in its clock '{clock}'; unit = '{unit}'",
|
|
234
|
+
f"unit = \"{clock}\", or omit unit"))
|
|
235
|
+
elif unit is not None and unit not in _UNITS:
|
|
236
|
+
refusals.append(Refusal(
|
|
237
|
+
"P-UNIT-CLOCK", "[environment] unit",
|
|
238
|
+
f"unit is \"generation\" or \"evaluation\", got '{unit}' (the system does not "
|
|
239
|
+
f"own its loop)"))
|
|
240
|
+
if unit is None and (dated or any(p.until is not None for p in decl.phases)):
|
|
241
|
+
refusals.append(Refusal(
|
|
242
|
+
"P-UNIT-MISSING", "[environment] unit",
|
|
243
|
+
"a date is written (a phase until or an event at) without its unit",
|
|
244
|
+
"unit = \"generation\" or unit = \"evaluation\""))
|
|
245
|
+
|
|
246
|
+
def check(where: str, name: str, value: Value, linear: bool) -> Value:
|
|
247
|
+
domain = fields.get(name)
|
|
248
|
+
if domain is None:
|
|
249
|
+
refusals.append(Refusal(
|
|
250
|
+
"P-ENV-UNKNOWN", where,
|
|
251
|
+
f"'{name}' is not a dimension of the environment spec "
|
|
252
|
+
f"(dimensions: {', '.join(fields) or 'none'})"))
|
|
253
|
+
return value
|
|
254
|
+
if linear:
|
|
255
|
+
if not isinstance(domain, (Real, Log)):
|
|
256
|
+
refusals.append(Refusal(
|
|
257
|
+
"P-PHASE-LINEAR-DOMAIN", where,
|
|
258
|
+
f"linear interpolation applies to Real and Log dimensions; "
|
|
259
|
+
f"'{name}' is {domain!r}"))
|
|
260
|
+
return value
|
|
261
|
+
pair = []
|
|
262
|
+
for member in value:
|
|
263
|
+
v, error = _normalise(member, domain, point=False)
|
|
264
|
+
if error is not None:
|
|
265
|
+
refusals.append(Refusal("P-ENV-DOMAIN", where, f"'{name}': {error}"))
|
|
266
|
+
pair.append(v)
|
|
267
|
+
return tuple(pair)
|
|
268
|
+
v, error = _normalise(value, domain)
|
|
269
|
+
if error is not None:
|
|
270
|
+
refusals.append(Refusal("P-ENV-DOMAIN", where, f"'{name}': {error}"))
|
|
271
|
+
return v
|
|
272
|
+
|
|
273
|
+
base = {n: check(f"[environment] {n}", n, v, False) for n, v in decl.base.items()}
|
|
274
|
+
phases = []
|
|
275
|
+
for i, p in enumerate(decl.phases):
|
|
276
|
+
linear = p.interpolation == "linear"
|
|
277
|
+
settings = {n: check(f"[[environment.phase]] #{i + 1} settings.{n}", n, v, linear)
|
|
278
|
+
for n, v in p.settings.items()}
|
|
279
|
+
phases.append(Phase(p.until, MappingProxyType(settings), p.interpolation))
|
|
280
|
+
|
|
281
|
+
first = decl.phases[0].settings if decl.phases else {}
|
|
282
|
+
for name in fields:
|
|
283
|
+
if name in _RESERVED or name in base or name in first:
|
|
284
|
+
continue
|
|
285
|
+
later = [i + 1 for i, p in enumerate(decl.phases) if name in p.settings]
|
|
286
|
+
detail = (f"it is first set in phase #{later[0]}, so it has no value from date 0"
|
|
287
|
+
if later else "the calendar never sets it")
|
|
288
|
+
refusals.append(Refusal(
|
|
289
|
+
"P-ENV-UNSET", f"[environment] {name}",
|
|
290
|
+
f"environment dimension '{name}' has no value at every date: {detail}",
|
|
291
|
+
f"write [environment] {name} = <value>"))
|
|
292
|
+
if refusals:
|
|
293
|
+
return None, refusals
|
|
294
|
+
normalised = EnvironmentDecl(unit, MappingProxyType(base), tuple(phases))
|
|
295
|
+
return Calendar(normalised, spec), []
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
@dataclass(frozen=True)
|
|
299
|
+
class Calendar:
|
|
300
|
+
"""A checked calendar: the environment point at each date.
|
|
301
|
+
|
|
302
|
+
Parameters
|
|
303
|
+
----------
|
|
304
|
+
decl : EnvironmentDecl
|
|
305
|
+
Normalised by :func:`build_calendar`.
|
|
306
|
+
spec : EnvironmentSpec
|
|
307
|
+
The system's environment spec.
|
|
308
|
+
|
|
309
|
+
Notes
|
|
310
|
+
-----
|
|
311
|
+
[C] §10.2, [X] CA2. Phase ``i`` covers ``[until_{i-1}, until_i)`` with
|
|
312
|
+
``until_{-1} = 0``; a dimension a phase does not write keeps its value at the end of
|
|
313
|
+
the previous phase; a linear phase gives ``from + (to - from) (d - start) /
|
|
314
|
+
(until - start)``; after the last ``until`` every value holds. On a periodic ``Real``,
|
|
315
|
+
a linear phase runs from ``from`` to ``to`` as written, at most one turn, and the point
|
|
316
|
+
gives ``lo`` for ``hi``.
|
|
317
|
+
"""
|
|
318
|
+
|
|
319
|
+
decl: EnvironmentDecl
|
|
320
|
+
spec: EnvironmentSpec
|
|
321
|
+
|
|
322
|
+
def point(self, date: int | float) -> Environment:
|
|
323
|
+
"""Return the environment point at a date.
|
|
324
|
+
|
|
325
|
+
Parameters
|
|
326
|
+
----------
|
|
327
|
+
date : int or float
|
|
328
|
+
In the calendar's unit, ``>= 0``.
|
|
329
|
+
|
|
330
|
+
Returns
|
|
331
|
+
-------
|
|
332
|
+
Environment
|
|
333
|
+
One value per dimension of the spec, in its order; ``Ordinal`` and
|
|
334
|
+
``Categorical`` values as codes (contract EN1), a periodic ``Real`` in
|
|
335
|
+
``[lo, hi)``.
|
|
336
|
+
|
|
337
|
+
Raises
|
|
338
|
+
------
|
|
339
|
+
ValueError
|
|
340
|
+
If ``date`` is negative or not a number.
|
|
341
|
+
"""
|
|
342
|
+
if isinstance(date, bool) or not isinstance(date, (int, float)) or not date >= 0:
|
|
343
|
+
raise ValueError(f"Calendar.point: date must be a number >= 0, got {date!r}")
|
|
344
|
+
values: dict[str, Value] = dict(self.decl.base)
|
|
345
|
+
start = 0
|
|
346
|
+
for p in self.decl.phases:
|
|
347
|
+
inside = p.until is None or date < p.until
|
|
348
|
+
for name, v in p.settings.items():
|
|
349
|
+
if p.interpolation != "linear":
|
|
350
|
+
values[name] = v
|
|
351
|
+
elif inside:
|
|
352
|
+
lo, hi = v
|
|
353
|
+
values[name] = lo + (hi - lo) * (date - start) / (p.until - start)
|
|
354
|
+
else:
|
|
355
|
+
values[name] = v[1]
|
|
356
|
+
if inside:
|
|
357
|
+
break
|
|
358
|
+
start = p.until
|
|
359
|
+
point: dict[str, int | float] = {}
|
|
360
|
+
for name, domain in self.spec.fields.items():
|
|
361
|
+
v = values[name]
|
|
362
|
+
if isinstance(domain, (Ordinal, Categorical)):
|
|
363
|
+
point[name] = next(i for i, label in enumerate(domain.values)
|
|
364
|
+
if label == v and isinstance(label, bool) == isinstance(v, bool))
|
|
365
|
+
else:
|
|
366
|
+
point[name] = _on_circle(v, domain) # the end of a linear turn
|
|
367
|
+
return Environment(point)
|
|
368
|
+
|
|
369
|
+
def __call__(self, date: int | float) -> Environment:
|
|
370
|
+
"""The dated function handed to a system that owns its loop: :meth:`point`."""
|
|
371
|
+
return self.point(date)
|