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.
Files changed (93) hide show
  1. MATE/__init__.py +22 -0
  2. MATE/__main__.py +7 -0
  3. MATE/cli/__init__.py +181 -0
  4. MATE/cli/commands.py +573 -0
  5. MATE/cli/workdir.py +571 -0
  6. MATE/compat/__init__.py +1 -0
  7. MATE/compat/interface.py +248 -0
  8. MATE/compat/sizes.py +144 -0
  9. MATE/config/__init__.py +1 -0
  10. MATE/config/axes.py +310 -0
  11. MATE/config/builders.py +498 -0
  12. MATE/config/calendar.py +371 -0
  13. MATE/config/canonical.py +543 -0
  14. MATE/config/criteria.py +396 -0
  15. MATE/config/events.py +183 -0
  16. MATE/config/invariance.py +227 -0
  17. MATE/config/loader.py +498 -0
  18. MATE/config/model.py +883 -0
  19. MATE/config/resolve.py +943 -0
  20. MATE/config/runtable.py +342 -0
  21. MATE/config/seeds.py +344 -0
  22. MATE/config/stages.py +632 -0
  23. MATE/config/structure.py +635 -0
  24. MATE/config/toml.py +237 -0
  25. MATE/conformance/__init__.py +14 -0
  26. MATE/conformance/baseline.py +250 -0
  27. MATE/conformance/checks.py +1604 -0
  28. MATE/conformance/sampler.py +179 -0
  29. MATE/errors.py +236 -0
  30. MATE/evaluation/__init__.py +1 -0
  31. MATE/evaluation/budget.py +160 -0
  32. MATE/evaluation/invalids.py +181 -0
  33. MATE/evaluation/requests.py +148 -0
  34. MATE/execution/__init__.py +1 -0
  35. MATE/execution/accelerator.py +60 -0
  36. MATE/execution/base.py +643 -0
  37. MATE/execution/choose.py +152 -0
  38. MATE/execution/devices.py +108 -0
  39. MATE/execution/process.py +377 -0
  40. MATE/execution/serial.py +37 -0
  41. MATE/execution/thread.py +106 -0
  42. MATE/execution/vectorised.py +32 -0
  43. MATE/launch.py +267 -0
  44. MATE/loop/__init__.py +6 -0
  45. MATE/loop/continuous.py +172 -0
  46. MATE/loop/driver.py +783 -0
  47. MATE/loop/events.py +266 -0
  48. MATE/loop/generational.py +140 -0
  49. MATE/loop/owns_loop.py +333 -0
  50. MATE/loop/subpops.py +508 -0
  51. MATE/modules/__init__.py +1 -0
  52. MATE/modules/construct.py +198 -0
  53. MATE/modules/discovery.py +238 -0
  54. MATE/modules/registry.py +970 -0
  55. MATE/modules/software.py +593 -0
  56. MATE/modules/versions.py +72 -0
  57. MATE/numerics.py +159 -0
  58. MATE/observation/__init__.py +10 -0
  59. MATE/observation/control.py +199 -0
  60. MATE/observation/hub.py +451 -0
  61. MATE/observation/records.py +125 -0
  62. MATE/observation/sinks.py +313 -0
  63. MATE/provenance.py +396 -0
  64. MATE/readout/__init__.py +10 -0
  65. MATE/readout/catalogue.py +282 -0
  66. MATE/readout/front.py +151 -0
  67. MATE/readout/readout.py +927 -0
  68. MATE/readout/series.py +155 -0
  69. MATE/readout/statistics.py +202 -0
  70. MATE/run/__init__.py +1 -0
  71. MATE/run/checkpointing.py +266 -0
  72. MATE/run/contexts.py +553 -0
  73. MATE/run/declarations.py +240 -0
  74. MATE/run/plan.py +464 -0
  75. MATE/run/preconditions.py +165 -0
  76. MATE/run/run.py +991 -0
  77. MATE/run/specs.py +319 -0
  78. MATE/run/times.py +103 -0
  79. MATE/scheduling/__init__.py +214 -0
  80. MATE/scheduling/local.py +119 -0
  81. MATE/scheduling/slurm.py +380 -0
  82. MATE/storage/__init__.py +1 -0
  83. MATE/storage/checkpoint.py +414 -0
  84. MATE/storage/codec.py +315 -0
  85. MATE/storage/database.py +359 -0
  86. MATE/storage/lease.py +397 -0
  87. MATE/storage/rundir.py +248 -0
  88. mate_runtime-1.0.0.dist-info/METADATA +58 -0
  89. mate_runtime-1.0.0.dist-info/RECORD +93 -0
  90. mate_runtime-1.0.0.dist-info/WHEEL +4 -0
  91. mate_runtime-1.0.0.dist-info/entry_points.txt +2 -0
  92. mate_runtime-1.0.0.dist-info/licenses/COPYING +674 -0
  93. mate_runtime-1.0.0.dist-info/licenses/COPYING.LESSER +165 -0
MATE/cli/workdir.py ADDED
@@ -0,0 +1,571 @@
1
+ """Readers of a workdir and of its run directories, for the command line.
2
+
3
+ ``MATE status``, ``verify`` and ``runs`` read the run table ([C] R21.4, R19.8); ``MATE diff``
4
+ compares blueprints ([C] R21.5); ``MATE show`` summarises run databases (design §1.2);
5
+ :func:`branch_protocol` builds the protocol of ``MATE branch`` ([C] R21.3). Each reader
6
+ returns the text the command prints, or raises :class:`WorkdirError`.
7
+ """
8
+
9
+ import sqlite3
10
+ import tomllib
11
+ from collections.abc import Mapping, Sequence
12
+ from pathlib import Path
13
+ from typing import Literal
14
+
15
+ from MATE.config import canonical
16
+ from MATE.config.invariance import Violation, default_hold, shared_branch_mismatches, verify
17
+ from MATE.config.model import Value, designates
18
+ from MATE.config.runtable import PlannedRun, RunTable
19
+ from MATE.errors import MATEError
20
+ from MATE.launch import run_state
21
+ from MATE.provenance import Provenance, read_provenance
22
+ from MATE.storage import codec
23
+ from MATE.storage.checkpoint import find_addressable
24
+ from MATE.storage.lease import live_lease, read_lease, running
25
+ from MATE.storage.rundir import RunDirectory
26
+
27
+ __all__ = ["WorkdirError", "status", "verify_workdir", "runs", "diff", "show",
28
+ "branch_protocol"]
29
+
30
+
31
+ class WorkdirError(MATEError):
32
+ """A workdir command cannot proceed.
33
+
34
+ Parameters
35
+ ----------
36
+ message : str
37
+ exit_status : int, default 3
38
+ 1 for a refusal (an invariant violated, a best without a total order), 3 for a
39
+ usage error (no run table, an ambiguous protocol).
40
+ """
41
+
42
+ message: str
43
+ exit_status: int
44
+
45
+ def __init__(self, message: str, exit_status: int = 3) -> None:
46
+ self.message = message
47
+ self.exit_status = exit_status
48
+ super().__init__(message)
49
+
50
+
51
+ # --- the run table ---------------------------------------------------------------------------
52
+
53
+
54
+ def _table(workdir: Path) -> RunTable:
55
+ """The run table of a workdir; a usage error without one."""
56
+ path = Path(workdir)
57
+ if path.exists() and not path.is_dir():
58
+ if path.suffix == ".toml":
59
+ raise WorkdirError(f"{workdir}: a protocol file, not a workdir; give the directory "
60
+ f"its launch wrote ([execution] workdir)")
61
+ raise WorkdirError(f"{workdir}: a file, not a workdir")
62
+ try:
63
+ table = RunTable.load(path)
64
+ except ValueError as exc:
65
+ raise WorkdirError(str(exc), 1) from None
66
+ if table is None:
67
+ raise WorkdirError(f"{workdir}: no run table (not a workdir)")
68
+ return table
69
+
70
+
71
+ def _protocol(table: RunTable, protocol: str | None, required: bool) -> str | None:
72
+ """The protocol aggregated: given, implied by a single one, or mandatory (R18.9)."""
73
+ if protocol is not None:
74
+ if protocol not in table.protocols:
75
+ raise WorkdirError(f"protocol '{protocol}' is not in the run table (found: "
76
+ f"{', '.join(table.protocols) or 'none'})")
77
+ return protocol
78
+ if len(table.protocols) == 1:
79
+ return next(iter(table.protocols))
80
+ if required and table.protocols:
81
+ raise WorkdirError(f"the workdir holds several protocols ({', '.join(table.protocols)});"
82
+ f" name one with --protocol")
83
+ return None
84
+
85
+
86
+ def _missing_state(row: PlannedRun, table: RunTable, workdir: Path) -> str | None:
87
+ """The R17.18 message of a chained run whose designated state will not exist."""
88
+ if row.parent is None:
89
+ return None
90
+ parent_ref, generation = row.parent
91
+ parent = table.find(parent_ref)
92
+ if parent is None:
93
+ return None
94
+ parent_dir = RunDirectory(Path(workdir) / parent.directory)
95
+ state, cause = run_state(parent_dir.path)
96
+ if state not in ("done", "failed") or find_addressable(parent_dir, generation) is not None:
97
+ return None
98
+ head = f"state {parent.directory}@{generation} missing"
99
+ if cause is None:
100
+ return f"{head}: parent did not run"
101
+ reason = cause.kind + (f": {cause.detail}" if cause.detail else "")
102
+ at = f" at generation {cause.generation}" if cause.generation is not None else ""
103
+ return f"{head}: parent stopped{at} ({reason})"
104
+
105
+
106
+ def _state(row: PlannedRun, table: RunTable, workdir: Path) -> tuple[str, str]:
107
+ """``(state label, detail)`` of a row."""
108
+ directory = Path(workdir) / row.directory
109
+ missing = _missing_state(row, table, workdir)
110
+ if not directory.is_dir():
111
+ return ("missing state", missing) if missing else ("not started", "")
112
+ holder = live_lease(directory)
113
+ if holder is not None:
114
+ return "running", holder.describe()
115
+ state, cause = run_state(directory)
116
+ if cause is not None and cause.kind == "missing_state":
117
+ return "missing state", cause.detail
118
+ if state == "new" and missing:
119
+ return "missing state", missing
120
+ labels = {"new": "not started", "interrupted": "interrupted", "running": "running",
121
+ "done": "done", "failed": "failed"}
122
+ detail = ""
123
+ if cause is not None:
124
+ detail = cause.kind + (f": {cause.detail}" if cause.detail else "")
125
+ if cause.generation is not None:
126
+ detail += f" (generation {cause.generation})"
127
+ elif state == "interrupted" and (stale := read_lease(directory)) is not None:
128
+ detail = f"lease left behind: {stale.describe()}" # killed without ending
129
+ return labels[state], detail
130
+
131
+
132
+ def status(workdir: Path, protocol: str | None = None) -> str:
133
+ """Report the progress of a workdir's runs ([C] R21.4).
134
+
135
+ Parameters
136
+ ----------
137
+ workdir : Path
138
+ protocol : str, optional
139
+ Restrict to one protocol's rows; default every row.
140
+
141
+ Returns
142
+ -------
143
+ str
144
+ One line per run (state and stop cause; host, pid and age of the last beat for a
145
+ running run), then the counts per state; missing states are reported with their
146
+ cause (R17.18).
147
+
148
+ Raises
149
+ ------
150
+ WorkdirError
151
+ Without a run table, or for an unknown protocol.
152
+ """
153
+ table = _table(workdir)
154
+ name = _protocol(table, protocol, required=False) if protocol is not None else None
155
+ rows = table.rows(name)
156
+ counts: dict[str, int] = {}
157
+ lines = [f"workdir {workdir}" + (f", protocol {name}" if name else "")]
158
+ for row in rows:
159
+ label, detail = _state(row, table, workdir)
160
+ counts[label] = counts.get(label, 0) + 1
161
+ lines.append(f" {row.directory:<10} {label:<13} {detail}".rstrip())
162
+ summary = ", ".join(f"{n} {label}" for label, n in counts.items())
163
+ lines.append(f"{len(rows)} run(s)" + (f": {summary}" if summary else ""))
164
+ return "\n".join(lines)
165
+
166
+
167
+ # --- invariance --------------------------------------------------------------------------------
168
+
169
+
170
+ def _hold(rows: Sequence[PlannedRun], workdir: Path,
171
+ provenances: Mapping[str, Provenance]) -> tuple[str, ...]:
172
+ """The held keys: ``[invariant] hold`` of the launch's protocol files, else the default."""
173
+ for row in rows:
174
+ sources = Path(workdir) / row.directory / "sources"
175
+ chain = [sources / "protocol.toml"]
176
+ chain += sorted(sources.glob("extends-*.toml"), key=lambda p: int(p.stem.split("-")[1]))
177
+ if not chain[0].is_file():
178
+ continue
179
+ for path in chain:
180
+ try:
181
+ with open(path, "rb") as handle:
182
+ table = tomllib.load(handle)
183
+ except (OSError, tomllib.TOMLDecodeError):
184
+ break
185
+ hold = table.get("invariant", {}).get("hold")
186
+ if isinstance(hold, list):
187
+ return tuple(hold)
188
+ break
189
+ owns_loop = bool(provenances) and all(
190
+ not any(m.kind == "method" for m in p.modules) for p in provenances.values())
191
+ return default_hold(owns_loop)
192
+
193
+
194
+ def _provenances(rows: Sequence[PlannedRun], workdir: Path) -> dict[str, Provenance]:
195
+ """Run directory -> provenance, for the runs that have one."""
196
+ out: dict[str, Provenance] = {}
197
+ for row in rows:
198
+ try:
199
+ out[row.directory] = read_provenance(Path(workdir) / row.directory)
200
+ except (OSError, ValueError):
201
+ continue
202
+ return out
203
+
204
+
205
+ def _violation_lines(violations: Sequence[Violation]) -> list[str]:
206
+ """One block per violation: key, subject, each value with its runs."""
207
+ lines: list[str] = []
208
+ for v in violations:
209
+ lines.append(f" {v.key} ({v.subject}) takes {len(v.found)} values:")
210
+ for value, dirs in v.found.items():
211
+ lines.append(f" {value}: {', '.join(dirs)}")
212
+ return lines
213
+
214
+
215
+ def _violations(workdir: Path, protocol: str | None
216
+ ) -> tuple[str | None, list[PlannedRun], tuple[str, ...], list[Violation]]:
217
+ """The protocol aggregated, its rows, the held keys and every violation."""
218
+ table = _table(workdir)
219
+ name = _protocol(table, protocol, required=True)
220
+ rows = table.rows(name)
221
+ provenances = _provenances(rows, workdir)
222
+ hold = _hold(rows, workdir, provenances)
223
+ found = verify(hold, provenances) + shared_branch_mismatches(rows, workdir)
224
+ return name, rows, hold, found
225
+
226
+
227
+ def verify_workdir(workdir: Path, protocol: str | None = None) -> tuple[str, bool]:
228
+ """Check the held invariants over one protocol's runs ([C] R19.8).
229
+
230
+ Parameters
231
+ ----------
232
+ workdir : Path
233
+ protocol : str, optional
234
+ Mandatory when the workdir holds several protocols (R18.9).
235
+
236
+ Returns
237
+ -------
238
+ tuple
239
+ ``(report, ok)``: every violation (key, subject, each value with its runs) and the
240
+ shared-branch mismatches; ``ok`` is ``False`` when there is any.
241
+
242
+ Raises
243
+ ------
244
+ WorkdirError
245
+ Without a run table, or for a missing or unknown protocol.
246
+
247
+ Notes
248
+ -----
249
+ The held keys are those of ``[invariant] hold`` in the protocol files copied into the
250
+ first run's ``sources/``, else the default set (R19.5).
251
+ """
252
+ name, rows, hold, found = _violations(workdir, protocol)
253
+ head = f"protocol {name}: " if name else ""
254
+ if not found:
255
+ return f"{head}{len(rows)} run(s), no violation (held: {', '.join(hold)})", True
256
+ lines = [f"{head}{len(found)} violation(s)"] + _violation_lines(found)
257
+ return "\n".join(lines), False
258
+
259
+
260
+ def _axis(value: Value) -> str:
261
+ """An axis value as written in a report."""
262
+ return codec.to_json(canonical.thaw(value))
263
+
264
+
265
+ def runs(workdir: Path, protocol: str | None = None) -> str:
266
+ """Return the run table of one protocol: blueprint -> runs, axis values ([C] R21.4).
267
+
268
+ Parameters
269
+ ----------
270
+ workdir : Path
271
+ protocol : str, optional
272
+ Mandatory when the workdir holds several protocols.
273
+
274
+ Returns
275
+ -------
276
+ str
277
+ One line per run: directory, stage, short fingerprint, replicate, seed, state, axes.
278
+
279
+ Raises
280
+ ------
281
+ WorkdirError
282
+ On any invariant violation (exit status 1, R19.8), without a run table, or for a
283
+ missing or unknown protocol.
284
+ """
285
+ name, rows, _, found = _violations(workdir, protocol)
286
+ if found:
287
+ raise WorkdirError("\n".join([f"protocol {name}: invariants violated, no aggregation"]
288
+ + _violation_lines(found)), 1)
289
+ table = _table(workdir)
290
+ lines = [f"{'run':<10} {'stage':<10} {'blueprint':<12} {'rep':>4} {'state':<13} "
291
+ f"{'seed':<40} axes"]
292
+ for row in rows:
293
+ label, _ = _state(row, table, workdir)
294
+ axes = ", ".join(f"{k} = {_axis(v)}" for k, v in row.axes.items())
295
+ lines.append(f"{row.directory:<10} {row.stage or '-':<10} "
296
+ f"{row.identity.fingerprint[:12]:<12} {row.replicate:>4} {label:<13} "
297
+ f"{row.identity.seed!s:<40} {axes}".rstrip())
298
+ return "\n".join(lines)
299
+
300
+
301
+ # --- diff --------------------------------------------------------------------------------------
302
+
303
+
304
+ def _blueprints(path: Path) -> list[dict[str, Value]]:
305
+ """The flat blueprints of a run directory (one) or of a workdir (each, not superseded)."""
306
+ path = Path(path)
307
+ if (path / "blueprint.toml").is_file():
308
+ return [RunDirectory(path).read_blueprint()[0]]
309
+ table = _table(path)
310
+ seen: dict[str, dict[str, Value]] = {}
311
+ for row in table.rows(None):
312
+ fp = row.identity.fingerprint
313
+ file = path / row.directory / "blueprint.toml"
314
+ if fp not in seen and file.is_file():
315
+ seen[fp] = RunDirectory(path / row.directory).read_blueprint()[0]
316
+ if not seen:
317
+ raise WorkdirError(f"{path}: no recorded blueprint")
318
+ return list(seen.values())
319
+
320
+
321
+ def _values(blueprints: Sequence[Mapping[str, Value]], key: str) -> list[str]:
322
+ """The distinct values of a path over blueprints, in order of first appearance."""
323
+ out: list[str] = []
324
+ for b in blueprints:
325
+ if key in b and (v := _axis(b[key])) not in out:
326
+ out.append(v)
327
+ return out
328
+
329
+
330
+ def diff(a: Path, b: Path) -> str:
331
+ """Compare the blueprints of two run directories or workdirs ([C] R21.5).
332
+
333
+ Parameters
334
+ ----------
335
+ a, b : Path
336
+ Each a run directory or a workdir.
337
+
338
+ Returns
339
+ -------
340
+ str
341
+ The flat paths constant within each side whose value differs between the sides,
342
+ then (with a workdir) the paths that vary within a side with their value sets, then
343
+ the paths present on one side only. Sides with identical blueprints give
344
+ ``no difference``.
345
+
346
+ Raises
347
+ ------
348
+ WorkdirError
349
+ For a path that is neither a run directory nor a workdir.
350
+ """
351
+ left, right = _blueprints(a), _blueprints(b)
352
+ keys_l = list(dict.fromkeys(k for bp in left for k in bp))
353
+ keys_r = list(dict.fromkeys(k for bp in right for k in bp))
354
+ both = [k for k in keys_l if k in keys_r]
355
+ differ, vary = [], []
356
+ for k in both:
357
+ vl, vr = _values(left, k), _values(right, k)
358
+ full = all(k in bp for bp in left) and all(k in bp for bp in right)
359
+ if len(vl) == 1 and len(vr) == 1 and full:
360
+ if vl != vr:
361
+ differ.append(f" {k}: {vl[0]} | {vr[0]}")
362
+ elif vl != vr or len(vl) > 1:
363
+ vary.append(f" {k}: {{{', '.join(vl)}}} | {{{', '.join(vr)}}}")
364
+ only_l = [f" {k} = {', '.join(_values(left, k))}" for k in keys_l if k not in keys_r]
365
+ only_r = [f" {k} = {', '.join(_values(right, k))}" for k in keys_r if k not in keys_l]
366
+ lines: list[str] = [f"--- {a}", f"+++ {b}"]
367
+ for title, block in ((f"differ ({a} | {b}):", differ),
368
+ ("vary within a side:", vary),
369
+ (f"only in {a}:", only_l),
370
+ (f"only in {b}:", only_r)):
371
+ if block:
372
+ lines.append(title)
373
+ lines.extend(block)
374
+ if len(lines) == 2:
375
+ lines.append("no difference")
376
+ return "\n".join(lines)
377
+
378
+
379
+ # --- show --------------------------------------------------------------------------------------
380
+
381
+
382
+ def _query(db: Path, sql: str, params: Sequence[object] = ()) -> list[tuple]:
383
+ """Run one read-only query on a run database."""
384
+ connection = sqlite3.connect(f"file:{db}?mode=ro", uri=True)
385
+ try:
386
+ return connection.execute(sql, tuple(params)).fetchall()
387
+ finally:
388
+ connection.close()
389
+
390
+
391
+ def _number(x: object) -> str:
392
+ """A number as shown in a summary."""
393
+ return "-" if x is None else f"{x:.6g}" if isinstance(x, float) else str(x)
394
+
395
+
396
+ def _show_run(directory: Path, objective: str | None, subpop: str | None) -> list[str]:
397
+ """The summary of one run."""
398
+ rundir = RunDirectory(directory)
399
+ db = rundir.database
400
+ if not db.is_file():
401
+ return [f"{rundir.name}: no database (not started)"]
402
+ lines = [f"{rundir.name}"]
403
+ try:
404
+ _, identity, stage = rundir.read_blueprint()
405
+ lines.append(f" blueprint {identity.fingerprint[:12]}, seed {identity.seed}"
406
+ + (f", stage {stage}" if stage else ""))
407
+ except (OSError, ValueError):
408
+ pass
409
+ outcome = _query(db, "SELECT status, kind, detail, run_generation FROM outcome "
410
+ "ORDER BY seq DESC LIMIT 1")
411
+ holder = live_lease(directory)
412
+ if holder is not None:
413
+ lines.append(f" status {running(holder)}")
414
+ elif outcome:
415
+ st, kind, detail, g = outcome[0]
416
+ at = f" at generation {g}" if g is not None else ""
417
+ lines.append(f" status {st}: {kind}" + (f" ({detail})" if detail else "") + at)
418
+ else:
419
+ lines.append(" status no outcome recorded (interrupted)")
420
+ gens = _query(db, "SELECT run_generation, spent_episodes, spent_steps FROM generations "
421
+ "ORDER BY run_generation DESC LIMIT 1")
422
+ if gens:
423
+ lines.append(f" generations {gens[0][0] + 1}")
424
+ lines.append(f" consumption {_number(gens[0][1])} episodes, {_number(gens[0][2])} steps")
425
+ try:
426
+ prov = read_provenance(directory)
427
+ modules = ", ".join(f"{m.kind} {m.name} {m.version}" for m in prov.modules)
428
+ lines.append(f" versions runtime {prov.runtime_version}, contract "
429
+ f"{prov.contract_version}" + (f", {modules}" if modules else ""))
430
+ except (OSError, ValueError):
431
+ pass
432
+
433
+ paths = [p for (p,) in _query(db, "SELECT path FROM subpops ORDER BY seq")]
434
+ paths = list(dict.fromkeys(paths))
435
+ if subpop is not None:
436
+ chosen = [p for p in paths if designates(subpop, p)]
437
+ if not chosen:
438
+ raise WorkdirError(f"{rundir.name}: no subpopulation '{subpop}' (found: "
439
+ f"{', '.join(paths) or 'none'})")
440
+ elif len(paths) > 1:
441
+ raise WorkdirError(f"{rundir.name}: several subpopulations ({', '.join(paths)}); "
442
+ f"name one with --subpop", 1)
443
+ else:
444
+ chosen = paths
445
+ for path in chosen:
446
+ lines.extend(_show_subpop(db, rundir.name, path, objective))
447
+ return lines
448
+
449
+
450
+ def _show_subpop(db: Path, run: str, path: str, objective: str | None) -> list[str]:
451
+ """Series per generation and best individual of one subpopulation."""
452
+ stats = _query(db, "SELECT run_generation, objective, best, mean FROM statistics "
453
+ "WHERE subpop = ? ORDER BY seq", (path,))
454
+ objectives = list(dict.fromkeys(o for _, o, _, _ in stats))
455
+ if objective is not None and objective not in objectives:
456
+ raise WorkdirError(f"{run}: no objective '{objective}' (found: "
457
+ f"{', '.join(objectives) or 'none'})")
458
+ if objective is None and len(objectives) >= 2:
459
+ raise WorkdirError(f"{run}: {len(objectives)} objectives ({', '.join(objectives)}): "
460
+ f"the best individual has no total order; name one with "
461
+ f"--objective", 1)
462
+ lines = [f" subpopulation {path}"]
463
+ for name in objectives:
464
+ lines.append(f" performance.{name}: generation, best, mean")
465
+ for g, o, best, mean in stats:
466
+ if o == name:
467
+ lines.append(f" {g:>6} {_number(best):>12} {_number(mean):>12}")
468
+ if not objectives:
469
+ lines.append(" no performance statistics recorded")
470
+ return lines
471
+ name = objective if objective is not None else objectives[0]
472
+ index = objectives.index(name)
473
+ rows = _query(db, "SELECT e.genome_id, e.run_generation, e.objectives FROM evaluations e "
474
+ "WHERE e.subpop = ? AND e.done = 1 AND e.objectives IS NOT NULL AND "
475
+ "e.genome_id IN (SELECT genome_id FROM fronts WHERE scope = 'run' AND "
476
+ "subpop = ?) ORDER BY e.rank", (path, path))
477
+ if not rows:
478
+ rows = _query(db, "SELECT genome_id, run_generation, objectives FROM evaluations "
479
+ "WHERE subpop = ? AND done = 1 AND objectives IS NOT NULL "
480
+ "ORDER BY rank", (path,))
481
+ best: tuple[float, str, object, list] | None = None
482
+ for gid, g, text in rows:
483
+ values = codec.from_json(text)
484
+ if index < len(values) and (best is None or values[index] > best[0]):
485
+ best = (values[index], gid, g, values)
486
+ if best is None:
487
+ lines.append(f" best ({name}): no completed evaluation")
488
+ else:
489
+ shown = ", ".join(_number(v) for v in best[3])
490
+ lines.append(f" best ({name}): genome {best[1]}, generation {best[2]}, "
491
+ f"objectives ({shown})")
492
+ return lines
493
+
494
+
495
+ def show(path: Path, objective: str | None = None, subpop: str | None = None) -> str:
496
+ """Summarise a run, or every run of a workdir (design §1.2).
497
+
498
+ Parameters
499
+ ----------
500
+ path : Path
501
+ A run directory or a workdir.
502
+ objective : str, optional
503
+ The objective by which the best individual is chosen; mandatory with K >= 2
504
+ ([X] ST4).
505
+ subpop : str, optional
506
+ The subpopulation shown; mandatory with several ([X] SQ1).
507
+
508
+ Returns
509
+ -------
510
+ str
511
+ Per run: stop cause (or who runs it), generations, consumption, versions, then per
512
+ subpopulation the performance statistics per generation and the best individual
513
+ (objectives are maximised).
514
+
515
+ Raises
516
+ ------
517
+ WorkdirError
518
+ Exit status 1 for a best without total order or several subpopulations unnamed;
519
+ 3 for a path that is neither a run directory nor a workdir.
520
+ """
521
+ path = Path(path)
522
+ if (path / "blueprint.toml").is_file() or (path / "run.sqlite").is_file():
523
+ return "\n".join(_show_run(path, objective, subpop))
524
+ table = _table(path)
525
+ out: list[str] = []
526
+ for row in table.rows(None):
527
+ out.extend(_show_run(path / row.directory, objective, subpop))
528
+ return "\n".join(out) if out else f"{path}: no run"
529
+
530
+
531
+ # --- branch ------------------------------------------------------------------------------------
532
+
533
+
534
+ def branch_protocol(run_dir: Path, generation: int | Literal["end"], count: int,
535
+ root_seed: int | Literal["fresh"]) -> dict:
536
+ """Build the protocol of ``MATE branch <run>@<generation>`` ([C] R21.3).
537
+
538
+ Parameters
539
+ ----------
540
+ run_dir : Path
541
+ The recorded run.
542
+ generation : int or "end"
543
+ count : int
544
+ ``[replicates] count``.
545
+ root_seed : int or "fresh"
546
+ ``[replicates] root_seed``.
547
+
548
+ Returns
549
+ -------
550
+ dict
551
+ The run's resolved blueprint as a raw protocol table, with ``[start] kind =
552
+ "from_run"``, ``run`` (the absolute run directory) and ``generation``, the
553
+ replicates, and ``[protocol] name = "<run directory name>@<generation>"``. No
554
+ ``[execution]`` table: the command line supplies it.
555
+
556
+ Raises
557
+ ------
558
+ WorkdirError
559
+ For a directory without ``blueprint.toml``.
560
+ """
561
+ rundir = RunDirectory(Path(run_dir).resolve())
562
+ try:
563
+ flat, _, _ = rundir.read_blueprint()
564
+ except (OSError, ValueError) as exc:
565
+ raise WorkdirError(f"{run_dir}: not a recorded run ({exc})") from None
566
+ raw = canonical.thaw(canonical.unflatten(flat))
567
+ assert isinstance(raw, dict)
568
+ raw["start"] = {"kind": "from_run", "run": str(rundir.path), "generation": generation}
569
+ raw["replicates"] = {**raw.get("replicates", {}), "count": count, "root_seed": root_seed}
570
+ raw["protocol"] = {"name": f"{rundir.name}@{generation}"}
571
+ return raw
@@ -0,0 +1 @@
1
+ """Interface compatibility: the test between a system's specs and a method's requirements."""