lsst-ctrl-bps 29.2025.3800__py3-none-any.whl → 29.2025.4000__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.
@@ -27,7 +27,14 @@
27
27
 
28
28
  """Classes and functions used in reporting run status."""
29
29
 
30
- __all__ = ["BaseRunReport", "DetailedRunReport", "ExitCodesReport", "SummaryRunReport", "compile_job_summary"]
30
+ __all__ = [
31
+ "BaseRunReport",
32
+ "DetailedRunReport",
33
+ "ExitCodesReport",
34
+ "SummaryRunReport",
35
+ "compile_code_summary",
36
+ "compile_job_summary",
37
+ ]
31
38
 
32
39
  import abc
33
40
  import logging
@@ -55,7 +62,7 @@ class BaseRunReport(abc.ABC):
55
62
 
56
63
  def __eq__(self, other):
57
64
  if isinstance(other, BaseRunReport):
58
- return all(self._table == other._table)
65
+ return self._table.pformat() == other._table.pformat()
59
66
  return False
60
67
 
61
68
  def __len__(self):
@@ -195,7 +202,7 @@ class DetailedRunReport(BaseRunReport):
195
202
  job_summary = run_report.job_summary
196
203
  if job_summary is None:
197
204
  id_ = run_report.global_wms_id if use_global_id else run_report.wms_id
198
- self._msg = f"WARNING: Job summary for run '{id_}' not available, report maybe incomplete."
205
+ self._msg = f"WARNING: Job summary for run '{id_}' not available, report may be incomplete."
199
206
  return
200
207
 
201
208
  if by_label_expected:
@@ -231,44 +238,60 @@ class ExitCodesReport(BaseRunReport):
231
238
  error handling from the wms service.
232
239
  """
233
240
 
234
- def add(self, run_report, use_global_id=False):
241
+ def add(self, run_report: WmsRunReport, use_global_id: bool = False) -> None:
235
242
  # Docstring inherited from the base class.
236
243
 
237
- # Use label ordering from the run summary as it should reflect
238
- # the ordering of the pipetasks in the pipeline.
244
+ exit_code_summary = run_report.exit_code_summary
245
+ if not exit_code_summary:
246
+ id_ = run_report.global_wms_id if use_global_id else run_report.wms_id
247
+ self._msg = f"WARNING: Exit code summary for run '{id_}' not available, report may be incomplete."
248
+ return
249
+
250
+ warnings = []
251
+
252
+ # If available, use label ordering from the run summary as it should
253
+ # reflect the ordering of the pipetasks in the pipeline.
239
254
  labels = []
240
255
  if run_report.run_summary:
241
256
  for part in run_report.run_summary.split(";"):
242
257
  label, _ = part.split(":")
243
258
  labels.append(label)
244
- else:
245
- id_ = run_report.global_wms_id if use_global_id else run_report.wms_id
246
- self._msg = f"WARNING: Job summary for run '{id_}' not available, report maybe incomplete."
247
- return
259
+ if not labels:
260
+ labels = sorted(exit_code_summary)
261
+ warnings.append("WARNING: Could not determine order of pipeline, instead sorted alphabetically.")
248
262
 
249
263
  # Payload (e.g. pipetask) error codes:
250
264
  # * 1: general failure,
251
265
  # * 2: command line error (e.g. unknown command and/or option).
252
266
  pyld_error_codes = {1, 2}
253
267
 
254
- exit_code_summary = run_report.exit_code_summary
268
+ missing_labels = set()
255
269
  for label in labels:
256
- exit_codes = exit_code_summary[label]
257
-
258
- pyld_errors = [code for code in exit_codes if code in pyld_error_codes]
259
- pyld_error_count = len(pyld_errors)
260
- pyld_error_summary = (
261
- ", ".join(sorted(str(code) for code in set(pyld_errors))) if pyld_errors else "None"
262
- )
263
-
264
- infra_errors = [code for code in exit_codes if code not in pyld_error_codes]
265
- infra_error_count = len(infra_errors)
266
- infra_error_summary = (
267
- ", ".join(sorted(str(code) for code in set(infra_errors))) if infra_errors else "None"
270
+ try:
271
+ exit_codes = exit_code_summary[label]
272
+ except KeyError:
273
+ missing_labels.add(label)
274
+ else:
275
+ pyld_errors = [code for code in exit_codes if code in pyld_error_codes]
276
+ pyld_error_count = len(pyld_errors)
277
+ pyld_error_summary = (
278
+ ", ".join(sorted(str(code) for code in set(pyld_errors))) if pyld_errors else "None"
279
+ )
280
+
281
+ infra_errors = [code for code in exit_codes if code not in pyld_error_codes]
282
+ infra_error_count = len(infra_errors)
283
+ infra_error_summary = (
284
+ ", ".join(sorted(str(code) for code in set(infra_errors))) if infra_errors else "None"
285
+ )
286
+
287
+ run = [label, pyld_error_count, pyld_error_summary, infra_error_count, infra_error_summary]
288
+ self._table.add_row(run)
289
+ if missing_labels:
290
+ warnings.append(
291
+ f"WARNING: Exit code summary was not available for job labels: {', '.join(missing_labels)}"
268
292
  )
269
-
270
- run = [label, pyld_error_count, pyld_error_summary, infra_error_count, infra_error_summary]
271
- self._table.add_row(run)
293
+ if warnings:
294
+ self._msg = "\n".join(warnings)
272
295
 
273
296
  def __str__(self):
274
297
  alignments = ["<"] + [">"] * (len(self._table.colnames) - 1)
@@ -276,7 +299,7 @@ class ExitCodesReport(BaseRunReport):
276
299
  return str("\n".join(lines))
277
300
 
278
301
 
279
- def compile_job_summary(report: WmsRunReport) -> None:
302
+ def compile_job_summary(report: WmsRunReport) -> list[str]:
280
303
  """Add a job summary to the run report if necessary.
281
304
 
282
305
  If the job summary is not provided, the function will attempt to compile
@@ -289,24 +312,89 @@ def compile_job_summary(report: WmsRunReport) -> None:
289
312
  report : `lsst.ctrl.bps.WmsRunReport`
290
313
  Information about a single run.
291
314
 
292
- Raises
293
- ------
294
- ValueError
295
- Raised if the job summary *and* information about individual jobs
296
- is not available.
315
+ Returns
316
+ -------
317
+ warnings : `list` [`str`]
318
+ List of messages describing any non-critical issues encountered during
319
+ processing. Empty if none.
297
320
  """
321
+ warnings: list[str] = []
322
+
323
+ # If the job summary already exists, exit early.
298
324
  if report.job_summary:
299
- return
300
- if not report.jobs:
301
- raise ValueError("job summary cannot be compiled: information about individual jobs not available.")
302
- job_summary = {}
303
- by_label = group_jobs_by_label(report.jobs)
304
- for label, job_group in by_label.items():
305
- by_label_state = group_jobs_by_state(job_group)
306
- _LOG.debug("by_label_state = %s", by_label_state)
307
- counts = {state: len(jobs) for state, jobs in by_label_state.items()}
308
- job_summary[label] = counts
309
- report.job_summary = job_summary
325
+ return warnings
326
+
327
+ if report.jobs:
328
+ job_summary = {}
329
+ by_label = group_jobs_by_label(report.jobs)
330
+ for label, job_group in by_label.items():
331
+ by_label_state = group_jobs_by_state(job_group)
332
+ _LOG.debug("by_label_state = %s", by_label_state)
333
+ counts = {state: len(jobs) for state, jobs in by_label_state.items()}
334
+ job_summary[label] = counts
335
+ report.job_summary = job_summary
336
+ else:
337
+ warnings.append("information about individual jobs not available")
338
+
339
+ return warnings
340
+
341
+
342
+ def compile_code_summary(report: WmsRunReport) -> list[str]:
343
+ """Add missing entries to the exit code summary if necessary.
344
+
345
+ A WMS plugin may exclude job labels for which there are no failures from
346
+ the exit code summary. The function will attempt to use the job summary,
347
+ if available, to add missing entries for these labels.
348
+
349
+ Parameters
350
+ ----------
351
+ report : `lsst.ctrl.bps.WmsRunReport`
352
+ Information about a single run.
353
+
354
+ Returns
355
+ -------
356
+ warnings : `list` [`str`]
357
+ List of messages describing any non-critical issues encountered during
358
+ processing. Empty if none.
359
+ """
360
+ warnings: list[str] = []
361
+
362
+ # If the job summary is not available, exit early.
363
+ if not report.job_summary:
364
+ return warnings
365
+
366
+ # A shallow copy is enough here because we won't be modifying the existing
367
+ # entries, only adding new ones if necessary.
368
+ exit_code_summary = dict(report.exit_code_summary) if report.exit_code_summary else {}
369
+
370
+ # Use the job summary to add the entries for labels with no failures
371
+ # *without* modifying already existing entries.
372
+ failure_summary = {label: states[WmsStates.FAILED] for label, states in report.job_summary.items()}
373
+ for label, count in failure_summary.items():
374
+ if count == 0:
375
+ exit_code_summary.setdefault(label, [])
376
+
377
+ # Check if there are any discrepancies between the data in the exit code
378
+ # summary and the job summary.
379
+ code_summary_labels = set(exit_code_summary)
380
+ failure_summary_labels = set(failure_summary)
381
+ mismatches = {
382
+ label
383
+ for label in failure_summary_labels & code_summary_labels
384
+ if len(exit_code_summary[label]) != failure_summary[label]
385
+ }
386
+ if mismatches:
387
+ warnings.append(
388
+ f"number of exit codes differs from number of failures for job labels: {', '.join(mismatches)}"
389
+ )
390
+ missing = failure_summary_labels - code_summary_labels
391
+ if missing:
392
+ warnings.append(f"exit codes not available for job labels: {', '.join(missing)}")
393
+
394
+ if exit_code_summary:
395
+ report.exit_code_summary = exit_code_summary
396
+
397
+ return warnings
310
398
 
311
399
 
312
400
  def group_jobs_by_state(jobs):
@@ -146,7 +146,7 @@ def create_job_quantum_graph_filename(config, job, out_prefix=None):
146
146
  found, subdir = config.search("subDirTemplate", opt={"curvals": curvals})
147
147
  if not found:
148
148
  subdir = "{job.label}"
149
- full_filename = Path("inputs") / subdir / f"quantum_{job.name}.qgraph"
149
+ full_filename = Path("inputs") / subdir / f"quantum_{job.name}.qg"
150
150
 
151
151
  if out_prefix is not None:
152
152
  full_filename = Path(out_prefix) / full_filename
@@ -29,18 +29,21 @@
29
29
  a QuantumGraph.
30
30
  """
31
31
 
32
- __all__ = ["ClusteredQuantumGraph", "QuantaCluster"]
32
+ from __future__ import annotations
33
33
 
34
+ __all__ = ["ClusteredQuantumGraph", "QuantaCluster"]
34
35
 
35
36
  import logging
36
37
  import pickle
37
38
  import re
39
+ import uuid
38
40
  from collections import Counter, defaultdict
39
41
  from pathlib import Path
40
42
 
41
43
  from networkx import DiGraph, is_directed_acyclic_graph, is_isomorphic, topological_sort
42
44
 
43
- from lsst.pipe.base import NodeId, QuantumGraph
45
+ from lsst.pipe.base.pipeline_graph import TaskImportMode
46
+ from lsst.pipe.base.quantum_graph import PredictedQuantumGraph, QuantumInfo
44
47
  from lsst.utils.iteration import ensure_iterable
45
48
 
46
49
  from .bps_draw import draw_networkx_dot
@@ -79,13 +82,17 @@ class QuantaCluster:
79
82
  self.tags = {}
80
83
 
81
84
  @classmethod
82
- def from_quantum_node(cls, quantum_node, template):
83
- """Create single quantum cluster from given quantum node.
85
+ def from_quantum_info(
86
+ cls, quantum_id: uuid.UUID, quantum_info: QuantumInfo, template: str
87
+ ) -> QuantaCluster:
88
+ """Create single quantum cluster from the given quantum information.
84
89
 
85
90
  Parameters
86
91
  ----------
87
- quantum_node : `lsst.pipe.base.QuantumNode`
88
- QuantumNode for which to make into a single quantum cluster.
92
+ quantum_id : `uuid.UUID`
93
+ ID of the quantum.
94
+ quantum_info : `lsst.pipe.base.quantum_graph.QuantumInfo`
95
+ Dictionary of additional information about the quantum.
89
96
  template : `str`
90
97
  Template for creating cluster name.
91
98
 
@@ -94,14 +101,13 @@ class QuantaCluster:
94
101
  cluster : `QuantaCluster`
95
102
  Newly created cluster containing the given quantum.
96
103
  """
97
- label = quantum_node.taskDef.label
98
- node_id = quantum_node.nodeId
99
- data_id = quantum_node.quantum.dataId
104
+ label = quantum_info["task_label"]
105
+ data_id = quantum_info["data_id"]
100
106
 
101
107
  # Gather info for name template into a dictionary.
102
108
  info = dict(data_id.required)
103
109
  info["label"] = label
104
- info["node_number"] = node_id
110
+ info["node_number"] = quantum_id
105
111
  _LOG.debug("template = %s", template)
106
112
  _LOG.debug("info for template = %s", info)
107
113
 
@@ -116,7 +122,7 @@ class QuantaCluster:
116
122
  _LOG.debug("template name = %s", name)
117
123
 
118
124
  cluster = QuantaCluster(name, label, info)
119
- cluster.add_quantum(quantum_node.nodeId, label)
125
+ cluster.add_quantum(quantum_id, label)
120
126
  return cluster
121
127
 
122
128
  @property
@@ -130,24 +136,12 @@ class QuantaCluster:
130
136
  """Counts of Quanta per taskDef.label in this cluster."""
131
137
  return Counter(self._task_label_counts)
132
138
 
133
- def add_quantum_node(self, quantum_node):
134
- """Add a quantumNode to this cluster.
135
-
136
- Parameters
137
- ----------
138
- quantum_node : `lsst.pipe.base.QuantumNode`
139
- Quantum node to add.
140
- """
141
- _LOG.debug("quantum_node = %s", quantum_node)
142
- _LOG.debug("quantum_node.nodeId = %s", quantum_node.nodeId)
143
- self.add_quantum(quantum_node.nodeId, quantum_node.taskDef.label)
144
-
145
139
  def add_quantum(self, node_id, task_label):
146
140
  """Add a quantumNode to this cluster.
147
141
 
148
142
  Parameters
149
143
  ----------
150
- node_id : `lsst.pipe.base.NodeId`
144
+ node_id : `uuid.UUID`
151
145
  ID for quantumNode to be added to cluster.
152
146
  task_label : `str`
153
147
  Task label for quantumNode to be added to cluster.
@@ -185,11 +179,10 @@ class ClusteredQuantumGraph:
185
179
  ----------
186
180
  name : `str`
187
181
  Name to be given to the ClusteredQuantumGraph.
188
- qgraph : `lsst.pipe.base.QuantumGraph`
189
- The QuantumGraph to be clustered.
182
+ qgraph : `lsst.pipe.base.quantum_graph.PredictedQuantumGraph`
183
+ The quantum graph to be clustered.
190
184
  qgraph_filename : `str`
191
- Filename for given QuantumGraph if it has already been
192
- serialized.
185
+ Filename for given quantum graph.
193
186
 
194
187
  Raises
195
188
  ------
@@ -203,11 +196,12 @@ class ClusteredQuantumGraph:
203
196
  use API over totally minimized memory usage.
204
197
  """
205
198
 
206
- def __init__(self, name, qgraph, qgraph_filename=None):
199
+ def __init__(self, name: str, qgraph: PredictedQuantumGraph, qgraph_filename: str):
207
200
  if "/" in name:
208
201
  raise ValueError(f"name cannot have a / ({name})")
209
202
  self._name = name
210
203
  self._quantum_graph = qgraph
204
+ self._quantum_only_xgraph = qgraph.quantum_only_xgraph
211
205
  self._quantum_graph_filename = Path(qgraph_filename).resolve()
212
206
  self._cluster_graph = DiGraph()
213
207
 
@@ -228,22 +222,27 @@ class ClusteredQuantumGraph:
228
222
  return False
229
223
  if len(self) != len(other):
230
224
  return False
231
- return self._quantum_graph == other._quantum_graph and is_isomorphic(
225
+ return is_isomorphic(self.qxgraph, other.qxgraph) and is_isomorphic(
232
226
  self._cluster_graph, other._cluster_graph
233
227
  )
234
228
 
235
229
  @property
236
- def name(self):
230
+ def name(self) -> str:
237
231
  """The name of the ClusteredQuantumGraph."""
238
232
  return self._name
239
233
 
240
234
  @property
241
- def qgraph(self):
242
- """The QuantumGraph associated with this Clustered
235
+ def qgraph(self) -> PredictedQuantumGraph:
236
+ """The quantum graph associated with this Clustered
243
237
  QuantumGraph.
244
238
  """
245
239
  return self._quantum_graph
246
240
 
241
+ @property
242
+ def qxgraph(self) -> DiGraph:
243
+ """A networkx graph of all quanta."""
244
+ return self._quantum_only_xgraph
245
+
247
246
  def add_cluster(self, clusters_for_adding):
248
247
  """Add a cluster of quanta as a node in the graph.
249
248
 
@@ -286,30 +285,26 @@ class ClusteredQuantumGraph:
286
285
  raise KeyError(f"{self.name} does not have a cluster named {name}") from ex
287
286
  return attr["cluster"]
288
287
 
289
- def get_quantum_node(self, id_):
290
- """Retrieve a QuantumNode from the ClusteredQuantumGraph by ID.
288
+ def get_quantum_info(self, id_: uuid.UUID) -> QuantumInfo:
289
+ """Retrieve a quantum info dict from the ClusteredQuantumGraph by ID.
291
290
 
292
291
  Parameters
293
292
  ----------
294
- id_ : `lsst.pipe.base.NodeId` or int
295
- ID of the QuantumNode to retrieve.
293
+ id_ : `uuid.UUID`
294
+ ID of the quantum to retrieve.
296
295
 
297
296
  Returns
298
297
  -------
299
- quantum_node : `lsst.pipe.base.QuantumNode`
300
- QuantumNode matching given ID.
298
+ quantum_info : `lsst.pipe.base.quantum_graph.QuantumInfo`
299
+ Quantum info dictionary for the given ID.
301
300
 
302
301
  Raises
303
302
  ------
304
303
  KeyError
305
304
  Raised if the ClusteredQuantumGraph does not contain
306
- a QuantumNode with given ID.
305
+ a quantum with given ID.
307
306
  """
308
- node_id = id_
309
- if isinstance(id_, int):
310
- node_id = NodeId(id, self._quantum_graph.graphID)
311
- _LOG.debug("get_quantum_node: node_id = %s", node_id)
312
- return self._quantum_graph.getQuantumNodeByNodeId(node_id)
307
+ return self._quantum_only_xgraph.nodes[id_]
313
308
 
314
309
  def __iter__(self):
315
310
  """Iterate over names of clusters.
@@ -414,8 +409,8 @@ class ClusteredQuantumGraph:
414
409
 
415
410
  def save(self, filename, format_=None):
416
411
  """Save the ClusteredQuantumGraph in a format that is loadable.
417
- The QuantumGraph is saved separately if hasn't already been
418
- serialized.
412
+
413
+ The quantum graph is assumed to have been saved separately.
419
414
 
420
415
  Parameters
421
416
  ----------
@@ -433,14 +428,6 @@ class ClusteredQuantumGraph:
433
428
  if format_ not in {"pickle"}:
434
429
  raise RuntimeError(f"Unknown format ({format_})")
435
430
 
436
- if not self._quantum_graph_filename:
437
- # Create filename based on given ClusteredQuantumGraph filename
438
- self._quantum_graph_filename = path.with_suffix(".qgraph")
439
-
440
- # If QuantumGraph file doesn't already exist, save it:
441
- if not Path(self._quantum_graph_filename).exists():
442
- self._quantum_graph.saveUri(self._quantum_graph_filename)
443
-
444
431
  if format_ == "pickle":
445
432
  # Don't save QuantumGraph in same file.
446
433
  tmp_qgraph = self._quantum_graph
@@ -503,14 +490,14 @@ class ClusteredQuantumGraph:
503
490
  cgraph = None
504
491
  if format_ == "pickle":
505
492
  with open(filename, "rb") as fh:
506
- cgraph = pickle.load(fh)
493
+ cgraph: ClusteredQuantumGraph = pickle.load(fh)
507
494
 
508
495
  # The QuantumGraph was saved separately
509
- try:
510
- cgraph._quantum_graph = QuantumGraph.loadUri(cgraph._quantum_graph_filename)
511
- except FileNotFoundError: # Try same path as ClusteredQuantumGraph
512
- new_filename = path.parent / Path(cgraph._quantum_graph_filename).name
513
- cgraph._quantum_graph = QuantumGraph.loadUri(new_filename)
496
+ with PredictedQuantumGraph.open(
497
+ cgraph._quantum_graph_filename, import_mode=TaskImportMode.DO_NOT_IMPORT
498
+ ) as reader:
499
+ reader.read_thin_graph()
500
+ cgraph._quantum_graph = reader.finish()
514
501
 
515
502
  return cgraph
516
503
 
lsst/ctrl/bps/drivers.py CHANGED
@@ -50,7 +50,7 @@ import logging
50
50
  import os
51
51
  from pathlib import Path
52
52
 
53
- from lsst.pipe.base import QuantumGraph
53
+ from lsst.pipe.base.quantum_graph import PredictedQuantumGraph
54
54
  from lsst.utils.timer import time_this
55
55
  from lsst.utils.usage import get_peak_mem_usage
56
56
 
@@ -111,7 +111,7 @@ def _init_submission_driver(config_file: str, **kwargs) -> BpsConfig:
111
111
  return config
112
112
 
113
113
 
114
- def acquire_qgraph_driver(config_file: str, **kwargs) -> tuple[BpsConfig, QuantumGraph]:
114
+ def acquire_qgraph_driver(config_file: str, **kwargs) -> tuple[BpsConfig, PredictedQuantumGraph]:
115
115
  """Read a quantum graph from a file or create one from pipeline definition.
116
116
 
117
117
  Parameters
@@ -125,7 +125,7 @@ def acquire_qgraph_driver(config_file: str, **kwargs) -> tuple[BpsConfig, Quantu
125
125
  -------
126
126
  config : `lsst.ctrl.bps.BpsConfig`
127
127
  Updated configuration.
128
- qgraph : `lsst.pipe.base.graph.QuantumGraph`
128
+ qgraph : `lsst.pipe.base.quantum_graph.PredictedQuantumGraph`
129
129
  A graph representing quanta.
130
130
  """
131
131
  config = _init_submission_driver(config_file, **kwargs)
@@ -1,6 +1,6 @@
1
1
  #USER pipelineYaml: "${OBS_SUBARU_DIR}/pipelines/DRP.yaml#processCcd"
2
2
  # OR
3
- #USER qgraphFile: "/path/to/existing/file.qgraph"
3
+ #USER qgraphFile: "/path/to/existing/file.qg"
4
4
 
5
5
 
6
6
  # At minimum, following group used in bps report and can be
@@ -94,7 +94,7 @@ clusterAlgorithm: lsst.ctrl.bps.quantum_clustering_funcs.single_quantum_clusteri
94
94
 
95
95
  # Templates for bps filenames
96
96
  submitPath: ${PWD}/submit/{outputRun}
97
- qgraphFileTemplate: "{uniqProcName}.qgraph"
97
+ qgraphFileTemplate: "{uniqProcName}.qg"
98
98
  subDirTemplate: "{label}/{tract}/{patch}/{band}/{subfilter}/{physical_filter}/{visit}/{exposure}"
99
99
  templateDataId: "{tract}_{patch}_{band}_{visit}_{exposure}_{detector}"
100
100
 
@@ -38,7 +38,10 @@ import subprocess
38
38
  from pathlib import Path
39
39
 
40
40
  from lsst.ctrl.bps import BpsConfig, BpsSubprocessError
41
- from lsst.pipe.base.graph import QuantumGraph
41
+ from lsst.pipe.base import QuantumGraph
42
+ from lsst.pipe.base.pipeline_graph import TaskImportMode
43
+ from lsst.pipe.base.quantum_graph import PredictedQuantumGraph
44
+ from lsst.resources import ResourcePath
42
45
  from lsst.utils import doImport
43
46
  from lsst.utils.logging import VERBOSE
44
47
  from lsst.utils.timer import time_this, timeMethod
@@ -47,7 +50,7 @@ _LOG = logging.getLogger(__name__)
47
50
 
48
51
 
49
52
  @timeMethod(logger=_LOG, logLevel=VERBOSE)
50
- def acquire_quantum_graph(config: BpsConfig, out_prefix: str = "") -> tuple[str, QuantumGraph]:
53
+ def acquire_quantum_graph(config: BpsConfig, out_prefix: str = "") -> tuple[str, PredictedQuantumGraph]:
51
54
  """Read a quantum graph from a file or create one from scratch.
52
55
 
53
56
  Parameters
@@ -62,8 +65,8 @@ def acquire_quantum_graph(config: BpsConfig, out_prefix: str = "") -> tuple[str,
62
65
  -------
63
66
  qgraph_filename : `str`
64
67
  Name of file containing QuantumGraph that was read into qgraph.
65
- qgraph : `lsst.pipe.base.graph.QuantumGraph`
66
- A QuantumGraph read in from pre-generated file or one that is the
68
+ qgraph : `lsst.pipe.base.quantum_graph.PredictedQuantumGraph`
69
+ A quantum graph read in from pre-generated file or one that is the
67
70
  result of running code that generates it.
68
71
  """
69
72
  # Check to see if user provided pre-generated QuantumGraph.
@@ -90,8 +93,15 @@ def acquire_quantum_graph(config: BpsConfig, out_prefix: str = "") -> tuple[str,
90
93
 
91
94
  _LOG.info("Reading quantum graph from '%s'", qgraph_filename)
92
95
  with time_this(log=_LOG, level=logging.INFO, prefix=None, msg="Completed reading quantum graph"):
93
- qgraph = QuantumGraph.loadUri(qgraph_filename)
94
-
96
+ qgraph_path = ResourcePath(qgraph_filename)
97
+ if qgraph_path.getExtension() == ".qg":
98
+ with PredictedQuantumGraph.open(qgraph_path, import_mode=TaskImportMode.DO_NOT_IMPORT) as reader:
99
+ reader.read_thin_graph()
100
+ qgraph = reader.finish()
101
+ elif qgraph_path.getExtension() == ".qgraph":
102
+ qgraph = PredictedQuantumGraph.from_old_quantum_graph(QuantumGraph.loadUri(qgraph_path))
103
+ else:
104
+ raise ValueError(f"Unrecognized extension for quantum graph file: {qgraph_filename}.")
95
105
  return qgraph_filename, qgraph
96
106
 
97
107
 
@@ -244,8 +254,8 @@ def cluster_quanta(config, qgraph, name):
244
254
  ----------
245
255
  config : `lsst.ctrl.bps.BpsConfig`
246
256
  BPS configuration.
247
- qgraph : `lsst.pipe.base.QuantumGraph`
248
- Original full QuantumGraph for the run.
257
+ qgraph : `lsst.pipe.base.quantum_graph.PredictedQuantumGraph`
258
+ Original full quantum graph for the run.
249
259
  name : `str`
250
260
  Name for the ClusteredQuantumGraph that will be generated.
251
261
 
@@ -31,28 +31,31 @@ __all__ = ["check_clustering_config"]
31
31
 
32
32
  import logging
33
33
  import re
34
+ import uuid
34
35
  from collections import defaultdict
35
36
  from typing import Any
36
37
  from uuid import UUID
37
38
 
38
39
  from networkx import DiGraph, NetworkXNoCycle, find_cycle, topological_sort
39
40
 
40
- from lsst.pipe.base import QuantumGraph, QuantumNode
41
+ from lsst.pipe.base.quantum_graph import PredictedQuantumGraph, QuantumInfo
41
42
 
42
43
  from . import BpsConfig, ClusteredQuantumGraph, QuantaCluster
43
44
 
44
45
  _LOG = logging.getLogger(__name__)
45
46
 
46
47
 
47
- def single_quantum_clustering(config: BpsConfig, qgraph: QuantumGraph, name: str) -> ClusteredQuantumGraph:
48
+ def single_quantum_clustering(
49
+ config: BpsConfig, qgraph: PredictedQuantumGraph, name: str
50
+ ) -> ClusteredQuantumGraph:
48
51
  """Create clusters with only single quantum.
49
52
 
50
53
  Parameters
51
54
  ----------
52
55
  config : `lsst.ctrl.bps.BpsConfig`
53
56
  BPS configuration.
54
- qgraph : `lsst.pipe.base.QuantumGraph`
55
- QuantumGraph to break into clusters for ClusteredQuantumGraph.
57
+ qgraph : `lsst.pipe.base.quantum_graph.PredictedQuantumGraph`
58
+ Quantum graph to break into clusters for ClusteredQuantumGraph.
56
59
  name : `str`
57
60
  Name to give to ClusteredQuantumGraph.
58
61
 
@@ -76,18 +79,19 @@ def single_quantum_clustering(config: BpsConfig, qgraph: QuantumGraph, name: str
76
79
  cached_template = {}
77
80
 
78
81
  # Create cluster of single quantum.
79
- for qnode in qgraph:
80
- if qnode.taskDef.label not in cached_template:
82
+ for quantum_id, quantum_info in cqgraph.qxgraph.nodes.items():
83
+ task_label = quantum_info["task_label"]
84
+ if task_label not in cached_template:
81
85
  found, template_data_id = config.search(
82
86
  "templateDataId",
83
- opt={"curvals": {"curr_pipetask": qnode.taskDef.label}, "replaceVars": False},
87
+ opt={"curvals": {"curr_pipetask": task_label}, "replaceVars": False},
84
88
  )
85
89
  if found:
86
90
  template = "{label}_" + template_data_id
87
91
  _, use_node_number = config.search(
88
92
  "useNodeIdInClusterName",
89
93
  opt={
90
- "curvals": {"curr_pipetask": qnode.taskDef.label},
94
+ "curvals": {"curr_pipetask": task_label},
91
95
  "replaceVars": False,
92
96
  "default": True,
93
97
  },
@@ -96,21 +100,21 @@ def single_quantum_clustering(config: BpsConfig, qgraph: QuantumGraph, name: str
96
100
  template = "{node_number}_" + template
97
101
  else:
98
102
  template = "{node_number}"
99
- cached_template[qnode.taskDef.label] = template
103
+ cached_template[task_label] = template
100
104
 
101
- cluster = QuantaCluster.from_quantum_node(qnode, cached_template[qnode.taskDef.label])
105
+ cluster = QuantaCluster.from_quantum_info(quantum_id, quantum_info, cached_template[task_label])
102
106
 
103
107
  # Save mapping for use when creating dependencies.
104
- number_to_name[qnode.nodeId] = cluster.name
108
+ number_to_name[quantum_id] = cluster.name
105
109
 
106
110
  cqgraph.add_cluster(cluster)
107
111
 
108
112
  # Add cluster dependencies.
109
- for qnode in qgraph:
113
+ for quantum_id in cqgraph.qxgraph:
110
114
  # Get child nodes.
111
- children = qgraph.determineOutputsOfQuantumNode(qnode)
115
+ children = cqgraph.qxgraph.successors(quantum_id)
112
116
  for child in children:
113
- cqgraph.add_dependency(number_to_name[qnode.nodeId], number_to_name[child.nodeId])
117
+ cqgraph.add_dependency(number_to_name[quantum_id], number_to_name[child])
114
118
 
115
119
  return cqgraph
116
120
 
@@ -209,15 +213,17 @@ def check_clustering_config(
209
213
  return list(topological_sort(clustered_task_graph)), ordered_tasks
210
214
 
211
215
 
212
- def dimension_clustering(config: BpsConfig, qgraph: QuantumGraph, name: str) -> ClusteredQuantumGraph:
216
+ def dimension_clustering(
217
+ config: BpsConfig, qgraph: PredictedQuantumGraph, name: str
218
+ ) -> ClusteredQuantumGraph:
213
219
  """Follow config instructions to make clusters based upon dimensions.
214
220
 
215
221
  Parameters
216
222
  ----------
217
223
  config : `lsst.ctrl.bps.BpsConfig`
218
224
  BPS configuration.
219
- qgraph : `lsst.pipe.base.QuantumGraph`
220
- QuantumGraph to break into clusters for ClusteredQuantumGraph.
225
+ qgraph : `lsst.pipe.base.quantum_graph.PredictedQuantumGraph`
226
+ Quantum graph to break into clusters for ClusteredQuantumGraph.
221
227
  name : `str`
222
228
  Name to give to ClusteredQuantumGraph.
223
229
 
@@ -264,7 +270,7 @@ def dimension_clustering(config: BpsConfig, qgraph: QuantumGraph, name: str) ->
264
270
  def add_clusters_per_quantum(
265
271
  config: BpsConfig,
266
272
  label: str,
267
- qgraph: QuantumGraph,
273
+ qgraph: PredictedQuantumGraph,
268
274
  cqgraph: ClusteredQuantumGraph,
269
275
  quantum_to_cluster: dict[UUID, str],
270
276
  ) -> None:
@@ -276,8 +282,8 @@ def add_clusters_per_quantum(
276
282
  BPS configuration.
277
283
  label : `str`
278
284
  The taskDef label for which to add clusters.
279
- qgraph : `lsst.pipe.base.QuantumGraph`
280
- QuantumGraph providing quanta for the clusters.
285
+ qgraph : `lsst.pipe.base.quantum_graph.PredictedQuantumGraph`
286
+ Quantum graph providing quanta for the clusters.
281
287
  cqgraph : `lsst.ctrl.bps.ClusteredQuantumGraph`
282
288
  The ClusteredQuantumGraph to which the new 1-quantum
283
289
  clusters are added (modified in method).
@@ -304,16 +310,10 @@ def add_clusters_per_quantum(
304
310
  else:
305
311
  template = "{node_number}"
306
312
 
307
- # Currently getQuantaForTask is currently a mapping taskDef to
308
- # Quanta, so quick enough to call repeatedly.
309
- task_def = qgraph.findTaskDefByLabel(label)
310
- assert task_def is not None, f"Given taskDef label ({label}) not found in QuantumGraph" # for mypy
311
- quantum_nodes = qgraph.getNodesForTask(task_def)
312
-
313
- for qnode in quantum_nodes:
314
- cluster = QuantaCluster.from_quantum_node(qnode, template)
313
+ for quantum_id in qgraph.quanta_by_task[label].values():
314
+ cluster = QuantaCluster.from_quantum_info(quantum_id, cqgraph.qxgraph.nodes[quantum_id], template)
315
315
  cqgraph.add_cluster(cluster)
316
- quantum_to_cluster[qnode.nodeId] = cluster.name
316
+ quantum_to_cluster[quantum_id] = cluster.name
317
317
  add_cluster_dependencies(cqgraph, cluster, quantum_to_cluster)
318
318
 
319
319
 
@@ -467,7 +467,7 @@ def partition_cluster_values(
467
467
  def add_dim_clusters(
468
468
  cluster_config: BpsConfig,
469
469
  cluster_label: str,
470
- qgraph: QuantumGraph,
470
+ qgraph: PredictedQuantumGraph,
471
471
  ordered_tasks: dict[str, DiGraph],
472
472
  cqgraph: ClusteredQuantumGraph,
473
473
  quantum_to_cluster: dict[UUID, str],
@@ -505,14 +505,15 @@ def add_dim_clusters(
505
505
  partition_values: set[str] = set()
506
506
  quanta_info: dict[str, list[dict[str, Any]]] = {}
507
507
  for task_label in topological_sort(ordered_tasks[cluster_label]):
508
- # Determine cluster for each node
509
- task_def = qgraph.findTaskDefByLabel(task_label)
510
- assert task_def is not None # for mypy
511
- quantum_nodes = qgraph.getNodesForTask(task_def)
512
-
513
- for qnode in quantum_nodes:
514
- cluster_name, info = get_cluster_name_from_node(
515
- qnode, cluster_dims, cluster_label, template, equal_dims, partition_dims
508
+ for quantum_id in qgraph.quanta_by_task[task_label].values():
509
+ cluster_name, info = get_cluster_name_from_info(
510
+ quantum_id,
511
+ cqgraph.qxgraph.nodes[quantum_id],
512
+ cluster_dims,
513
+ cluster_label,
514
+ template,
515
+ equal_dims,
516
+ partition_dims,
516
517
  )
517
518
  if "partition_key" in info:
518
519
  partition_values.add(info["partition_key"])
@@ -551,23 +552,22 @@ def add_cluster_dependencies(
551
552
  from quantum_to_cluster or if their parent quantum node ids
552
553
  are missing from quantum_to_cluster.
553
554
  """
554
- qgraph = cqgraph.qgraph
555
555
  for node_id in cluster.qgraph_node_ids:
556
- cluster_node = qgraph.getQuantumNodeByNodeId(node_id)
557
- parents = qgraph.determineInputsToQuantumNode(cluster_node)
558
- for parent in parents:
556
+ cluster_node_info = cqgraph.get_quantum_info(node_id)
557
+ parents = cqgraph.qxgraph.predecessors(node_id)
558
+ for parent_id in parents:
559
559
  try:
560
- if quantum_to_cluster[parent.nodeId] != quantum_to_cluster[node_id]:
561
- cqgraph.add_dependency(quantum_to_cluster[parent.nodeId], quantum_to_cluster[node_id])
560
+ if quantum_to_cluster[parent_id] != quantum_to_cluster[node_id]:
561
+ cqgraph.add_dependency(quantum_to_cluster[parent_id], quantum_to_cluster[node_id])
562
562
  except KeyError as e: # pragma: no cover
563
563
  # For debugging a problem internal to method
564
- qnode = qgraph.getQuantumNodeByNodeId(e.args[0])
564
+ qnode_info = cqgraph.get_quantum_info(e.args[0])
565
565
  _LOG.error(
566
566
  "Quanta missing when clustering: cluster node = %s, %s; missing = %s, %s",
567
- cluster_node.taskDef.label,
568
- cluster_node.quantum.dataId,
569
- qnode.taskDef.label,
570
- qnode.quantum.dataId,
567
+ cluster_node_info["task_label"],
568
+ cluster_node_info["data_id"],
569
+ qnode_info["task_label"],
570
+ qnode_info["data_id"],
571
571
  )
572
572
  _LOG.error(quantum_to_cluster)
573
573
  raise
@@ -576,13 +576,13 @@ def add_cluster_dependencies(
576
576
  def add_dim_clusters_dependency(
577
577
  cluster_config: BpsConfig,
578
578
  cluster_label: str,
579
- qgraph: QuantumGraph,
579
+ qgraph: PredictedQuantumGraph,
580
580
  ordered_tasks: dict[str, DiGraph],
581
581
  cqgraph: ClusteredQuantumGraph,
582
582
  quantum_to_cluster: dict[UUID, str],
583
583
  ) -> None:
584
584
  """Add clusters for a cluster label to a ClusteredQuantumGraph using
585
- QuantumGraph dependencies as well as dimension values to help when
585
+ quantum graph dependencies as well as dimension values to help when
586
586
  some do not have particular dimension value.
587
587
 
588
588
  Parameters
@@ -591,8 +591,8 @@ def add_dim_clusters_dependency(
591
591
  BPS configuration for specific cluster label.
592
592
  cluster_label : `str`
593
593
  Cluster label for which to add clusters.
594
- qgraph : `lsst.pipe.base.QuantumGraph`
595
- QuantumGraph providing quanta for the clusters.
594
+ qgraph : `lsst.pipe.base.quantum_graph.PredictedQuantumGraph`
595
+ Quantum graph providing quanta for the clusters.
596
596
  ordered_tasks : `dict` [`str`, `networkx.DiGraph`]
597
597
  Mapping of cluster label to task label subgraph.
598
598
  cqgraph : `lsst.ctrl.bps.ClusteredQuantumGraph`
@@ -618,9 +618,9 @@ def add_dim_clusters_dependency(
618
618
  method = cluster_config["findDependencyMethod"]
619
619
  match method:
620
620
  case "source":
621
- find_possible_nodes = qgraph.determineOutputsOfQuantumNode
621
+ find_possible_nodes = cqgraph.qxgraph.successors
622
622
  case "sink":
623
- find_possible_nodes = qgraph.determineInputsToQuantumNode
623
+ find_possible_nodes = cqgraph.qxgraph.predecessors
624
624
  label_search_order.reverse()
625
625
  case _:
626
626
  raise RuntimeError(f"Invalid findDependencyMethod ({method})")
@@ -634,49 +634,60 @@ def add_dim_clusters_dependency(
634
634
  # quicker to check
635
635
  quanta_visited = set()
636
636
  for task_label in label_search_order:
637
- task_def = qgraph.findTaskDefByLabel(task_label)
638
- assert task_def is not None # for mypy
639
- for node in qgraph.getNodesForTask(task_def):
637
+ for quantum_id in qgraph.quanta_by_task[task_label].values():
640
638
  # skip if visited before
641
- if node.nodeId in quanta_visited:
639
+ if quantum_id in quanta_visited:
642
640
  continue
643
641
 
644
- cluster_name, info = get_cluster_name_from_node(
645
- node, cluster_dims, cluster_label, template, equal_dims, partition_dims
642
+ cluster_name, info = get_cluster_name_from_info(
643
+ quantum_id,
644
+ cqgraph.qxgraph.nodes[quantum_id],
645
+ cluster_dims,
646
+ cluster_label,
647
+ template,
648
+ equal_dims,
649
+ partition_dims,
646
650
  )
647
651
  if "partition_key" in info:
648
652
  partition_values.add(info["partition_key"])
649
653
  quanta_info.setdefault(cluster_name, []).append(info)
650
- quanta_visited.add(node.nodeId)
654
+ quanta_visited.add(quantum_id)
651
655
 
652
656
  # Use dependencies to find other quantum to add
653
657
  # Note: in testing, using the following code was faster than
654
658
  # using networkx descendants and ancestors functions
655
659
  # While traversing the QuantumGraph, nodes may appear
656
660
  # repeatedly in possible_nodes.
657
- nodes_to_use = [node]
661
+ nodes_to_use = [quantum_id]
658
662
  while nodes_to_use:
659
663
  node_to_use = nodes_to_use.pop()
660
- possible_nodes = find_possible_nodes(node_to_use)
661
- for possible_node in possible_nodes:
664
+ possible_node_ids = find_possible_nodes(node_to_use)
665
+ for possible_node_id in possible_node_ids:
662
666
  # skip if visited before
663
- if possible_node.nodeId in quanta_visited:
667
+ if possible_node_id in quanta_visited:
664
668
  continue
665
- quanta_visited.add(possible_node.nodeId)
666
-
667
- if possible_node.taskDef.label in ordered_tasks[cluster_label]:
668
- cluster_name, info = get_cluster_name_from_node(
669
- possible_node, cluster_dims, cluster_label, template, equal_dims, partition_dims
669
+ quanta_visited.add(possible_node_id)
670
+
671
+ possible_node_info = cqgraph.qxgraph.nodes[possible_node_id]
672
+ if possible_node_info["task_label"] in ordered_tasks[cluster_label]:
673
+ cluster_name, info = get_cluster_name_from_info(
674
+ possible_node_id,
675
+ possible_node_info,
676
+ cluster_dims,
677
+ cluster_label,
678
+ template,
679
+ equal_dims,
680
+ partition_dims,
670
681
  )
671
682
  if "partition_key" in info:
672
683
  partition_values.add(info["partition_key"])
673
684
  quanta_info.setdefault(cluster_name, []).append(info)
674
- nodes_to_use.append(possible_node)
685
+ nodes_to_use.append(possible_node_id)
675
686
  else:
676
687
  _LOG.debug(
677
688
  "label (%s) not in ordered_tasks. Not adding possible quantum %s",
678
- possible_node.taskDef.label,
679
- possible_node.nodeId,
689
+ possible_node_info["task_label"],
690
+ possible_node_id,
680
691
  )
681
692
 
682
693
  make_and_add_clusters(
@@ -746,8 +757,9 @@ def make_and_add_clusters(
746
757
  add_cluster_dependencies(cqgraph, cluster, quantum_to_cluster)
747
758
 
748
759
 
749
- def get_cluster_name_from_node(
750
- node: QuantumNode,
760
+ def get_cluster_name_from_info(
761
+ quantum_id: uuid.UUID,
762
+ quantum_info: QuantumInfo,
751
763
  cluster_dims: list[str],
752
764
  cluster_label: str,
753
765
  template: str,
@@ -758,8 +770,10 @@ def get_cluster_name_from_node(
758
770
 
759
771
  Parameters
760
772
  ----------
761
- node : `lsst.pipe.base.QuantumNode`
762
- QuantumNode from which to create the cluster.
773
+ quantum_id : `uuid.UUID`
774
+ Unique ID for the quantum.
775
+ quantum_info : `lsst.pipe.base.quantum_graph.QuantumInfo`
776
+ Info dictionary from which to create the cluster.
763
777
  cluster_dims : `list` [`str`]
764
778
  Dimension names to be used when clustering.
765
779
  cluster_label : `str`
@@ -780,8 +794,8 @@ def get_cluster_name_from_node(
780
794
  """
781
795
  # Gather info for cluster name template into a dictionary.
782
796
  info: dict[str, Any] = {
783
- "node_number": node.nodeId,
784
- "node_label": node.taskDef.label,
797
+ "node_number": quantum_id,
798
+ "node_label": quantum_info["task_label"],
785
799
  "label": cluster_label,
786
800
  }
787
801
 
@@ -789,8 +803,7 @@ def get_cluster_name_from_node(
789
803
  all_dims = cluster_dims + partition_dims
790
804
 
791
805
  missing_info = set()
792
- assert node.quantum.dataId is not None # for mypy
793
- data_id_info = dict(node.quantum.dataId.mapping)
806
+ data_id_info = dict(quantum_info["data_id"].mapping)
794
807
  for dim_name in all_dims:
795
808
  _LOG.debug("dim_name = %s", dim_name)
796
809
  if dim_name in data_id_info:
@@ -807,7 +820,7 @@ def get_cluster_name_from_node(
807
820
 
808
821
  if missing_info:
809
822
  raise RuntimeError(
810
- f"Quantum {node.nodeId} ({data_id_info}) missing dimensions: {','.join(missing_info)}; "
823
+ f"Quantum {quantum_id} ({data_id_info}) missing dimensions: {','.join(missing_info)}; "
811
824
  f"required for cluster {cluster_label}"
812
825
  )
813
826
 
lsst/ctrl/bps/report.py CHANGED
@@ -38,12 +38,18 @@ import sys
38
38
  from collections.abc import Callable, Sequence
39
39
  from typing import TextIO
40
40
 
41
- from lsst.utils import doImport
42
-
43
- from .bps_reports import DetailedRunReport, ExitCodesReport, SummaryRunReport, compile_job_summary
44
- from .wms_service import WmsRunReport, WmsStates
45
-
46
- BPS_POSTPROCESSORS = (compile_job_summary,)
41
+ from lsst.utils import doImportType
42
+
43
+ from .bps_reports import (
44
+ DetailedRunReport,
45
+ ExitCodesReport,
46
+ SummaryRunReport,
47
+ compile_code_summary,
48
+ compile_job_summary,
49
+ )
50
+ from .wms_service import BaseWmsService, WmsRunReport, WmsStates
51
+
52
+ BPS_POSTPROCESSORS = (compile_job_summary, compile_code_summary)
47
53
  """Postprocessors for massaging run reports
48
54
  (`tuple` [`Callable` [[`WmsRunReport`], None]).
49
55
  """
@@ -111,7 +117,7 @@ def display_report(
111
117
 
112
118
  run_report.add(run, use_global_id=is_global)
113
119
  if run_report.message:
114
- print(run_report.message, file=file)
120
+ messages.append(run_report.message)
115
121
 
116
122
  print(run_brief, file=file)
117
123
  print("\n", file=file)
@@ -132,6 +138,8 @@ def display_report(
132
138
  ]
133
139
  run_exits_report = ExitCodesReport(fields)
134
140
  run_exits_report.add(run, use_global_id=is_global)
141
+ if run_exits_report.message:
142
+ messages.append(run_exits_report.message)
135
143
  print("\n", file=file)
136
144
  print(run_exits_report, file=file)
137
145
  run_exits_report.clear()
@@ -145,12 +153,13 @@ def display_report(
145
153
  print(run_brief, file=file)
146
154
 
147
155
  if messages:
148
- print("\n".join(messages), file=file)
156
+ uniques = list(dict.fromkeys(messages))
157
+ print("\n".join(uniques), file=file)
149
158
  print("\n", file=file)
150
159
 
151
160
 
152
161
  def retrieve_report(
153
- wms_service: str,
162
+ wms_service_fqn: str,
154
163
  *,
155
164
  run_id: str | None = None,
156
165
  user: str | None = None,
@@ -163,7 +172,7 @@ def retrieve_report(
163
172
 
164
173
  Parameters
165
174
  ----------
166
- wms_service : `str`
175
+ wms_service_fqn : `str`
167
176
  Name of the WMS service class.
168
177
  run_id : `str`, optional
169
178
  A run id the report will be restricted to.
@@ -196,11 +205,21 @@ def retrieve_report(
196
205
  messages : `list` [`str`]
197
206
  Errors that happened during report retrieval and/or processing.
198
207
  Empty if no issues were encountered.
208
+
209
+ Raises
210
+ ------
211
+ TypeError
212
+ Raised if the WMS service class is not a subclass of BaseWmsService.
199
213
  """
200
- messages = []
214
+ messages: list[str] = []
201
215
 
202
- wms_service_class = doImport(wms_service)
216
+ wms_service_class = doImportType(wms_service_fqn)
217
+ if not issubclass(wms_service_class, BaseWmsService):
218
+ raise TypeError(
219
+ f"Invalid WMS service class '{wms_service_fqn}'; must be a subclass of BaseWmsService"
220
+ )
203
221
  wms_service = wms_service_class({})
222
+
204
223
  reports, message = wms_service.report(
205
224
  wms_workflow_id=run_id, user=user, hist=hist, pass_thru=pass_thru, is_global=is_global
206
225
  )
@@ -210,12 +229,12 @@ def retrieve_report(
210
229
  if postprocessors:
211
230
  for report in reports:
212
231
  for postprocessor in postprocessors:
213
- try:
214
- postprocessor(report)
215
- except Exception as exc:
216
- messages.append(
217
- f"Postprocessing error for '{report.wms_id}': {str(exc)} "
218
- f"(origin: {postprocessor.__name__})"
219
- )
232
+ if warnings := postprocessor(report):
233
+ for warning in warnings:
234
+ messages.append(
235
+ f"WARNING: Report may be incomplete. "
236
+ f"There was an issue with report postprocessing for '{report.wms_id}': "
237
+ f"{warning} (origin: {postprocessor.__name__})"
238
+ )
220
239
 
221
240
  return reports, messages
@@ -190,16 +190,6 @@ def create_init_workflow(
190
190
  # Adjust job attributes values if necessary.
191
191
  _handle_job_values(job_values, gwjob)
192
192
 
193
- # Pick a node id for each task (not quantum!) to avoid reading the entire
194
- # quantum graph during the initialization stage.
195
- node_ids = []
196
- for task_label in qgraph.pipeline_graph.tasks:
197
- task_def = qgraph.findTaskDefByLabel(task_label)
198
- node = next(iter(qgraph.getNodesForTask(task_def)))
199
- node_ids.append(node.nodeId)
200
- gwjob.cmdvals["qgraphId"] = qgraph.graphID
201
- gwjob.cmdvals["qgraphNodeId"] = ",".join(sorted([f"{node_id}" for node_id in node_ids]))
202
-
203
193
  init_workflow.add_job(gwjob)
204
194
  init_workflow.add_job_inputs(gwjob.name, [qgraph_gwfile])
205
195
  _enhance_command(config, init_workflow, gwjob, {})
@@ -682,14 +672,13 @@ def create_generic_workflow(
682
672
  # either common or aggregate for all Quanta in cluster.
683
673
  for node_id in iter(cluster.qgraph_node_ids):
684
674
  _LOG.debug("node_id=%s", node_id)
685
- qnode = cqgraph.get_quantum_node(node_id)
675
+ quantum_info = cqgraph.get_quantum_info(node_id)
686
676
 
687
- if qnode.taskDef.label not in cached_pipetask_values:
688
- search_opt["curvals"]["curr_pipetask"] = qnode.taskDef.label
689
- cached_pipetask_values[qnode.taskDef.label] = _get_job_values(
690
- config, search_opt, "runQuantumCommand"
691
- )
692
- _handle_job_values(cached_pipetask_values[qnode.taskDef.label], gwjob, unset_attributes)
677
+ task_label = quantum_info["task_label"]
678
+ if task_label not in cached_pipetask_values:
679
+ search_opt["curvals"]["curr_pipetask"] = task_label
680
+ cached_pipetask_values[task_label] = _get_job_values(config, search_opt, "runQuantumCommand")
681
+ _handle_job_values(cached_pipetask_values[task_label], gwjob, unset_attributes)
693
682
 
694
683
  # Update job with workflow attribute and profile values.
695
684
  qgraph_gwfile = _get_qgraph_gwfile(
@@ -699,7 +688,6 @@ def create_generic_workflow(
699
688
  generic_workflow.add_job(gwjob)
700
689
  generic_workflow.add_job_inputs(gwjob.name, [qgraph_gwfile])
701
690
 
702
- gwjob.cmdvals["qgraphId"] = cqgraph.qgraph.graphID
703
691
  gwjob.cmdvals["qgraphNodeId"] = ",".join(
704
692
  sorted([f"{node_id}" for node_id in cluster.qgraph_node_ids])
705
693
  )
lsst/ctrl/bps/version.py CHANGED
@@ -1,2 +1,2 @@
1
1
  __all__ = ["__version__"]
2
- __version__ = "29.2025.3800"
2
+ __version__ = "29.2025.4000"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lsst-ctrl-bps
3
- Version: 29.2025.3800
3
+ Version: 29.2025.4000
4
4
  Summary: Pluggable execution of workflow graphs from Rubin pipelines.
5
5
  Author-email: Rubin Observatory Data Management <dm-admin@lists.lsst.org>
6
6
  License: BSD 3-Clause License
@@ -4,25 +4,25 @@ lsst/ctrl/bps/__init__.py,sha256=qNei3h4EwqUnyyCsBUkYXcRNNlSvzUzbM6Zdkd4eklE,166
4
4
  lsst/ctrl/bps/_exceptions.py,sha256=qNftFVE8yhiRD8yvACKEf5iHhi1o2eaa69ZCBvw6lDo,1974
5
5
  lsst/ctrl/bps/bps_config.py,sha256=4t1jyIBiw9leAySjkcksCaZ3CPJMOt8afxGxTEWRSyQ,18460
6
6
  lsst/ctrl/bps/bps_draw.py,sha256=Sl04jpbm6jB_5lBzfJprqnDes9t_aqQQhr0Ag5RSv-Y,1984
7
- lsst/ctrl/bps/bps_reports.py,sha256=j6HdAvkOBTSxSHmkx_vd04FvifToTaZVGpwfF-LIUu0,12487
8
- lsst/ctrl/bps/bps_utils.py,sha256=-w0HSUXKw8jgJFl28yzwJuuqLqT_UOw12_XQBELUWrE,12206
7
+ lsst/ctrl/bps/bps_reports.py,sha256=73l5Gz00VQq8NgoBrGkvCaUQJE5kw2emhQxxzthMQzs,15574
8
+ lsst/ctrl/bps/bps_utils.py,sha256=7SpzRjqpPNw_hVCyDRxoQ-cDvPnlzZ8k4epawAksM0Q,12202
9
9
  lsst/ctrl/bps/cancel.py,sha256=mAdBi-oUpepyo-1MCqx_I34dbm6cqT0VJu3d2-y9T2Y,3317
10
- lsst/ctrl/bps/clustered_quantum_graph.py,sha256=mBf8s_DlTzGCFq7aAKmD1cAXq6Cqr8QxjwpgXPOQcRc,18765
10
+ lsst/ctrl/bps/clustered_quantum_graph.py,sha256=vpsjyzIr6e5as6rkRphrleY8q2I_8xXE3BawLGCtTlo,18167
11
11
  lsst/ctrl/bps/constants.py,sha256=dsnsNMqwU6Xl_ln6nQ0PmxsghlbBY9mLJWf2rlzYayQ,1733
12
12
  lsst/ctrl/bps/construct.py,sha256=o-JhTF_eehRiIxy3xe2UgBISyKNBg-kduthGiqfa50g,8100
13
- lsst/ctrl/bps/drivers.py,sha256=M_zWXvzK23MBBNvqdbJCnA_9DUCoFznCjWfwQw9BO6Q,22512
13
+ lsst/ctrl/bps/drivers.py,sha256=yQ7bY-8Ccy_uAvw5YiQYLu9W2fkePb0Rde_xwyIXyAg,22561
14
14
  lsst/ctrl/bps/generic_workflow.py,sha256=BwEFBudncziHTZaTTknOQroY_a-33mGf-mq9PzxH8G0,53720
15
15
  lsst/ctrl/bps/initialize.py,sha256=DCQwB9ZSo_36Ncs8jMJwxKZcYCu46jjww6rzyiuMFKY,7554
16
16
  lsst/ctrl/bps/ping.py,sha256=orwTZUNFtlexMYFcNWW_48jaa7Jo1oK4_eb_HuC-p5E,2235
17
- lsst/ctrl/bps/pre_transform.py,sha256=mPUW1QuHwgkSgZGVK6xO4RN590z4g1zf3p4kNsx_yOg,10222
17
+ lsst/ctrl/bps/pre_transform.py,sha256=uxhSiG-_NyX175nL7d1dvOmLprdbN_gxaWr7X7nzhXE,10931
18
18
  lsst/ctrl/bps/prepare.py,sha256=Fa2OEQIo4Pa8R5WmRo0PvJgXWNjynRijATvu1x80qlw,3129
19
- lsst/ctrl/bps/quantum_clustering_funcs.py,sha256=8fidDw53MpcqeWVXs0F0jIvSU0fLV_-1fEbC-Jhqg28,31816
20
- lsst/ctrl/bps/report.py,sha256=6qCt6-0apuVZrevtQzJLRmQc_ve_uPyNH3HkgC5KSjc,7898
19
+ lsst/ctrl/bps/quantum_clustering_funcs.py,sha256=UuVHtwsoSIUJnAPyoIGX46TzoZRD6gIzgNrPFPaq3G8,32143
20
+ lsst/ctrl/bps/report.py,sha256=893m2KBBaQouAml8DKmmVxBo-Q7fFizxl_OT6wyFBXk,8575
21
21
  lsst/ctrl/bps/restart.py,sha256=yVwxeviLiehyIfPmwU-H3tJ9ou7OWZZcrNf8PMxjr8o,2298
22
22
  lsst/ctrl/bps/status.py,sha256=Lrt0cAqROv77B8UvYXGimCa4cDHBD1N0K2Xx7oS6fXk,3362
23
23
  lsst/ctrl/bps/submit.py,sha256=Ev-yhcoZwtBPIo5bRt_4XFJRtgQBd8JHUurEfn01HpU,2880
24
- lsst/ctrl/bps/transform.py,sha256=P3dGwKStoCLOn7F2Oez6B__f7bGV5jlUF_Vj9lJQcU0,35256
25
- lsst/ctrl/bps/version.py,sha256=1tuAAU5Re7MRhtcbYlCODj58a79T6z8ie3-QgPCtLgE,55
24
+ lsst/ctrl/bps/transform.py,sha256=y_78yBO17T4oBJ-3tCKtMwcgaT45XrLxwtEEr1vUIMQ,34687
25
+ lsst/ctrl/bps/version.py,sha256=OZgl6BR-Rxy62NJlHO2yg_Rq4EhfmRSKDkEEnAyc-3Q,55
26
26
  lsst/ctrl/bps/wms_service.py,sha256=l3T6i1MG72dhHY0KXMUlBjWUpCLOfaySs7o2W8oCwhs,18891
27
27
  lsst/ctrl/bps/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
28
  lsst/ctrl/bps/cli/bps.py,sha256=CHfAL-U4mSi-FqeGKtmkX5nI5H9gkRre4XEWNVdeMRk,2559
@@ -32,16 +32,16 @@ lsst/ctrl/bps/cli/opt/__init__.py,sha256=6-bAftMGxt_uEKrQl0gtrg8dJLBNsPd_DWCApg_
32
32
  lsst/ctrl/bps/cli/opt/arguments.py,sha256=Q9nU1Ad1CRv0HgIM68bu6sqCfJT1om-ARZy7pVXGJ94,1534
33
33
  lsst/ctrl/bps/cli/opt/option_groups.py,sha256=xylZX9CmxrTg1FJQ0ziyWYF0Se6CvO6ydgAwUZFTkR4,2722
34
34
  lsst/ctrl/bps/cli/opt/options.py,sha256=pZQpjJ2Vrx6kYJGs5vVERBMFstPocwBXHffxkWkmavg,3088
35
- lsst/ctrl/bps/etc/bps_defaults.yaml,sha256=UwKAKWl09-Xjtl6kiz-AiXA9SDczeCvPGRXOPG3fzb8,4828
35
+ lsst/ctrl/bps/etc/bps_defaults.yaml,sha256=wYIF5qqqw3KnEBGHxDscCLzFSK-kKSj_NLbPSmf-8Rg,4820
36
36
  lsst/ctrl/bps/tests/config_test_utils.py,sha256=WM8Vrigk4OO0TBoL1A73a6hLhf2a6-ACD20fROJ0U7A,3537
37
37
  lsst/ctrl/bps/tests/gw_test_utils.py,sha256=zVVQqzwSiQgPgk9TnqDzgR7uDnaTMeuBLYKA8vOp5RI,22452
38
- lsst_ctrl_bps-29.2025.3800.dist-info/licenses/COPYRIGHT,sha256=Lc6NoAEFQ65v_SmtS9NwfHTOuSUtC2Umbjv5zyowiQM,61
39
- lsst_ctrl_bps-29.2025.3800.dist-info/licenses/LICENSE,sha256=pRExkS03v0MQW-neNfIcaSL6aiAnoLxYgtZoFzQ6zkM,232
40
- lsst_ctrl_bps-29.2025.3800.dist-info/licenses/bsd_license.txt,sha256=7MIcv8QRX9guUtqPSBDMPz2SnZ5swI-xZMqm_VDSfxY,1606
41
- lsst_ctrl_bps-29.2025.3800.dist-info/licenses/gpl-v3.0.txt,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
42
- lsst_ctrl_bps-29.2025.3800.dist-info/METADATA,sha256=aHhvIlTTGIT0HrxaLiIfbWUSw7LRuvWzsiHKBZMzfjo,2190
43
- lsst_ctrl_bps-29.2025.3800.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
44
- lsst_ctrl_bps-29.2025.3800.dist-info/entry_points.txt,sha256=d6FhN79h7s9frdBI7YkScsGEInwpGGub49pAjXWbIbI,51
45
- lsst_ctrl_bps-29.2025.3800.dist-info/top_level.txt,sha256=eUWiOuVVm9wwTrnAgiJT6tp6HQHXxIhj2QSZ7NYZH80,5
46
- lsst_ctrl_bps-29.2025.3800.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
47
- lsst_ctrl_bps-29.2025.3800.dist-info/RECORD,,
38
+ lsst_ctrl_bps-29.2025.4000.dist-info/licenses/COPYRIGHT,sha256=Lc6NoAEFQ65v_SmtS9NwfHTOuSUtC2Umbjv5zyowiQM,61
39
+ lsst_ctrl_bps-29.2025.4000.dist-info/licenses/LICENSE,sha256=pRExkS03v0MQW-neNfIcaSL6aiAnoLxYgtZoFzQ6zkM,232
40
+ lsst_ctrl_bps-29.2025.4000.dist-info/licenses/bsd_license.txt,sha256=7MIcv8QRX9guUtqPSBDMPz2SnZ5swI-xZMqm_VDSfxY,1606
41
+ lsst_ctrl_bps-29.2025.4000.dist-info/licenses/gpl-v3.0.txt,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
42
+ lsst_ctrl_bps-29.2025.4000.dist-info/METADATA,sha256=QLzpoPS9PyGTZ-1lgk8iceVvuEkxgiCuSYxB_fDoqLw,2190
43
+ lsst_ctrl_bps-29.2025.4000.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
44
+ lsst_ctrl_bps-29.2025.4000.dist-info/entry_points.txt,sha256=d6FhN79h7s9frdBI7YkScsGEInwpGGub49pAjXWbIbI,51
45
+ lsst_ctrl_bps-29.2025.4000.dist-info/top_level.txt,sha256=eUWiOuVVm9wwTrnAgiJT6tp6HQHXxIhj2QSZ7NYZH80,5
46
+ lsst_ctrl_bps-29.2025.4000.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
47
+ lsst_ctrl_bps-29.2025.4000.dist-info/RECORD,,