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.
Files changed (29) hide show
  1. lsst/pipe/base/_status.py +156 -11
  2. lsst/pipe/base/log_capture.py +98 -7
  3. lsst/pipe/base/pipeline_graph/expressions.py +3 -3
  4. lsst/pipe/base/quantum_graph/_common.py +21 -1
  5. lsst/pipe/base/quantum_graph/_multiblock.py +14 -39
  6. lsst/pipe/base/quantum_graph/_predicted.py +90 -90
  7. lsst/pipe/base/quantum_graph/_provenance.py +345 -200
  8. lsst/pipe/base/quantum_graph/aggregator/_communicators.py +19 -19
  9. lsst/pipe/base/quantum_graph/aggregator/_progress.py +77 -84
  10. lsst/pipe/base/quantum_graph/aggregator/_scanner.py +201 -72
  11. lsst/pipe/base/quantum_graph/aggregator/_structs.py +45 -35
  12. lsst/pipe/base/quantum_graph/aggregator/_supervisor.py +15 -17
  13. lsst/pipe/base/quantum_graph/aggregator/_writer.py +57 -149
  14. lsst/pipe/base/quantum_graph_builder.py +0 -1
  15. lsst/pipe/base/quantum_provenance_graph.py +2 -44
  16. lsst/pipe/base/single_quantum_executor.py +43 -9
  17. lsst/pipe/base/tests/mocks/_data_id_match.py +1 -1
  18. lsst/pipe/base/tests/mocks/_pipeline_task.py +1 -1
  19. lsst/pipe/base/version.py +1 -1
  20. {lsst_pipe_base-29.2025.4500.dist-info → lsst_pipe_base-29.2025.4700.dist-info}/METADATA +1 -1
  21. {lsst_pipe_base-29.2025.4500.dist-info → lsst_pipe_base-29.2025.4700.dist-info}/RECORD +29 -29
  22. {lsst_pipe_base-29.2025.4500.dist-info → lsst_pipe_base-29.2025.4700.dist-info}/WHEEL +0 -0
  23. {lsst_pipe_base-29.2025.4500.dist-info → lsst_pipe_base-29.2025.4700.dist-info}/entry_points.txt +0 -0
  24. {lsst_pipe_base-29.2025.4500.dist-info → lsst_pipe_base-29.2025.4700.dist-info}/licenses/COPYRIGHT +0 -0
  25. {lsst_pipe_base-29.2025.4500.dist-info → lsst_pipe_base-29.2025.4700.dist-info}/licenses/LICENSE +0 -0
  26. {lsst_pipe_base-29.2025.4500.dist-info → lsst_pipe_base-29.2025.4700.dist-info}/licenses/bsd_license.txt +0 -0
  27. {lsst_pipe_base-29.2025.4500.dist-info → lsst_pipe_base-29.2025.4700.dist-info}/licenses/gpl-v3.0.txt +0 -0
  28. {lsst_pipe_base-29.2025.4500.dist-info → lsst_pipe_base-29.2025.4700.dist-info}/top_level.txt +0 -0
  29. {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 Progress, make_worker_log
63
- from ._structs import IngestRequest, ScanReport, ScanResult
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 = Progress(log, config)
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[ScanResult | Literal[_Sentinel.NO_MORE_WRITE_REQUESTS]] | None = (
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.finish_ingests()
409
+ self.progress.quantum_ingests.close()
410
410
  case _Sentinel.SCANNER_DONE:
411
411
  self._n_scanners_done += 1
412
- self.progress.finish_scans()
412
+ self.progress.scans.close()
413
413
  case _Sentinel.WRITER_DONE:
414
414
  self._writer_done = True
415
- self.progress.finish_writes()
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, scan_result: ScanResult) -> None:
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
- scan_result : `ScanResult`
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(scan_result, block=False)
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.report_ingests(n_producers)
533
+ self.progress.quantum_ingests.update(n_producers)
534
534
  case _Sentinel.WRITE_REPORT:
535
- self.progress.report_write()
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, scan_result: ScanResult) -> None:
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
- scan_result : `ScanResult`
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(scan_result, block=False)
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[ScanResult]:
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 : `ScanResult`
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__ = ("Progress", "make_worker_log")
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 Progress:
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._n_scanned: int = 0
70
- self._n_ingested: int = 0
71
- self._n_written: int = 0
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
- self._periodic_log.log(
124
- "%s quanta scanned, %s quantum outputs ingested, "
125
- "%s provenance quanta written (of %s) after %0.1fs.",
126
- self._n_scanned,
127
- self._n_ingested,
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: