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/cli/commands.py
ADDED
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
"""The commands of the ``MATE`` command line ([X] §24, [C] §21, design §3.14).
|
|
2
|
+
|
|
3
|
+
Each ``cmd_<name>`` function takes the parsed arguments and returns the exit status: 0
|
|
4
|
+
success, 1 refusal (software, interface, preconditions, deferred feature), 2 run failures,
|
|
5
|
+
3 usage error. Reports go to stdout, errors to stderr.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import sys
|
|
10
|
+
import tempfile
|
|
11
|
+
from collections.abc import Sequence
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from packaging.utils import canonicalize_name
|
|
15
|
+
|
|
16
|
+
from MATE.compat.interface import Outcome, check_interface
|
|
17
|
+
from MATE.config.loader import LoadedProtocol, load_protocol
|
|
18
|
+
from MATE.config.model import Value
|
|
19
|
+
from MATE.config.toml import parse_toml_value
|
|
20
|
+
from MATE.errors import ProtocolRefused, Refusal, SoftwareCompatibilityError
|
|
21
|
+
from MATE.launch import exec_run, launch
|
|
22
|
+
from MATE.modules import registry
|
|
23
|
+
from MATE.modules.construct import (ModuleConstructionError, construct_module,
|
|
24
|
+
declared_defaults, missing_parameters)
|
|
25
|
+
from MATE.modules.discovery import ModuleEntry, installed_modules
|
|
26
|
+
from MATE.modules.software import ModuleRef, check_software, render
|
|
27
|
+
from MATE.observation.control import send_command
|
|
28
|
+
from MATE.run.plan import LaunchResult
|
|
29
|
+
from MATE.run.specs import NormalisedSpecs, is_collective, normalise_specs
|
|
30
|
+
from MATE.scheduling.slurm import SubmissionError
|
|
31
|
+
from MATE.storage.lease import RunBusyError
|
|
32
|
+
|
|
33
|
+
from MATE.cli import workdir
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"REPLAY_REFUSAL",
|
|
37
|
+
"DASHBOARD_REFUSAL",
|
|
38
|
+
"UsageError",
|
|
39
|
+
"cmd_launch", "cmd_status", "cmd_verify", "cmd_runs", "cmd_diff", "cmd_show",
|
|
40
|
+
"cmd_branch", "cmd_control", "cmd_list", "cmd_matrix", "cmd_check", "cmd_search",
|
|
41
|
+
"cmd_install", "cmd_replay", "cmd_dashboard", "cmd_exec_run",
|
|
42
|
+
"matrix",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
REPLAY_REFUSAL = ("MATE replay: visual replay is not available in MATE 1.0.0 (the replay "
|
|
46
|
+
"interface of systems is not yet specified)")
|
|
47
|
+
DASHBOARD_REFUSAL = ("the terminal dashboard is not available in MATE 1.0.0; control a run "
|
|
48
|
+
"with 'MATE control <run> stop|suspend|resume|checkpoint|verbosity'")
|
|
49
|
+
|
|
50
|
+
_FAILURES = ("method_error", "system_error", "refused")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class UsageError(Exception):
|
|
54
|
+
"""A command-line argument the command cannot use (exit status 3)."""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _out(text: str) -> None:
|
|
58
|
+
"""Print a report."""
|
|
59
|
+
print(text, file=sys.stdout)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _err(text: str) -> None:
|
|
63
|
+
"""Print an error."""
|
|
64
|
+
print(text, file=sys.stderr)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# --- launching ---------------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _sweep(specs: Sequence[str]) -> list[tuple[str, Sequence[Value]]]:
|
|
71
|
+
"""``--sweep path=v1,v2`` -> (path, values), each value a TOML value ([C] R21.2)."""
|
|
72
|
+
out = []
|
|
73
|
+
for spec in specs:
|
|
74
|
+
path, sep, values = spec.partition("=")
|
|
75
|
+
if not sep or not path.strip() or not values.strip():
|
|
76
|
+
raise UsageError(f"--sweep {spec!r}: expected <path>=<v1>,<v2>,...")
|
|
77
|
+
try:
|
|
78
|
+
parsed = parse_toml_value(f"[{values}]")
|
|
79
|
+
except ValueError as exc:
|
|
80
|
+
raise UsageError(f"--sweep {spec!r}: {exc}") from None
|
|
81
|
+
out.append((path.strip(), tuple(parsed))) # type: ignore[arg-type]
|
|
82
|
+
return out
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _overrides(args: argparse.Namespace) -> dict[str, Value]:
|
|
86
|
+
"""The ``[execution]`` keys given on the command line ([C] R21.1)."""
|
|
87
|
+
out: dict[str, Value] = {}
|
|
88
|
+
for key in ("workers", "executor", "scheduler", "verbosity", "workdir", "retention",
|
|
89
|
+
"checkpoint_every"):
|
|
90
|
+
value = getattr(args, key, None)
|
|
91
|
+
if value is not None:
|
|
92
|
+
out[key] = str(value) if key == "workdir" else value
|
|
93
|
+
return out
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _sinks(args: argparse.Namespace) -> list[str] | None:
|
|
97
|
+
"""``--sinks S,...``."""
|
|
98
|
+
if getattr(args, "sinks", None) is None:
|
|
99
|
+
return None
|
|
100
|
+
return [s.strip() for s in args.sinks.split(",") if s.strip()]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _load(load: object, debug: bool = False) -> LoadedProtocol | int:
|
|
104
|
+
"""Call a loading thunk; a refusal is printed and gives status 1.
|
|
105
|
+
|
|
106
|
+
No run directory exists yet: the software check's tracebacks go to stderr under
|
|
107
|
+
``--debug``.
|
|
108
|
+
"""
|
|
109
|
+
try:
|
|
110
|
+
return load() # type: ignore[operator]
|
|
111
|
+
except ProtocolRefused as exc:
|
|
112
|
+
_err(exc.render())
|
|
113
|
+
except SoftwareCompatibilityError as exc:
|
|
114
|
+
_err(exc.render())
|
|
115
|
+
traces = [f.debug for f in exc.report.failures if f.debug]
|
|
116
|
+
if traces:
|
|
117
|
+
_err("\n".join(traces) if debug else "Rerun with --debug to print the traceback.")
|
|
118
|
+
except ValueError as exc:
|
|
119
|
+
_err(str(exc))
|
|
120
|
+
return 1
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _launch(loaded: LoadedProtocol, args: argparse.Namespace) -> int:
|
|
124
|
+
"""Launch and report; a submission ``sbatch`` cannot make gives status 1."""
|
|
125
|
+
try:
|
|
126
|
+
result = launch(loaded, sinks=_sinks(args), debug=args.debug, dry_run=args.dry_run)
|
|
127
|
+
except SubmissionError as exc:
|
|
128
|
+
_err(str(exc))
|
|
129
|
+
return 1
|
|
130
|
+
return _report(loaded, result, args.dry_run)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _report(loaded: LoadedProtocol, result: LaunchResult, dry_run: bool) -> int:
|
|
134
|
+
"""Print what a launch did; status 2 when a run failed."""
|
|
135
|
+
names = {row.identity: row.directory for row in loaded.runs}
|
|
136
|
+
for note in loaded.notes:
|
|
137
|
+
_out(note)
|
|
138
|
+
failed = 0
|
|
139
|
+
skipped = dict(result.skipped)
|
|
140
|
+
for ref, cause in result.runs.items():
|
|
141
|
+
if ref in skipped:
|
|
142
|
+
shown = f"left alone, {skipped[ref]}"
|
|
143
|
+
elif cause is None:
|
|
144
|
+
shown = "prepared" if dry_run else "submitted" if result.submitted else "not run"
|
|
145
|
+
else:
|
|
146
|
+
shown = cause.kind + (f" ({cause.detail})" if cause.detail else "")
|
|
147
|
+
if cause.generation is not None:
|
|
148
|
+
shown += f" at generation {cause.generation}"
|
|
149
|
+
failed += cause.kind in _FAILURES
|
|
150
|
+
_out(f" {names.get(ref, ref.fingerprint[:12]):<10} {shown}")
|
|
151
|
+
for _, message in result.missing:
|
|
152
|
+
_out(f" {message}")
|
|
153
|
+
if result.submitted:
|
|
154
|
+
_out(f"submitted Slurm job(s): {', '.join(result.submitted)}")
|
|
155
|
+
_out(f"{len(result.runs)} run(s) in {result.workdir}"
|
|
156
|
+
+ (f", {failed} failed" if failed else ""))
|
|
157
|
+
return 2 if failed else 0
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def cmd_launch(args: argparse.Namespace) -> int:
|
|
161
|
+
"""``MATE launch <proto.toml>``: load, check, schedule ([X] §24)."""
|
|
162
|
+
if args.dashboard:
|
|
163
|
+
_err(DASHBOARD_REFUSAL)
|
|
164
|
+
return 1
|
|
165
|
+
sweep = _sweep(args.sweep or [])
|
|
166
|
+
loaded = _load(lambda: load_protocol(Path(args.protocol), cwd=Path.cwd(),
|
|
167
|
+
execution_overrides=_overrides(args), sweep=sweep),
|
|
168
|
+
args.debug)
|
|
169
|
+
if isinstance(loaded, int):
|
|
170
|
+
return loaded
|
|
171
|
+
return _launch(loaded, args)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _branch_target(text: str) -> tuple[Path, int | str]:
|
|
175
|
+
"""``<run>@<generation>``."""
|
|
176
|
+
run, sep, generation = text.rpartition("@")
|
|
177
|
+
if not sep or not run:
|
|
178
|
+
raise UsageError(f"{text!r}: expected <run directory>@<generation>")
|
|
179
|
+
if generation == "end":
|
|
180
|
+
return Path(run), "end"
|
|
181
|
+
try:
|
|
182
|
+
g = int(generation)
|
|
183
|
+
except ValueError:
|
|
184
|
+
raise UsageError(f"{text!r}: the generation is an integer or 'end'") from None
|
|
185
|
+
if g < 0:
|
|
186
|
+
raise UsageError(f"{text!r}: the generation is an integer >= 0 or 'end'")
|
|
187
|
+
return Path(run), g
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def cmd_branch(args: argparse.Namespace) -> int:
|
|
191
|
+
"""``MATE branch <run>@<g> --count N --root-seed S`` ([C] R21.3)."""
|
|
192
|
+
run, generation = _branch_target(args.target)
|
|
193
|
+
if args.root_seed == "fresh":
|
|
194
|
+
root_seed: int | str = "fresh"
|
|
195
|
+
else:
|
|
196
|
+
try:
|
|
197
|
+
root_seed = int(args.root_seed)
|
|
198
|
+
except ValueError:
|
|
199
|
+
raise UsageError(f"--root-seed {args.root_seed!r}: an integer or 'fresh'") from None
|
|
200
|
+
try:
|
|
201
|
+
raw = workdir.branch_protocol(run, generation, args.count, root_seed) # type: ignore[arg-type]
|
|
202
|
+
except workdir.WorkdirError as exc:
|
|
203
|
+
_err(exc.message)
|
|
204
|
+
return exc.exit_status
|
|
205
|
+
overrides = _overrides(args)
|
|
206
|
+
if "workdir" not in overrides:
|
|
207
|
+
overrides["workdir"] = str(Path.cwd() / raw["protocol"]["name"])
|
|
208
|
+
loaded = _load(lambda: load_protocol(raw, cwd=Path.cwd(), execution_overrides=overrides),
|
|
209
|
+
args.debug)
|
|
210
|
+
if isinstance(loaded, int):
|
|
211
|
+
return loaded
|
|
212
|
+
return _launch(loaded, args)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def cmd_exec_run(args: argparse.Namespace) -> int:
|
|
216
|
+
"""Hidden ``MATE exec-run <run directory>`` ([X] SC4); a run left alone gives status 0."""
|
|
217
|
+
try:
|
|
218
|
+
return exec_run(Path(args.directory), debug=args.debug)
|
|
219
|
+
except RunBusyError as exc:
|
|
220
|
+
_out(exc.message)
|
|
221
|
+
return 0
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
# --- the workdir -------------------------------------------------------------------------------
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _workdir_call(call: object) -> int:
|
|
228
|
+
"""Print what a workdir reader returns; its errors carry their status."""
|
|
229
|
+
try:
|
|
230
|
+
text = call() # type: ignore[operator]
|
|
231
|
+
except workdir.WorkdirError as exc:
|
|
232
|
+
_err(exc.message)
|
|
233
|
+
return exc.exit_status
|
|
234
|
+
_out(text)
|
|
235
|
+
return 0
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def cmd_status(args: argparse.Namespace) -> int:
|
|
239
|
+
"""``MATE status <workdir>`` ([C] R21.4)."""
|
|
240
|
+
return _workdir_call(lambda: workdir.status(Path(args.workdir), args.protocol))
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def cmd_verify(args: argparse.Namespace) -> int:
|
|
244
|
+
"""``MATE verify <workdir>`` ([C] R19.8): non-zero on any violation."""
|
|
245
|
+
try:
|
|
246
|
+
text, ok = workdir.verify_workdir(Path(args.workdir), args.protocol)
|
|
247
|
+
except workdir.WorkdirError as exc:
|
|
248
|
+
_err(exc.message)
|
|
249
|
+
return exc.exit_status
|
|
250
|
+
(_out if ok else _err)(text)
|
|
251
|
+
return 0 if ok else 1
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def cmd_runs(args: argparse.Namespace) -> int:
|
|
255
|
+
"""``MATE runs <workdir>`` ([C] R21.4)."""
|
|
256
|
+
return _workdir_call(lambda: workdir.runs(Path(args.workdir), args.protocol))
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def cmd_diff(args: argparse.Namespace) -> int:
|
|
260
|
+
"""``MATE diff <a> <b>`` ([C] R21.5)."""
|
|
261
|
+
return _workdir_call(lambda: workdir.diff(Path(args.a), Path(args.b)))
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def cmd_show(args: argparse.Namespace) -> int:
|
|
265
|
+
"""``MATE show <run or workdir>`` (design §1.2)."""
|
|
266
|
+
return _workdir_call(lambda: workdir.show(Path(args.path), args.objective, args.subpop))
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def cmd_control(args: argparse.Namespace) -> int:
|
|
270
|
+
"""``MATE control <run> <command> [value]`` (DD-3)."""
|
|
271
|
+
directory = Path(args.run)
|
|
272
|
+
if not (directory / "plan.json").is_file():
|
|
273
|
+
raise UsageError(f"{directory}: not a run directory (no plan.json)")
|
|
274
|
+
if args.command_name == "verbosity" and args.value is None:
|
|
275
|
+
raise UsageError("control verbosity: a value is required")
|
|
276
|
+
try:
|
|
277
|
+
seq = send_command(directory, args.command_name, args.value)
|
|
278
|
+
except ValueError as exc:
|
|
279
|
+
raise UsageError(str(exc)) from None
|
|
280
|
+
_out(f"command {seq} ({args.command_name}) sent to {directory}")
|
|
281
|
+
return 0
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
# --- modules -----------------------------------------------------------------------------------
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _modules() -> list[ModuleEntry]:
|
|
288
|
+
"""Installed modules, one per (kind, name)."""
|
|
289
|
+
seen: dict[tuple[str, str], ModuleEntry] = {}
|
|
290
|
+
for e in installed_modules():
|
|
291
|
+
seen.setdefault((e.kind, e.name), e)
|
|
292
|
+
return list(seen.values())
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def cmd_list(args: argparse.Namespace) -> int:
|
|
296
|
+
"""``MATE list``: installed modules and their software status ([X] S10)."""
|
|
297
|
+
entries = _modules()
|
|
298
|
+
if not entries:
|
|
299
|
+
_out("no module installed")
|
|
300
|
+
return 0
|
|
301
|
+
for e in entries:
|
|
302
|
+
report = check_software([ModuleRef(e.kind, e.name)], None)
|
|
303
|
+
if report.ok:
|
|
304
|
+
status = "ok"
|
|
305
|
+
else:
|
|
306
|
+
f = report.failures[0]
|
|
307
|
+
status = f"failed: requires {f.requirement}; found {f.found}"
|
|
308
|
+
_out(f"{e.kind:<7} {e.name:<24} {e.distribution} {e.distribution_version:<10} "
|
|
309
|
+
f"{status}")
|
|
310
|
+
return 0
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
class _Module:
|
|
314
|
+
"""A module as ``MATE matrix`` sees it: constructed on its declared defaults, or why not."""
|
|
315
|
+
|
|
316
|
+
def __init__(self, entry: ModuleEntry) -> None:
|
|
317
|
+
self.entry = entry
|
|
318
|
+
self.instance: object | None = None
|
|
319
|
+
self.specs: NormalisedSpecs | None = None
|
|
320
|
+
self.note = ""
|
|
321
|
+
report = check_software([ModuleRef(entry.kind, entry.name)], None)
|
|
322
|
+
if not report.ok:
|
|
323
|
+
f = report.failures[0]
|
|
324
|
+
self.note = f"software: requires {f.requirement}; found {f.found}"
|
|
325
|
+
return
|
|
326
|
+
cls = report.classes[(entry.kind, entry.name)]
|
|
327
|
+
missing = missing_parameters(cls)
|
|
328
|
+
if missing:
|
|
329
|
+
self.note = f"needs settings: {', '.join(missing)}"
|
|
330
|
+
return
|
|
331
|
+
try:
|
|
332
|
+
self.instance = construct_module(cls, declared_defaults(cls))
|
|
333
|
+
except ModuleConstructionError as exc:
|
|
334
|
+
self.note = exc.message
|
|
335
|
+
return
|
|
336
|
+
if entry.kind == "system":
|
|
337
|
+
owns = bool(getattr(self.instance.capabilities, "owns_loop", False)) # type: ignore[union-attr]
|
|
338
|
+
specs, refusals = normalise_specs(self.instance, owns_loop=owns) # type: ignore[arg-type]
|
|
339
|
+
if specs is None:
|
|
340
|
+
self.note = "; ".join(r.message for r in refusals)
|
|
341
|
+
self.instance = None
|
|
342
|
+
return
|
|
343
|
+
self.specs = specs
|
|
344
|
+
if owns:
|
|
345
|
+
self.note = "owns its loop"
|
|
346
|
+
elif is_collective(specs):
|
|
347
|
+
self.note = "collective evaluation (not available in MATE 1.0.0)"
|
|
348
|
+
|
|
349
|
+
@property
|
|
350
|
+
def usable(self) -> bool:
|
|
351
|
+
"""Whether cells can be computed for it."""
|
|
352
|
+
return self.instance is not None and not self.note
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _cell(method: _Module, system: _Module) -> tuple[str, bool]:
|
|
356
|
+
"""``(cell text, compatible)`` of one pair ([X] MX2)."""
|
|
357
|
+
specs = system.specs
|
|
358
|
+
assert specs is not None and specs.interface is not None
|
|
359
|
+
report = check_interface(specs.interface, specs.performance, specs.roles,
|
|
360
|
+
method.instance.requirements, # type: ignore[union-attr]
|
|
361
|
+
system.instance.capabilities) # type: ignore[union-attr]
|
|
362
|
+
if report.outcome is Outcome.INCOMPATIBLE:
|
|
363
|
+
dims = ", ".join(dict.fromkeys(m.dimension for m in report.mismatches))
|
|
364
|
+
return f"incompatible ({dims})", False
|
|
365
|
+
if report.outcome is Outcome.CONDITIONAL:
|
|
366
|
+
bounds = ", ".join(dict.fromkeys(f"{c.organ}.{c.bound}" for c in report.conditional))
|
|
367
|
+
return f"conditional ({bounds})", True
|
|
368
|
+
return "compatible", True
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
_REQUIRED = frozenset({"P-META-MISSING", "P-SETTING-MISSING"})
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _required(refusals: Sequence[Refusal]) -> str:
|
|
375
|
+
"""``method: a, b; system: c``: the settings a smoke protocol lacks, by module."""
|
|
376
|
+
groups: dict[str, list[str]] = {}
|
|
377
|
+
for r in refusals:
|
|
378
|
+
kind = "system" if r.code == "P-SETTING-MISSING" else "method"
|
|
379
|
+
groups.setdefault(kind, []).append(r.where.rsplit(" ", 1)[-1])
|
|
380
|
+
return "; ".join(f"{kind}: {', '.join(names)}" for kind, names in groups.items())
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def _smoke(method: str, system: str, directory: Path) -> tuple[str, bool]:
|
|
384
|
+
"""Two real generations of a pair ([X] MX2): ``(outcome, passed)``.
|
|
385
|
+
|
|
386
|
+
The outcome is ``ok``, the stop cause, or the settings the pair needs, declared without
|
|
387
|
+
a default: a protocol must write them, so the pair is not run, and that is no failure.
|
|
388
|
+
"""
|
|
389
|
+
raw = {
|
|
390
|
+
"protocol": {"name": f"{method}-{system}"},
|
|
391
|
+
"system": {"module": system},
|
|
392
|
+
"method": {"module": method},
|
|
393
|
+
"replicates": {"count": 1, "root_seed": 0},
|
|
394
|
+
"stop": {"generations": 2},
|
|
395
|
+
"execution": {"executor": "serial", "retention": "minimal", "sinks": ["silent"],
|
|
396
|
+
"workdir": str(directory / f"{method}-{system}")},
|
|
397
|
+
}
|
|
398
|
+
try:
|
|
399
|
+
loaded = load_protocol(raw, cwd=directory)
|
|
400
|
+
except ProtocolRefused as exc:
|
|
401
|
+
if all(r.code in _REQUIRED for r in exc.refusals):
|
|
402
|
+
return f"settings required ({_required(exc.refusals)})", True
|
|
403
|
+
return "refused (" + ", ".join(dict.fromkeys(r.code for r in exc.refusals)) + ")", False
|
|
404
|
+
except SoftwareCompatibilityError:
|
|
405
|
+
return "refused (software)", False
|
|
406
|
+
try:
|
|
407
|
+
result = launch(loaded)
|
|
408
|
+
except Exception as exc: # a smoke test reports whatever breaks
|
|
409
|
+
return f"failed: {type(exc).__name__}: {exc}", False
|
|
410
|
+
causes = [c for c in result.runs.values() if c is not None]
|
|
411
|
+
if not causes:
|
|
412
|
+
return "not run", False
|
|
413
|
+
cause = causes[0]
|
|
414
|
+
if cause.kind == "criterion":
|
|
415
|
+
return "ok", True
|
|
416
|
+
return "failed: " + cause.kind + (f" ({cause.detail})" if cause.detail else ""), False
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def matrix(smoke: bool = False, entries: Sequence[ModuleEntry] | None = None
|
|
420
|
+
) -> tuple[list[str], bool]:
|
|
421
|
+
"""Compute the methods x systems table of installed modules ([X] §23.1).
|
|
422
|
+
|
|
423
|
+
Parameters
|
|
424
|
+
----------
|
|
425
|
+
smoke : bool, default False
|
|
426
|
+
Run two generations of every compatible pair (serial executor, temporary
|
|
427
|
+
directory, seed 0, retention ``minimal``).
|
|
428
|
+
entries : sequence of ModuleEntry, optional
|
|
429
|
+
The modules; default every installed module.
|
|
430
|
+
|
|
431
|
+
Returns
|
|
432
|
+
-------
|
|
433
|
+
tuple
|
|
434
|
+
``(lines, ok)``: a line per module that yields no cell (with the reason), then one
|
|
435
|
+
line per (method, system) pair; ``ok`` is ``False`` when a smoke run failed. A pair
|
|
436
|
+
whose modules declare settings without a default shows them as required and does
|
|
437
|
+
not fail.
|
|
438
|
+
"""
|
|
439
|
+
entries = _modules() if entries is None else list(entries)
|
|
440
|
+
modules = [_Module(e) for e in entries]
|
|
441
|
+
methods = [m for m in modules if m.entry.kind == "method"]
|
|
442
|
+
systems = [m for m in modules if m.entry.kind == "system"]
|
|
443
|
+
lines: list[str] = []
|
|
444
|
+
for m in modules:
|
|
445
|
+
if not m.usable:
|
|
446
|
+
lines.append(f"{m.entry.kind} {m.entry.name}: {m.note}")
|
|
447
|
+
width_m = max((len(m.entry.name) for m in methods), default=6)
|
|
448
|
+
width_s = max((len(s.entry.name) for s in systems), default=6)
|
|
449
|
+
lines.append(f"{'method':<{width_m}} {'system':<{width_s}} result")
|
|
450
|
+
ok = True
|
|
451
|
+
with tempfile.TemporaryDirectory(prefix="MATE-matrix-") as tmp:
|
|
452
|
+
for m in methods:
|
|
453
|
+
for s in systems:
|
|
454
|
+
if not (m.usable and s.usable):
|
|
455
|
+
cell = "-"
|
|
456
|
+
compatible = False
|
|
457
|
+
else:
|
|
458
|
+
cell, compatible = _cell(m, s)
|
|
459
|
+
if smoke and compatible:
|
|
460
|
+
outcome, passed = _smoke(m.entry.name, s.entry.name, Path(tmp))
|
|
461
|
+
ok = ok and passed
|
|
462
|
+
cell += f"; smoke: {outcome}"
|
|
463
|
+
lines.append(f"{m.entry.name:<{width_m}} {s.entry.name:<{width_s}} {cell}")
|
|
464
|
+
return lines, ok
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def cmd_matrix(args: argparse.Namespace) -> int:
|
|
468
|
+
"""``MATE matrix [--smoke]`` ([X] §23.1): status 2 when a smoke run fails."""
|
|
469
|
+
lines, ok = matrix(args.smoke)
|
|
470
|
+
_out("\n".join(lines))
|
|
471
|
+
return 0 if ok else 2
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def _settings(pairs: Sequence[str]) -> dict[str, Value]:
|
|
475
|
+
"""``--set k=v`` -> settings, each value a TOML value."""
|
|
476
|
+
out: dict[str, Value] = {}
|
|
477
|
+
for pair in pairs:
|
|
478
|
+
key, sep, text = pair.partition("=")
|
|
479
|
+
if not sep or not key.strip():
|
|
480
|
+
raise UsageError(f"--set {pair!r}: expected <key>=<value>")
|
|
481
|
+
try:
|
|
482
|
+
out[key.strip()] = parse_toml_value(text)
|
|
483
|
+
except ValueError as exc:
|
|
484
|
+
raise UsageError(f"--set {pair!r}: {exc}") from None
|
|
485
|
+
return out
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def cmd_check(args: argparse.Namespace) -> int:
|
|
489
|
+
"""``MATE check <module>`` ([X] §22): status 2 when a test fails (CF7)."""
|
|
490
|
+
# Imported here: the conformance suite is a heavy, separate part of the runtime.
|
|
491
|
+
from MATE.conformance import check_module
|
|
492
|
+
|
|
493
|
+
settings = _settings(args.set or [])
|
|
494
|
+
try:
|
|
495
|
+
results = check_module(args.module, kind=args.kind, system=args.system,
|
|
496
|
+
method=args.method,
|
|
497
|
+
settings=settings, tolerance=args.tolerance)
|
|
498
|
+
except (ProtocolRefused, SoftwareCompatibilityError) as exc:
|
|
499
|
+
_err(exc.render())
|
|
500
|
+
return 1
|
|
501
|
+
except ValueError as exc:
|
|
502
|
+
_err(str(exc))
|
|
503
|
+
return 1
|
|
504
|
+
failed = 0
|
|
505
|
+
for r in results:
|
|
506
|
+
failed += r.status == "fail"
|
|
507
|
+
_out(f" {r.test:<15} {r.status:<5} {r.detail}".rstrip())
|
|
508
|
+
_out(f"{args.module}: {len(results)} test(s), {failed} failed")
|
|
509
|
+
return 2 if failed else 0
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def cmd_search(args: argparse.Namespace) -> int:
|
|
513
|
+
"""``MATE search <text>`` ([X] RG5): one line per module, with the version installed by
|
|
514
|
+
default."""
|
|
515
|
+
try:
|
|
516
|
+
found = registry.search(args.text)
|
|
517
|
+
except registry.RegistryError as exc:
|
|
518
|
+
_err(str(exc))
|
|
519
|
+
return 1
|
|
520
|
+
for url in registry.registry_urls():
|
|
521
|
+
age = registry.cache_age(url)
|
|
522
|
+
if age is not None and age >= 86400:
|
|
523
|
+
_out(f"(registry {url}: cache from {age / 86400:.1f} day(s) ago, offline or not "
|
|
524
|
+
f"updated)")
|
|
525
|
+
for e in found:
|
|
526
|
+
latest = e.latest()
|
|
527
|
+
version = latest.version if latest is not None else "withdrawn"
|
|
528
|
+
for m in e.modules:
|
|
529
|
+
organs = ", ".join(f"{k}: {v}" for k, v in m.organs.items())
|
|
530
|
+
owner = f" ({e.distribution})" if m.name != e.name else ""
|
|
531
|
+
_out(f"{m.kind:<7} {m.name:<20} {version:<12} {e.summary}{owner}"
|
|
532
|
+
+ (f" [{organs}]" if organs else ""))
|
|
533
|
+
if not found:
|
|
534
|
+
_out(f"no module matches {args.text!r}")
|
|
535
|
+
return 0
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def cmd_install(args: argparse.Namespace) -> int:
|
|
539
|
+
"""``MATE install <name>...`` ([X] RG6): pip, then the software check of the modules."""
|
|
540
|
+
status = 0
|
|
541
|
+
for name in args.names:
|
|
542
|
+
try:
|
|
543
|
+
done = registry.install(name)
|
|
544
|
+
except registry.RegistryError as exc:
|
|
545
|
+
_err(str(exc))
|
|
546
|
+
status = 1
|
|
547
|
+
continue
|
|
548
|
+
wanted = {canonicalize_name(d) for d in done.distributions}
|
|
549
|
+
refs = [ModuleRef(e.kind, e.name) for e in _modules()
|
|
550
|
+
if canonicalize_name(e.distribution) in wanted]
|
|
551
|
+
if not refs:
|
|
552
|
+
_out(f"installed {done.target}; it declares no MATE module")
|
|
553
|
+
continue
|
|
554
|
+
report = check_software(refs, None)
|
|
555
|
+
(_out if report.ok else _err)(render(report))
|
|
556
|
+
status = status or (0 if report.ok else 1)
|
|
557
|
+
return status
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
# --- deferred ----------------------------------------------------------------------------------
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def cmd_replay(args: argparse.Namespace) -> int:
|
|
564
|
+
"""``MATE replay``: refused in MATE 1.0.0 (design §1.3)."""
|
|
565
|
+
_err(REPLAY_REFUSAL)
|
|
566
|
+
return 1
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def cmd_dashboard(args: argparse.Namespace) -> int:
|
|
570
|
+
"""``MATE dashboard``: refused in MATE 1.0.0 (design §1.3)."""
|
|
571
|
+
_err(DASHBOARD_REFUSAL)
|
|
572
|
+
return 1
|
|
573
|
+
|