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
@@ -0,0 +1,451 @@
1
+ """The observer of a run: loggers, emitters, worker channels and the dispatch to sinks.
2
+
3
+ Threading rule (design §3.6): sinks and ``on_emit`` are called only from the run's driver
4
+ thread. Process and thread workers log and emit through a :class:`WorkerChannel`; the driver
5
+ moves their records to the sinks with :meth:`Observer.drain`. An emitter may hold its
6
+ emissions instead: the executor hands them over with :meth:`Observer.receive` once the
7
+ results of the work that made them have returned.
8
+ """
9
+
10
+ import multiprocessing
11
+ import queue
12
+ import threading
13
+ import time
14
+ from collections.abc import Callable, Mapping, Sequence
15
+ from dataclasses import dataclass, replace
16
+ from typing import Any, Literal
17
+
18
+ from MATE_contract import EventSink, Logger
19
+
20
+ from MATE.observation.records import LEVELS, LOGGER_LEVELS, Emission, EventRecord, LogRecord
21
+ from MATE.observation.sinks import Sink
22
+
23
+ __all__ = ["Observer", "WorkerChannel"]
24
+
25
+
26
+ _DEBUG, _INFO, _WARNING, _ERROR = (LOGGER_LEVELS[name]
27
+ for name in ("debug", "info", "warning", "error"))
28
+
29
+
30
+ def _threshold(verbosity: str) -> int:
31
+ """Return the threshold of a verbosity name."""
32
+ try:
33
+ return LEVELS[verbosity]
34
+ except KeyError:
35
+ raise ValueError(
36
+ f"verbosity {verbosity!r}: unknown; expected one of {', '.join(LEVELS)}") from None
37
+
38
+
39
+ def _format(msg: str, args: tuple[object, ...]) -> str:
40
+ """Return ``msg % args``, or the raw pieces when they do not match."""
41
+ if not args:
42
+ return msg
43
+ try:
44
+ return msg % args
45
+ except (TypeError, ValueError, KeyError):
46
+ return f"{msg} {args!r}"
47
+
48
+
49
+ class _Logger:
50
+ """A logger whose level test precedes any formatting ([K] LG1)."""
51
+
52
+ __slots__ = ("_path", "_level_of", "_write")
53
+
54
+ def __init__(self, path: str, level_of: Callable[[], int],
55
+ write: Callable[[int, str, str], None]) -> None:
56
+ self._path = path
57
+ self._level_of = level_of
58
+ self._write = write
59
+
60
+ def _log(self, level: int, msg: str, args: tuple[object, ...]) -> None:
61
+ """Format and write one message if its level passes the threshold."""
62
+ if level < self._level_of():
63
+ return
64
+ self._write(level, self._path, _format(msg, args))
65
+
66
+ def debug(self, msg: str, /, *args: object) -> None:
67
+ """Log a debugging message (level 10)."""
68
+ self._log(_DEBUG, msg, args)
69
+
70
+ def info(self, msg: str, /, *args: object) -> None:
71
+ """Log an informative message (level 30)."""
72
+ self._log(_INFO, msg, args)
73
+
74
+ def warning(self, msg: str, /, *args: object) -> None:
75
+ """Log a warning (level 35)."""
76
+ self._log(_WARNING, msg, args)
77
+
78
+ def error(self, msg: str, /, *args: object) -> None:
79
+ """Log an error (level 40)."""
80
+ self._log(_ERROR, msg, args)
81
+
82
+
83
+ class _Emitter:
84
+ """An event sink bound to one component; builds an :class:`Emission` per call."""
85
+
86
+ __slots__ = ("_bound", "_clock", "_send")
87
+
88
+ def __init__(self, bound: Mapping[str, Any], clock: Callable[[], tuple[int | None, object]],
89
+ send: Callable[[Emission], None]) -> None:
90
+ self._bound = bound
91
+ self._clock = clock
92
+ self._send = send
93
+
94
+ def __call__(self, name: str, /, **fields: object) -> None:
95
+ """Emit the event ``name`` with its fields."""
96
+ b = self._bound
97
+ generation, date = b["run_generation"], b["date"]
98
+ if generation is None or date is None:
99
+ now_generation, now_date = self._clock()
100
+ generation = now_generation if generation is None else generation
101
+ date = now_date if date is None else date
102
+ self._send(Emission(origin=b["origin"], module=b["module"], subpop=b["subpop"],
103
+ path=b["path"], name=name, fields=fields, run_generation=generation,
104
+ date=date, request_ranks=b["request_ranks"], wall=time.time()))
105
+
106
+
107
+ def _no_clock() -> tuple[None, None]:
108
+ """The clock of a worker: generation and date are given at emitter creation."""
109
+ return None, None
110
+
111
+
112
+ @dataclass(frozen=True)
113
+ class WorkerChannel:
114
+ """The handle a worker logs and emits through ([X] SK2).
115
+
116
+ A channel of kind ``"process"`` holds a multiprocessing queue and event: it is handed to a
117
+ worker process at its creation (process or pool initializer arguments). A channel of kind
118
+ ``"thread"`` holds a ``queue.SimpleQueue`` and a ``threading.Event``.
119
+
120
+ Parameters
121
+ ----------
122
+ kind : {"process", "thread"}
123
+ records : queue
124
+ Where the worker puts its records, drained by :meth:`Observer.drain`.
125
+ stop : event
126
+ The stop flag ([K] CT7, [X] GU5).
127
+ """
128
+
129
+ kind: Literal["process", "thread"]
130
+ records: Any
131
+ stop: Any
132
+
133
+ def logger(self, path: str, verbosity: str) -> Logger:
134
+ """Return a logger for a component of the worker.
135
+
136
+ Parameters
137
+ ----------
138
+ path : str
139
+ Log path of the component.
140
+ verbosity : str
141
+ Threshold applied in the worker, the run's initial verbosity (DD-13).
142
+
143
+ Returns
144
+ -------
145
+ Logger
146
+ Messages below the threshold are neither formatted nor sent.
147
+ """
148
+ threshold = _threshold(verbosity)
149
+ put = self.records.put
150
+ return _Logger(path, lambda: threshold,
151
+ lambda level, p, text: put(("m", level, p, text, time.time())))
152
+
153
+ def emitter(self, *, origin: str, module: str, subpop: str | None, path: str,
154
+ request_ranks: tuple[int, ...] | None, run_generation: int | None,
155
+ date: object, hold: list[Emission] | None = None) -> EventSink:
156
+ """Return an event sink for a component of the worker.
157
+
158
+ Parameters
159
+ ----------
160
+ origin : {"system", "method"}
161
+ module : str
162
+ subpop : str or None
163
+ path : str
164
+ request_ranks : tuple of int or None
165
+ Candidate index -> request rank, for an evaluate batch.
166
+ run_generation : int or None
167
+ date : object
168
+ hold : list of Emission, optional
169
+ Where the emissions are kept instead of being sent; the executor returns them
170
+ with the results of the work that made them.
171
+
172
+ Returns
173
+ -------
174
+ EventSink
175
+ Each call sends one :class:`Emission` to the driver, or appends it to ``hold``.
176
+ """
177
+ bound = {"origin": origin, "module": module, "subpop": subpop, "path": path,
178
+ "request_ranks": request_ranks, "run_generation": run_generation, "date": date}
179
+ if hold is not None:
180
+ return _Emitter(bound, _no_clock, hold.append)
181
+ put = self.records.put
182
+ return _Emitter(bound, _no_clock, lambda emission: put(("e", emission)))
183
+
184
+ def stopping(self) -> bool:
185
+ """Whether the run is stopping ([K] CT7, [X] GU5).
186
+
187
+ Returns
188
+ -------
189
+ bool
190
+ """
191
+ return self.stop.is_set()
192
+
193
+
194
+ class Observer:
195
+ """The observation hub of one run ([X] §17).
196
+
197
+ Parameters
198
+ ----------
199
+ run : str
200
+ The run directory name, written in every record.
201
+ sinks : sequence of Sink
202
+ Where records go, in order.
203
+ verbosity : {"debug", "verbose", "normal", "quiet"}
204
+ The message threshold ([X] OB3).
205
+ clock : callable, optional
206
+ Returns the current ``(run_generation, date)``, used for emissions and events that
207
+ do not carry them.
208
+
209
+ Attributes
210
+ ----------
211
+ on_emit : callable or None
212
+ Receives every module emission; set by the run to the readout. When ``None``,
213
+ emissions go to the event stream as they are.
214
+
215
+ Notes
216
+ -----
217
+ Implements [X] OB2, SK1, SK2 and [K] LG1. Records are numbered by one counter shared by
218
+ messages and events. Sinks and ``on_emit`` are called from the driver thread only.
219
+ """
220
+
221
+ def __init__(self, run: str, sinks: Sequence[Sink], verbosity: str, *,
222
+ clock: Callable[[], tuple[int | None, object]] = lambda: (None, None)) -> None:
223
+ self.run = run
224
+ self._sinks = list(sinks)
225
+ self._threshold = _threshold(verbosity)
226
+ self._clock = clock
227
+ self._seq = 0
228
+ self._channels: dict[str, WorkerChannel] = {}
229
+ self._stopping = False
230
+ self._closed = False
231
+ self.on_emit: Callable[[Emission], None] | None = None
232
+
233
+ def _next(self) -> int:
234
+ """Return the next record rank."""
235
+ self._seq += 1
236
+ return self._seq
237
+
238
+ def _write(self, level: int, path: str, text: str, wall: float | None = None) -> None:
239
+ """Send one formatted message to the sinks."""
240
+ record = LogRecord(self.run, self._next(), time.time() if wall is None else wall,
241
+ level, path, text)
242
+ for sink in self._sinks:
243
+ sink.message(record)
244
+
245
+ def receive(self, emission: Emission) -> None:
246
+ """Hand a module emission to ``on_emit``, or straight to the event stream.
247
+
248
+ Parameters
249
+ ----------
250
+ emission : Emission
251
+
252
+ Notes
253
+ -----
254
+ Called from the driver thread: by its emitters, by :meth:`drain`, and by an executor
255
+ releasing the emissions it held for work whose results have returned.
256
+ """
257
+ if self.on_emit is not None:
258
+ self.on_emit(emission)
259
+ else:
260
+ self.publish(EventRecord(self.run, 0, emission.wall, emission.path, emission.name,
261
+ emission.run_generation, emission.date,
262
+ dict(emission.fields)))
263
+
264
+ def logger(self, path: str) -> Logger:
265
+ """Return the logger of a component of the driver process.
266
+
267
+ Parameters
268
+ ----------
269
+ path : str
270
+ Log path of the component ([X] OB3).
271
+
272
+ Returns
273
+ -------
274
+ Logger
275
+ The level test comes before any formatting ([K] LG1); the threshold followed is
276
+ the observer's current one.
277
+ """
278
+ return _Logger(path, lambda: self._threshold, self._write)
279
+
280
+ def emitter(self, *, origin: str, module: str, subpop: str | None, path: str,
281
+ request_ranks: tuple[int, ...] | None = None,
282
+ run_generation: int | None = None, date: object = None,
283
+ hold: list[Emission] | None = None) -> EventSink:
284
+ """Return the event sink of a component of the driver process.
285
+
286
+ Parameters
287
+ ----------
288
+ origin : {"system", "method"}
289
+ module : str
290
+ subpop : str or None
291
+ path : str
292
+ request_ranks : tuple of int or None, default None
293
+ run_generation : int or None, default None
294
+ ``None``: read from the clock at each emission.
295
+ date : object, default None
296
+ ``None``: read from the clock at each emission.
297
+ hold : list of Emission, optional
298
+ Where the emissions are kept instead of being received at once; the executor
299
+ hands them over with :meth:`receive` when the results of their work return.
300
+
301
+ Returns
302
+ -------
303
+ EventSink
304
+ Each call builds one :class:`Emission` and hands it to ``on_emit``, or appends it
305
+ to ``hold``.
306
+ """
307
+ bound = {"origin": origin, "module": module, "subpop": subpop, "path": path,
308
+ "request_ranks": request_ranks, "run_generation": run_generation, "date": date}
309
+ return _Emitter(bound, self._clock, self.receive if hold is None else hold.append)
310
+
311
+ def message(self, level: int, path: str, text: str) -> None:
312
+ """Write one of the runtime's own messages.
313
+
314
+ Parameters
315
+ ----------
316
+ level : int
317
+ 10 to 40; 20 for the runtime's detail.
318
+ path : str
319
+ text : str
320
+ """
321
+ if level >= self._threshold:
322
+ self._write(level, path, text)
323
+
324
+ def trace(self, path: str, text: str) -> None:
325
+ """Hand a traceback to every sink at the debug level, whatever the verbosity.
326
+
327
+ Parameters
328
+ ----------
329
+ path : str
330
+ text : str
331
+ The traceback.
332
+
333
+ Notes
334
+ -----
335
+ [X] RN5, S4: a traceback goes to the debug log. Each sink applies its own threshold,
336
+ so the file sink writes it to ``debug.log`` only and the terminal skips it.
337
+ """
338
+ self._write(10, path, text)
339
+
340
+ def event(self, name: str, *, path: str = "run", **fields: object) -> None:
341
+ """Publish one of the runtime's own events (design §3.15).
342
+
343
+ Parameters
344
+ ----------
345
+ name : str
346
+ ``run.start``, ``generation.end``, ``checkpoint``, ...
347
+ path : str, default "run"
348
+ **fields : object
349
+ Plain data.
350
+ """
351
+ generation, date = self._clock()
352
+ self.publish(EventRecord(self.run, 0, time.time(), path, name, generation, date,
353
+ dict(fields)))
354
+
355
+ def publish(self, record: EventRecord) -> None:
356
+ """Send an event to the sinks.
357
+
358
+ Parameters
359
+ ----------
360
+ record : EventRecord
361
+ Its ``run`` and ``seq`` are set by the observer.
362
+ """
363
+ record = replace(record, run=self.run, seq=self._next())
364
+ for sink in self._sinks:
365
+ sink.event(record)
366
+
367
+ def set_verbosity(self, verbosity: str) -> None:
368
+ """Change the threshold of the observer and of every sink that has one ([X] GU5).
369
+
370
+ Parameters
371
+ ----------
372
+ verbosity : {"debug", "verbose", "normal", "quiet"}
373
+
374
+ Raises
375
+ ------
376
+ ValueError
377
+ For an unknown verbosity.
378
+ """
379
+ self._threshold = _threshold(verbosity)
380
+ for sink in self._sinks:
381
+ setter = getattr(sink, "set_verbosity", None)
382
+ if setter is not None:
383
+ setter(verbosity)
384
+
385
+ def channel(self, kind: Literal["process", "thread"]) -> WorkerChannel:
386
+ """Return the worker channel of a kind, created on first call.
387
+
388
+ Parameters
389
+ ----------
390
+ kind : {"process", "thread"}
391
+
392
+ Returns
393
+ -------
394
+ WorkerChannel
395
+ ``"process"``: a queue and an event of the ``forkserver`` context;
396
+ ``"thread"``: a ``queue.SimpleQueue`` and a ``threading.Event``.
397
+ """
398
+ channel = self._channels.get(kind)
399
+ if channel is None:
400
+ if kind == "process":
401
+ context = multiprocessing.get_context("forkserver")
402
+ channel = WorkerChannel("process", context.Queue(), context.Event())
403
+ elif kind == "thread":
404
+ channel = WorkerChannel("thread", queue.SimpleQueue(), threading.Event())
405
+ else:
406
+ raise ValueError(f"channel kind {kind!r}: unknown; expected 'process' or 'thread'")
407
+ if self._stopping:
408
+ channel.stop.set()
409
+ self._channels[kind] = channel
410
+ return channel
411
+
412
+ def drain(self) -> None:
413
+ """Move the records the workers have sent to the sinks and ``on_emit``.
414
+
415
+ Notes
416
+ -----
417
+ Called by the driver thread only; records of one channel keep their arrival order.
418
+ Worker messages are filtered again at the current threshold.
419
+ """
420
+ for channel in self._channels.values():
421
+ while True:
422
+ try:
423
+ item = channel.records.get_nowait()
424
+ except queue.Empty:
425
+ break
426
+ if item[0] == "m":
427
+ _, level, path, text, wall = item
428
+ if level >= self._threshold:
429
+ self._write(level, path, text, wall)
430
+ else:
431
+ self.receive(item[1])
432
+
433
+ def set_stopping(self) -> None:
434
+ """Set the stop flag of every channel, present and future ([K] CT7)."""
435
+ self._stopping = True
436
+ for channel in self._channels.values():
437
+ channel.stop.set()
438
+
439
+ def close(self) -> None:
440
+ """Drain the channels, close the sinks and release the queues."""
441
+ if self._closed:
442
+ return
443
+ self.drain()
444
+ self._closed = True
445
+ for sink in self._sinks:
446
+ sink.close()
447
+ process = self._channels.get("process")
448
+ if process is not None:
449
+ process.records.close()
450
+ process.records.join_thread()
451
+
@@ -0,0 +1,125 @@
1
+ """Records of the two observation streams: messages for humans and structured events.
2
+
3
+ Levels are integers ([X] OB3, DD-13): a verbosity names a threshold, a logger method a level.
4
+ """
5
+
6
+ from collections.abc import Mapping
7
+ from dataclasses import dataclass
8
+ from types import MappingProxyType
9
+ from typing import Literal
10
+
11
+ __all__ = ["LEVELS", "LOGGER_LEVELS", "LEVEL_NAMES", "LogRecord", "EventRecord", "Emission"]
12
+
13
+ LEVELS: Mapping[str, int] = MappingProxyType(
14
+ {"debug": 10, "verbose": 20, "normal": 30, "quiet": 40})
15
+ """Verbosity name -> threshold ([X] OB3)."""
16
+
17
+ LOGGER_LEVELS: Mapping[str, int] = MappingProxyType(
18
+ {"debug": 10, "info": 30, "warning": 35, "error": 40})
19
+ """Logger method -> level (DD-13); level 20 serves the runtime's own detailed messages."""
20
+
21
+ LEVEL_NAMES: Mapping[int, str] = MappingProxyType(
22
+ {10: "DEBUG", 20: "VERBOSE", 30: "INFO", 35: "WARNING", 40: "ERROR"})
23
+ """Level -> the label written in the journals."""
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class LogRecord:
28
+ """One message for humans ([X] §17.2).
29
+
30
+ Parameters
31
+ ----------
32
+ run : str
33
+ The run directory name.
34
+ seq : int
35
+ Rank of the record in the run's observation streams.
36
+ wall : float
37
+ Wall-clock time (seconds since the epoch) at which the message was logged.
38
+ level : int
39
+ Its level (``LOGGER_LEVELS``, or 20 for the runtime's detail).
40
+ path : str
41
+ Log path of the component, e.g. ``run_017/subpop.proies/method.DEBoG/mutation``.
42
+ message : str
43
+ The formatted message.
44
+ """
45
+
46
+ run: str
47
+ seq: int
48
+ wall: float
49
+ level: int
50
+ path: str
51
+ message: str
52
+
53
+
54
+ @dataclass(frozen=True)
55
+ class EventRecord:
56
+ """One structured event ([X] §17.2).
57
+
58
+ Parameters
59
+ ----------
60
+ run : str
61
+ The run directory name.
62
+ seq : int
63
+ Rank of the record in the run's observation streams.
64
+ wall : float
65
+ Wall-clock time of the emission.
66
+ path : str
67
+ Path of the emitting component.
68
+ name : str
69
+ Event name.
70
+ run_generation : int or None
71
+ Run generation at the emission.
72
+ date : object or None
73
+ Date in the calendar's unit.
74
+ fields : mapping of str to object
75
+ Plain data.
76
+ """
77
+
78
+ run: str
79
+ seq: int
80
+ wall: float
81
+ path: str
82
+ name: str
83
+ run_generation: int | None
84
+ date: object | None
85
+ fields: Mapping[str, object]
86
+
87
+
88
+ @dataclass(frozen=True)
89
+ class Emission:
90
+ """One ``ctx.emit`` call, before the readout classifies it (design §3.6).
91
+
92
+ Parameters
93
+ ----------
94
+ origin : {"system", "method"}
95
+ Kind of the emitting module.
96
+ module : str
97
+ Module name.
98
+ subpop : str or None
99
+ Subpopulation path, for method emissions.
100
+ path : str
101
+ Log path of the emitting component.
102
+ name : str
103
+ The name as emitted (before the ``method.`` prefix, [K] LG3).
104
+ fields : mapping of str to object
105
+ The keyword fields of the call.
106
+ run_generation : int or None
107
+ Run generation at the emission.
108
+ date : object
109
+ Date at the emission.
110
+ request_ranks : tuple of int or None
111
+ For an evaluate batch: candidate index -> request rank (trace attribution).
112
+ wall : float
113
+ Wall-clock time of the call.
114
+ """
115
+
116
+ origin: Literal["system", "method"]
117
+ module: str
118
+ subpop: str | None
119
+ path: str
120
+ name: str
121
+ fields: Mapping[str, object]
122
+ run_generation: int | None
123
+ date: object
124
+ request_ranks: tuple[int, ...] | None
125
+ wall: float