lsst-pipe-base 29.2025.4500__py3-none-any.whl → 29.2025.4700__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.
- lsst/pipe/base/_status.py +156 -11
- lsst/pipe/base/log_capture.py +98 -7
- lsst/pipe/base/pipeline_graph/expressions.py +3 -3
- lsst/pipe/base/quantum_graph/_common.py +21 -1
- lsst/pipe/base/quantum_graph/_multiblock.py +14 -39
- lsst/pipe/base/quantum_graph/_predicted.py +90 -90
- lsst/pipe/base/quantum_graph/_provenance.py +345 -200
- lsst/pipe/base/quantum_graph/aggregator/_communicators.py +19 -19
- lsst/pipe/base/quantum_graph/aggregator/_progress.py +77 -84
- lsst/pipe/base/quantum_graph/aggregator/_scanner.py +201 -72
- lsst/pipe/base/quantum_graph/aggregator/_structs.py +45 -35
- lsst/pipe/base/quantum_graph/aggregator/_supervisor.py +15 -17
- lsst/pipe/base/quantum_graph/aggregator/_writer.py +57 -149
- lsst/pipe/base/quantum_graph_builder.py +0 -1
- lsst/pipe/base/quantum_provenance_graph.py +2 -44
- lsst/pipe/base/single_quantum_executor.py +43 -9
- lsst/pipe/base/tests/mocks/_data_id_match.py +1 -1
- lsst/pipe/base/tests/mocks/_pipeline_task.py +1 -1
- lsst/pipe/base/version.py +1 -1
- {lsst_pipe_base-29.2025.4500.dist-info → lsst_pipe_base-29.2025.4700.dist-info}/METADATA +1 -1
- {lsst_pipe_base-29.2025.4500.dist-info → lsst_pipe_base-29.2025.4700.dist-info}/RECORD +29 -29
- {lsst_pipe_base-29.2025.4500.dist-info → lsst_pipe_base-29.2025.4700.dist-info}/WHEEL +0 -0
- {lsst_pipe_base-29.2025.4500.dist-info → lsst_pipe_base-29.2025.4700.dist-info}/entry_points.txt +0 -0
- {lsst_pipe_base-29.2025.4500.dist-info → lsst_pipe_base-29.2025.4700.dist-info}/licenses/COPYRIGHT +0 -0
- {lsst_pipe_base-29.2025.4500.dist-info → lsst_pipe_base-29.2025.4700.dist-info}/licenses/LICENSE +0 -0
- {lsst_pipe_base-29.2025.4500.dist-info → lsst_pipe_base-29.2025.4700.dist-info}/licenses/bsd_license.txt +0 -0
- {lsst_pipe_base-29.2025.4500.dist-info → lsst_pipe_base-29.2025.4700.dist-info}/licenses/gpl-v3.0.txt +0 -0
- {lsst_pipe_base-29.2025.4500.dist-info → lsst_pipe_base-29.2025.4700.dist-info}/top_level.txt +0 -0
- {lsst_pipe_base-29.2025.4500.dist-info → lsst_pipe_base-29.2025.4700.dist-info}/zip-safe +0 -0
|
@@ -59,8 +59,8 @@ from typing import Any, Literal, Self, TypeAlias, TypeVar, Union
|
|
|
59
59
|
from lsst.utils.logging import VERBOSE, LsstLogAdapter
|
|
60
60
|
|
|
61
61
|
from ._config import AggregatorConfig
|
|
62
|
-
from ._progress import
|
|
63
|
-
from ._structs import IngestRequest, ScanReport,
|
|
62
|
+
from ._progress import ProgressManager, make_worker_log
|
|
63
|
+
from ._structs import IngestRequest, ScanReport, WriteRequest
|
|
64
64
|
|
|
65
65
|
_T = TypeVar("_T")
|
|
66
66
|
|
|
@@ -340,7 +340,7 @@ class SupervisorCommunicator:
|
|
|
340
340
|
config: AggregatorConfig,
|
|
341
341
|
) -> None:
|
|
342
342
|
self.config = config
|
|
343
|
-
self.progress =
|
|
343
|
+
self.progress = ProgressManager(log, config)
|
|
344
344
|
self.n_scanners = n_scanners
|
|
345
345
|
# The supervisor sends scan requests to scanners on this queue.
|
|
346
346
|
# When complete, the supervisor sends n_scanners sentinals and each
|
|
@@ -361,7 +361,7 @@ class SupervisorCommunicator:
|
|
|
361
361
|
# scanner and the supervisor send one sentinal when done, and the
|
|
362
362
|
# writer waits for (n_scanners + 1) sentinals to arrive before it
|
|
363
363
|
# starts its shutdown.
|
|
364
|
-
self._write_requests: Queue[
|
|
364
|
+
self._write_requests: Queue[WriteRequest | Literal[_Sentinel.NO_MORE_WRITE_REQUESTS]] | None = (
|
|
365
365
|
context.make_queue() if config.output_path is not None else None
|
|
366
366
|
)
|
|
367
367
|
# All other workers use this queue to send many different kinds of
|
|
@@ -406,13 +406,13 @@ class SupervisorCommunicator:
|
|
|
406
406
|
pass
|
|
407
407
|
case _Sentinel.INGESTER_DONE:
|
|
408
408
|
self._ingester_done = True
|
|
409
|
-
self.progress.
|
|
409
|
+
self.progress.quantum_ingests.close()
|
|
410
410
|
case _Sentinel.SCANNER_DONE:
|
|
411
411
|
self._n_scanners_done += 1
|
|
412
|
-
self.progress.
|
|
412
|
+
self.progress.scans.close()
|
|
413
413
|
case _Sentinel.WRITER_DONE:
|
|
414
414
|
self._writer_done = True
|
|
415
|
-
self.progress.
|
|
415
|
+
self.progress.writes.close()
|
|
416
416
|
case unexpected:
|
|
417
417
|
raise AssertionError(f"Unexpected message {unexpected!r} to supervisor.")
|
|
418
418
|
self.log.verbose(
|
|
@@ -461,17 +461,17 @@ class SupervisorCommunicator:
|
|
|
461
461
|
"""
|
|
462
462
|
self._scan_requests.put(_ScanRequest(quantum_id), block=False)
|
|
463
463
|
|
|
464
|
-
def request_write(self,
|
|
464
|
+
def request_write(self, request: WriteRequest) -> None:
|
|
465
465
|
"""Send a request to the writer to write provenance for the given scan.
|
|
466
466
|
|
|
467
467
|
Parameters
|
|
468
468
|
----------
|
|
469
|
-
|
|
469
|
+
request : `WriteRequest`
|
|
470
470
|
Information from scanning a quantum (or knowing you don't have to,
|
|
471
471
|
in the case of blocked quanta).
|
|
472
472
|
"""
|
|
473
473
|
assert self._write_requests is not None, "Writer should not be used if writing is disabled."
|
|
474
|
-
self._write_requests.put(
|
|
474
|
+
self._write_requests.put(request, block=False)
|
|
475
475
|
|
|
476
476
|
def poll(self) -> Iterator[ScanReport]:
|
|
477
477
|
"""Poll for reports from workers while sending scan requests.
|
|
@@ -530,9 +530,9 @@ class SupervisorCommunicator:
|
|
|
530
530
|
if not already_failing:
|
|
531
531
|
raise FatalWorkerError()
|
|
532
532
|
case _IngestReport(n_producers=n_producers):
|
|
533
|
-
self.progress.
|
|
533
|
+
self.progress.quantum_ingests.update(n_producers)
|
|
534
534
|
case _Sentinel.WRITE_REPORT:
|
|
535
|
-
self.progress.
|
|
535
|
+
self.progress.writes.update(1)
|
|
536
536
|
case _ProgressLog(message=message, level=level):
|
|
537
537
|
self.progress.log.log(level, "%s [after %0.1fs]", message, self.progress.elapsed_time)
|
|
538
538
|
case _:
|
|
@@ -626,10 +626,10 @@ class WorkerCommunicator:
|
|
|
626
626
|
|
|
627
627
|
Parameters
|
|
628
628
|
----------
|
|
629
|
-
message : `str`
|
|
630
|
-
Log message.
|
|
631
629
|
level : `int`
|
|
632
630
|
Log level. Should be ``VERBOSE`` or higher.
|
|
631
|
+
message : `str`
|
|
632
|
+
Log message.
|
|
633
633
|
"""
|
|
634
634
|
self._reports.put(_ProgressLog(message=message, level=level), block=False)
|
|
635
635
|
|
|
@@ -728,16 +728,16 @@ class ScannerCommunicator(WorkerCommunicator):
|
|
|
728
728
|
else:
|
|
729
729
|
self._reports.put(_IngestReport(1), block=False)
|
|
730
730
|
|
|
731
|
-
def request_write(self,
|
|
731
|
+
def request_write(self, request: WriteRequest) -> None:
|
|
732
732
|
"""Ask the writer to write provenance for a quantum.
|
|
733
733
|
|
|
734
734
|
Parameters
|
|
735
735
|
----------
|
|
736
|
-
|
|
736
|
+
request : `WriteRequest`
|
|
737
737
|
Result of scanning a quantum.
|
|
738
738
|
"""
|
|
739
739
|
assert self._write_requests is not None, "Writer should not be used if writing is disabled."
|
|
740
|
-
self._write_requests.put(
|
|
740
|
+
self._write_requests.put(request, block=False)
|
|
741
741
|
|
|
742
742
|
def get_compression_dict(self) -> bytes | None:
|
|
743
743
|
"""Attempt to get the compression dict from the writer.
|
|
@@ -913,12 +913,12 @@ class WriterCommunicator(WorkerCommunicator):
|
|
|
913
913
|
self._reports.put(_Sentinel.WRITER_DONE, block=False)
|
|
914
914
|
return result
|
|
915
915
|
|
|
916
|
-
def poll(self) -> Iterator[
|
|
916
|
+
def poll(self) -> Iterator[WriteRequest]:
|
|
917
917
|
"""Poll for writer requests from the scanner workers and supervisor.
|
|
918
918
|
|
|
919
919
|
Yields
|
|
920
920
|
------
|
|
921
|
-
request : `
|
|
921
|
+
request : `WriteRequest`
|
|
922
922
|
The result of a quantum scan.
|
|
923
923
|
|
|
924
924
|
Notes
|
|
@@ -27,20 +27,86 @@
|
|
|
27
27
|
|
|
28
28
|
from __future__ import annotations
|
|
29
29
|
|
|
30
|
-
__all__ = ("
|
|
30
|
+
__all__ = ("ProgressCounter", "ProgressManager", "make_worker_log")
|
|
31
31
|
|
|
32
32
|
import logging
|
|
33
33
|
import os
|
|
34
34
|
import time
|
|
35
35
|
from types import TracebackType
|
|
36
|
-
from typing import Self
|
|
36
|
+
from typing import Any, Self
|
|
37
37
|
|
|
38
38
|
from lsst.utils.logging import TRACE, VERBOSE, LsstLogAdapter, PeriodicLogger, getLogger
|
|
39
39
|
|
|
40
40
|
from ._config import AggregatorConfig
|
|
41
41
|
|
|
42
42
|
|
|
43
|
-
class
|
|
43
|
+
class ProgressCounter:
|
|
44
|
+
"""A progress tracker for an individual aspect of the aggregation process.
|
|
45
|
+
|
|
46
|
+
Parameters
|
|
47
|
+
----------
|
|
48
|
+
parent : `ProgressManager`
|
|
49
|
+
The parent progress manager object.
|
|
50
|
+
description : `str`
|
|
51
|
+
Human-readable description of this aspect.
|
|
52
|
+
unit : `str`
|
|
53
|
+
Unit (in plural form) for the items being counted.
|
|
54
|
+
total : `int`, optional
|
|
55
|
+
Expected total number of items. May be set later.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def __init__(self, parent: ProgressManager, description: str, unit: str, total: int | None = None):
|
|
59
|
+
self._parent = parent
|
|
60
|
+
self.total = total
|
|
61
|
+
self._description = description
|
|
62
|
+
self._current = 0
|
|
63
|
+
self._unit = unit
|
|
64
|
+
self._bar: Any = None
|
|
65
|
+
|
|
66
|
+
def update(self, n: int) -> None:
|
|
67
|
+
"""Report that ``n`` new items have been processed.
|
|
68
|
+
|
|
69
|
+
Parameters
|
|
70
|
+
----------
|
|
71
|
+
n : `int`
|
|
72
|
+
Number of new items processed.
|
|
73
|
+
"""
|
|
74
|
+
self._current += n
|
|
75
|
+
if self._parent.interactive:
|
|
76
|
+
if self._bar is None:
|
|
77
|
+
if n == self.total:
|
|
78
|
+
return
|
|
79
|
+
from tqdm import tqdm
|
|
80
|
+
|
|
81
|
+
self._bar = tqdm(desc=self._description, total=self.total, leave=False, unit=f" {self._unit}")
|
|
82
|
+
else:
|
|
83
|
+
self._bar.update(n)
|
|
84
|
+
if self._current == self.total:
|
|
85
|
+
self._bar.close()
|
|
86
|
+
self._parent._log_status()
|
|
87
|
+
|
|
88
|
+
def close(self) -> None:
|
|
89
|
+
"""Close the counter, guaranteeing that `update` will not be called
|
|
90
|
+
again.
|
|
91
|
+
"""
|
|
92
|
+
if self._bar is not None:
|
|
93
|
+
self._bar.close()
|
|
94
|
+
self._bar = None
|
|
95
|
+
|
|
96
|
+
def append_log_terms(self, msg: list[str]) -> None:
|
|
97
|
+
"""Append a log message for this counter to a list if it is active.
|
|
98
|
+
|
|
99
|
+
Parameters
|
|
100
|
+
----------
|
|
101
|
+
msg : `list` [ `str` ]
|
|
102
|
+
List of messages to concatenate into a single line and log
|
|
103
|
+
together, to be modified in-place.
|
|
104
|
+
"""
|
|
105
|
+
if self.total is not None and self._current > 0 and self._current < self.total:
|
|
106
|
+
msg.append(f"{self._description} ({self._current} of {self.total} {self._unit})")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class ProgressManager:
|
|
44
110
|
"""A helper class for the provenance aggregator that handles reporting
|
|
45
111
|
progress to the user.
|
|
46
112
|
|
|
@@ -66,10 +132,9 @@ class Progress:
|
|
|
66
132
|
self.log = log
|
|
67
133
|
self.config = config
|
|
68
134
|
self._periodic_log = PeriodicLogger(self.log, config.log_status_interval)
|
|
69
|
-
self.
|
|
70
|
-
self.
|
|
71
|
-
self.
|
|
72
|
-
self._n_quanta: int | None = None
|
|
135
|
+
self.scans = ProgressCounter(self, "scanning", "quanta")
|
|
136
|
+
self.writes = ProgressCounter(self, "writing", "quanta")
|
|
137
|
+
self.quantum_ingests = ProgressCounter(self, "ingesting outputs", "quanta")
|
|
73
138
|
self.interactive = config.interactive_status
|
|
74
139
|
|
|
75
140
|
def __enter__(self) -> Self:
|
|
@@ -90,29 +155,6 @@ class Progress:
|
|
|
90
155
|
self._logging_redirect.__exit__(exc_type, exc_value, traceback)
|
|
91
156
|
return None
|
|
92
157
|
|
|
93
|
-
def set_n_quanta(self, n_quanta: int) -> None:
|
|
94
|
-
"""Set the total number of quanta.
|
|
95
|
-
|
|
96
|
-
Parameters
|
|
97
|
-
----------
|
|
98
|
-
n_quanta : `int`
|
|
99
|
-
Total number of quanta, including special "init" quanta.
|
|
100
|
-
|
|
101
|
-
Notes
|
|
102
|
-
-----
|
|
103
|
-
This method must be called before any of the ``report_*`` methods.
|
|
104
|
-
"""
|
|
105
|
-
self._n_quanta = n_quanta
|
|
106
|
-
if self.interactive:
|
|
107
|
-
from tqdm import tqdm
|
|
108
|
-
|
|
109
|
-
self._scan_progress = tqdm(desc="Scanning", total=n_quanta, leave=False, unit="quanta")
|
|
110
|
-
self._ingest_progress = tqdm(
|
|
111
|
-
desc="Ingesting", total=n_quanta, leave=False, smoothing=0.1, unit="quanta"
|
|
112
|
-
)
|
|
113
|
-
if self.config.output_path is not None:
|
|
114
|
-
self._write_progress = tqdm(desc="Writing", total=n_quanta, leave=False, unit="quanta")
|
|
115
|
-
|
|
116
158
|
@property
|
|
117
159
|
def elapsed_time(self) -> float:
|
|
118
160
|
"""The time in seconds since the start of the aggregator."""
|
|
@@ -120,60 +162,11 @@ class Progress:
|
|
|
120
162
|
|
|
121
163
|
def _log_status(self) -> None:
|
|
122
164
|
"""Invoke the periodic logger with the current status."""
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
self._n_written,
|
|
129
|
-
self._n_quanta,
|
|
130
|
-
self.elapsed_time,
|
|
131
|
-
)
|
|
132
|
-
|
|
133
|
-
def report_scan(self) -> None:
|
|
134
|
-
"""Report that a quantum was scanned."""
|
|
135
|
-
self._n_scanned += 1
|
|
136
|
-
if self.interactive:
|
|
137
|
-
self._scan_progress.update(1)
|
|
138
|
-
else:
|
|
139
|
-
self._log_status()
|
|
140
|
-
|
|
141
|
-
def finish_scans(self) -> None:
|
|
142
|
-
"""Report that all scanning is done."""
|
|
143
|
-
if self.interactive:
|
|
144
|
-
self._scan_progress.close()
|
|
145
|
-
|
|
146
|
-
def report_ingests(self, n_quanta: int) -> None:
|
|
147
|
-
"""Report that ingests for multiple quanta were completed.
|
|
148
|
-
|
|
149
|
-
Parameters
|
|
150
|
-
----------
|
|
151
|
-
n_quanta : `int`
|
|
152
|
-
Number of quanta whose outputs were ingested.
|
|
153
|
-
"""
|
|
154
|
-
self._n_ingested += n_quanta
|
|
155
|
-
if self.interactive:
|
|
156
|
-
self._ingest_progress.update(n_quanta)
|
|
157
|
-
else:
|
|
158
|
-
self._log_status()
|
|
159
|
-
|
|
160
|
-
def finish_ingests(self) -> None:
|
|
161
|
-
"""Report that all ingests are done."""
|
|
162
|
-
if self.interactive:
|
|
163
|
-
self._ingest_progress.close()
|
|
164
|
-
|
|
165
|
-
def report_write(self) -> None:
|
|
166
|
-
"""Report that a quantum's provenance was written."""
|
|
167
|
-
self._n_written += 1
|
|
168
|
-
if self.interactive:
|
|
169
|
-
self._write_progress.update()
|
|
170
|
-
else:
|
|
171
|
-
self._log_status()
|
|
172
|
-
|
|
173
|
-
def finish_writes(self) -> None:
|
|
174
|
-
"""Report that all writes are done."""
|
|
175
|
-
if self.interactive:
|
|
176
|
-
self._write_progress.close()
|
|
165
|
+
log_terms: list[str] = []
|
|
166
|
+
self.scans.append_log_terms(log_terms)
|
|
167
|
+
self.writes.append_log_terms(log_terms)
|
|
168
|
+
self.quantum_ingests.append_log_terms(log_terms)
|
|
169
|
+
self._periodic_log.log("Status after %0.1fs: %s.", self.elapsed_time, "; ".join(log_terms))
|
|
177
170
|
|
|
178
171
|
|
|
179
172
|
def make_worker_log(name: str, config: AggregatorConfig) -> LsstLogAdapter:
|