lsst-ctrl-bps 29.2025.3900__tar.gz → 29.2025.4000__tar.gz

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 (73) hide show
  1. {lsst_ctrl_bps-29.2025.3900/python/lsst_ctrl_bps.egg-info → lsst_ctrl_bps-29.2025.4000}/PKG-INFO +1 -1
  2. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/doc/lsst.ctrl.bps/quickstart.rst +6 -7
  3. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/python/lsst/ctrl/bps/bps_utils.py +1 -1
  4. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/python/lsst/ctrl/bps/clustered_quantum_graph.py +48 -61
  5. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/python/lsst/ctrl/bps/drivers.py +3 -3
  6. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/python/lsst/ctrl/bps/etc/bps_defaults.yaml +2 -2
  7. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/python/lsst/ctrl/bps/pre_transform.py +18 -8
  8. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/python/lsst/ctrl/bps/quantum_clustering_funcs.py +96 -83
  9. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/python/lsst/ctrl/bps/transform.py +6 -18
  10. lsst_ctrl_bps-29.2025.4000/python/lsst/ctrl/bps/version.py +2 -0
  11. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000/python/lsst_ctrl_bps.egg-info}/PKG-INFO +1 -1
  12. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/tests/test_clustered_quantum_graph.py +18 -22
  13. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/tests/test_pre_transform.py +6 -7
  14. lsst_ctrl_bps-29.2025.3900/python/lsst/ctrl/bps/version.py +0 -2
  15. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/COPYRIGHT +0 -0
  16. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/LICENSE +0 -0
  17. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/MANIFEST.in +0 -0
  18. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/README.md +0 -0
  19. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/bsd_license.txt +0 -0
  20. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/doc/lsst.ctrl.bps/CHANGES.rst +0 -0
  21. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/doc/lsst.ctrl.bps/index.rst +0 -0
  22. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/gpl-v3.0.txt +0 -0
  23. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/pyproject.toml +0 -0
  24. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/python/lsst/__init__.py +0 -0
  25. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/python/lsst/ctrl/__init__.py +0 -0
  26. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/python/lsst/ctrl/bps/__init__.py +0 -0
  27. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/python/lsst/ctrl/bps/_exceptions.py +0 -0
  28. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/python/lsst/ctrl/bps/bps_config.py +0 -0
  29. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/python/lsst/ctrl/bps/bps_draw.py +0 -0
  30. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/python/lsst/ctrl/bps/bps_reports.py +0 -0
  31. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/python/lsst/ctrl/bps/cancel.py +0 -0
  32. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/python/lsst/ctrl/bps/cli/__init__.py +0 -0
  33. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/python/lsst/ctrl/bps/cli/bps.py +0 -0
  34. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/python/lsst/ctrl/bps/cli/cmd/__init__.py +0 -0
  35. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/python/lsst/ctrl/bps/cli/cmd/commands.py +0 -0
  36. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/python/lsst/ctrl/bps/cli/opt/__init__.py +0 -0
  37. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/python/lsst/ctrl/bps/cli/opt/arguments.py +0 -0
  38. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/python/lsst/ctrl/bps/cli/opt/option_groups.py +0 -0
  39. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/python/lsst/ctrl/bps/cli/opt/options.py +0 -0
  40. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/python/lsst/ctrl/bps/constants.py +0 -0
  41. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/python/lsst/ctrl/bps/construct.py +0 -0
  42. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/python/lsst/ctrl/bps/generic_workflow.py +0 -0
  43. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/python/lsst/ctrl/bps/initialize.py +0 -0
  44. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/python/lsst/ctrl/bps/ping.py +0 -0
  45. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/python/lsst/ctrl/bps/prepare.py +0 -0
  46. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/python/lsst/ctrl/bps/report.py +0 -0
  47. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/python/lsst/ctrl/bps/restart.py +0 -0
  48. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/python/lsst/ctrl/bps/status.py +0 -0
  49. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/python/lsst/ctrl/bps/submit.py +0 -0
  50. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/python/lsst/ctrl/bps/tests/config_test_utils.py +0 -0
  51. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/python/lsst/ctrl/bps/tests/gw_test_utils.py +0 -0
  52. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/python/lsst/ctrl/bps/wms_service.py +0 -0
  53. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/python/lsst_ctrl_bps.egg-info/SOURCES.txt +0 -0
  54. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/python/lsst_ctrl_bps.egg-info/dependency_links.txt +0 -0
  55. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/python/lsst_ctrl_bps.egg-info/entry_points.txt +0 -0
  56. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/python/lsst_ctrl_bps.egg-info/requires.txt +0 -0
  57. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/python/lsst_ctrl_bps.egg-info/top_level.txt +0 -0
  58. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/python/lsst_ctrl_bps.egg-info/zip-safe +0 -0
  59. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/setup.cfg +0 -0
  60. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/tests/test_bps_reports.py +0 -0
  61. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/tests/test_bps_utils.py +0 -0
  62. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/tests/test_bpsconfig.py +0 -0
  63. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/tests/test_cli_commands.py +0 -0
  64. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/tests/test_construct.py +0 -0
  65. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/tests/test_drivers.py +0 -0
  66. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/tests/test_generic_workflow.py +0 -0
  67. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/tests/test_initialize.py +0 -0
  68. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/tests/test_ping.py +0 -0
  69. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/tests/test_quantum_clustering_funcs.py +0 -0
  70. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/tests/test_report.py +0 -0
  71. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/tests/test_status.py +0 -0
  72. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/tests/test_transform.py +0 -0
  73. {lsst_ctrl_bps-29.2025.3900 → lsst_ctrl_bps-29.2025.4000}/tests/test_wms_service.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lsst-ctrl-bps
3
- Version: 29.2025.3900
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
@@ -157,7 +157,7 @@ or a pre-made file containing a serialized QuantumGraph, for example
157
157
 
158
158
  .. code-block:: YAML
159
159
 
160
- qgraphFile: pipelines_check_w_2020_45.qgraph
160
+ qgraphFile: pipelines_check_w_2020_45.qg
161
161
 
162
162
  .. warning::
163
163
 
@@ -846,7 +846,7 @@ Supported settings
846
846
  When to output job QuantumGraph files (default = TRANSFORM).
847
847
 
848
848
  * NEVER = all jobs will use full QuantumGraph file. (Warning: make sure
849
- runQuantumCommand has ``--qgraph-id {qgraphId} --qgraph-node-id {qgraphNodeId}``.)
849
+ runQuantumCommand has ``--qgraph-node-id {qgraphNodeId}``.)
850
850
  * TRANSFORM = Output QuantumGraph files after creating GenericWorkflow.
851
851
  * PREPARE = QuantumGraph files are output after creating WMS submission.
852
852
 
@@ -904,7 +904,7 @@ Reserved keywords
904
904
  However, contrary to YAML specification, it is currently not portable.
905
905
 
906
906
  **qgraphId**
907
- Internal ID for the full QuantumGraph (passed as ``--qgraph-id`` on pipetask command line).
907
+ Ignored; accepted for backwards compatibility.
908
908
 
909
909
  **qgraphNodeId**
910
910
  Comma-separated list of internal QuantumGraph node numbers to be
@@ -1079,13 +1079,12 @@ single full QuantumGraph file plus node numbers for each job. The default is
1079
1079
  using per-job QuantumGraph files.
1080
1080
 
1081
1081
  To use full QuantumGraph file, the submit YAML must set ``whenSaveJobQgraph`` to
1082
- "NEVER" and the ``pipetask run`` command must include ``--qgraph-id {qgraphId}
1083
- --qgraph-node-id {qgraphNodeId}``. For example:
1082
+ "NEVER" and the ``pipetask run`` command must include ``--qgraph-node-id {qgraphNodeId}``. For example:
1084
1083
 
1085
1084
  .. code::
1086
1085
 
1087
1086
  whenSaveJobQgraph: "NEVER"
1088
- runQuantumCommand: "${CTRL_MPEXEC_DIR}/bin/pipetask --long-log run -b {butlerConfig} --output {output} --output-run {outputRun} --qgraph {qgraphFile} --qgraph-id {qgraphId} --qgraph-node-id {qgraphNodeId} --skip-init-writes --extend-run --clobber-outputs --skip-existing"
1087
+ runQuantumCommand: "${CTRL_MPEXEC_DIR}/bin/pipetask --long-log run -b {butlerConfig} --output {output} --output-run {outputRun} --qgraph {qgraphFile} --qgraph-node-id {qgraphNodeId} --skip-init-writes --extend-run --clobber-outputs --skip-existing"
1089
1088
 
1090
1089
 
1091
1090
  .. warning::
@@ -1244,7 +1243,7 @@ The major differences to users are:
1244
1243
  the output run in the provided pre-existing quantum graph.
1245
1244
  - ``final_post_finalJob.out``: An internal file for debugging incorrect
1246
1245
  reporting of final run status.
1247
- - ``<qgraph_filename>_orig.qgraph``: A backup copy of the original
1246
+ - ``<qgraph_filename>_orig.qg``: A backup copy of the original
1248
1247
  pre-existing quantum graph file that was used for submitting the run. Note
1249
1248
  that this file will *not* be present in the submit directory if the
1250
1249
  pipeline YAML specification was used during the submission instead.
@@ -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
 
@@ -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
 
@@ -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
  )
@@ -0,0 +1,2 @@
1
+ __all__ = ["__version__"]
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.3900
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
@@ -50,54 +50,49 @@ class TestQuantaCluster(unittest.TestCase):
50
50
 
51
51
  def setUp(self):
52
52
  self.qgraph = make_test_quantum_graph()
53
- nodes = list(self.qgraph.getNodesForTask(self.qgraph.findTaskDefByLabel("T1")))
54
- self.qnode1 = nodes[0]
55
- self.qnode2 = nodes[1]
53
+ self.id1, self.id2, *_ = self.qgraph.quanta_by_task["T1"].values()
54
+ self.info1 = self.qgraph.quantum_only_xgraph.nodes[self.id1]
55
+ self.info2 = self.qgraph.quantum_only_xgraph.nodes[self.id2]
56
56
 
57
57
  def tearDown(self):
58
58
  pass
59
59
 
60
60
  def testQgraphNodeIds(self):
61
- qc = QuantaCluster.from_quantum_node(self.qnode1, "{node_number}")
62
- self.assertEqual(qc.qgraph_node_ids, frozenset([self.qnode1.nodeId]))
61
+ qc = QuantaCluster.from_quantum_info(self.id1, self.info1, "{node_number}")
62
+ self.assertEqual(qc.qgraph_node_ids, frozenset([self.id1]))
63
63
 
64
64
  def testQuantaCountsNone(self):
65
65
  qc = QuantaCluster("NoQuanta", "the_label")
66
66
  self.assertEqual(qc.quanta_counts, Counter())
67
67
 
68
68
  def testQuantaCounts(self):
69
- qc = QuantaCluster.from_quantum_node(self.qnode1, "{node_number}")
69
+ qc = QuantaCluster.from_quantum_info(self.id1, self.info1, "{node_number}")
70
70
  self.assertEqual(qc.quanta_counts, Counter({"T1": 1}))
71
71
 
72
- def testAddQuantumNode(self):
73
- qc = QuantaCluster.from_quantum_node(self.qnode1, "{node_number}")
74
- qc.add_quantum_node(self.qnode2)
75
- self.assertEqual(qc.quanta_counts, Counter({"T1": 2}))
76
-
77
72
  def testAddQuantum(self):
78
- qc = QuantaCluster.from_quantum_node(self.qnode1, "{node_number}")
79
- qc.add_quantum(self.qnode2.quantum, self.qnode2.taskDef.label)
73
+ qc = QuantaCluster.from_quantum_info(self.id1, self.info1, "{node_number}")
74
+ qc.add_quantum(self.id2, self.info2["task_label"])
80
75
  self.assertEqual(qc.quanta_counts, Counter({"T1": 2}))
81
76
 
82
77
  def testStr(self):
83
- qc = QuantaCluster.from_quantum_node(self.qnode1, "{node_number}")
78
+ qc = QuantaCluster.from_quantum_info(self.id1, self.info1, "{node_number}")
84
79
  self.assertIn(qc.name, str(qc))
85
80
  self.assertIn("T1", str(qc))
86
81
  self.assertIn("tags", str(qc))
87
82
 
88
83
  def testEqual(self):
89
- qc1 = QuantaCluster.from_quantum_node(self.qnode1, "{node_number}")
90
- qc2 = QuantaCluster.from_quantum_node(self.qnode1, "{node_number}")
84
+ qc1 = QuantaCluster.from_quantum_info(self.id1, self.info1, "{node_number}")
85
+ qc2 = QuantaCluster.from_quantum_info(self.id1, self.info1, "{node_number}")
91
86
  self.assertEqual(qc1, qc2)
92
87
 
93
88
  def testNotEqual(self):
94
- qc1 = QuantaCluster.from_quantum_node(self.qnode1, "{node_number}")
95
- qc2 = QuantaCluster.from_quantum_node(self.qnode2, "{node_number}")
89
+ qc1 = QuantaCluster.from_quantum_info(self.id1, self.info1, "{node_number}")
90
+ qc2 = QuantaCluster.from_quantum_info(self.id2, self.info2, "{node_number}")
96
91
  self.assertNotEqual(qc1, qc2)
97
92
 
98
93
  def testHash(self):
99
- qc1 = QuantaCluster.from_quantum_node(self.qnode1, "{node_number}")
100
- qc2 = QuantaCluster.from_quantum_node(self.qnode2, "{node_number}")
94
+ qc1 = QuantaCluster.from_quantum_info(self.id1, self.info1, "{node_number}")
95
+ qc2 = QuantaCluster.from_quantum_info(self.id2, self.info2, "{node_number}")
101
96
  self.assertNotEqual(hash(qc1), hash(qc2))
102
97
 
103
98
 
@@ -196,8 +191,9 @@ class TestClusteredQuantumGraph(unittest.TestCase):
196
191
  def testValidateDuplicateId(self):
197
192
  # Add new Quanta with duplicate Quantum
198
193
  qc1 = self.cqg1.get_cluster("T1_1_2")
199
- qnode = self.cqg1.get_quantum_node(next(iter(qc1.qgraph_node_ids)))
200
- qc = QuantaCluster.from_quantum_node(qnode, "DuplicateId")
194
+ quantum_id = next(iter(qc1.qgraph_node_ids))
195
+ quantum_info = self.cqg1.get_quantum_info(quantum_id)
196
+ qc = QuantaCluster.from_quantum_info(quantum_id, quantum_info, "DuplicateId")
201
197
  self.cqg1.add_cluster(qc)
202
198
  qc2 = self.cqg1.get_cluster("T23_1_2")
203
199
  self.cqg1.add_dependency(qc2, qc)
@@ -35,8 +35,7 @@ from pathlib import Path
35
35
 
36
36
  from lsst.ctrl.bps import BpsConfig, BpsSubprocessError, ClusteredQuantumGraph
37
37
  from lsst.ctrl.bps.pre_transform import cluster_quanta, create_quantum_graph, execute, update_quantum_graph
38
- from lsst.daf.butler import DimensionUniverse
39
- from lsst.pipe.base import QuantumGraph
38
+ from lsst.pipe.base.tests.mocks import InMemoryRepo
40
39
 
41
40
  TESTDIR = os.path.abspath(os.path.dirname(__file__))
42
41
  _LOG = logging.getLogger(__name__)
@@ -82,7 +81,7 @@ class TestCreatingQuantumGraph(unittest.TestCase):
82
81
  "submitPath": self.tmpdir,
83
82
  "whenSaveJobQgraph": "NEVER",
84
83
  "uniqProcName": "my_test",
85
- "qgraphFileTemplate": "{uniqProcName}.qgraph",
84
+ "qgraphFileTemplate": "{uniqProcName}.qg",
86
85
  }
87
86
  self.logger = logging.getLogger("lsst.ctrl.bps")
88
87
 
@@ -125,8 +124,8 @@ class TestUpdatingQuantumGraph(unittest.TestCase):
125
124
  "submitPath": self.tmpdir,
126
125
  "whenSaveJobQgraph": "NEVER",
127
126
  "uniqProcName": "my_test",
128
- "qgraphFileTemplate": "{uniqProcName}.qgraph",
129
- "inputQgraphFile": f"{self.tmpdir}/src.qgraph",
127
+ "qgraphFileTemplate": "{uniqProcName}.qg",
128
+ "inputQgraphFile": f"{self.tmpdir}/src.qg",
130
129
  }
131
130
  self.logger = logging.getLogger("lsst.ctrl.bps")
132
131
 
@@ -195,7 +194,7 @@ class TestClusterQuanta(unittest.TestCase):
195
194
  "validateClusteredQgraph": True,
196
195
  }
197
196
  config = BpsConfig(settings, search_order=[])
198
- qgraph = QuantumGraph({}, universe=DimensionUniverse())
197
+ qgraph = InMemoryRepo().make_quantum_graph()
199
198
  with self.assertRaisesRegex(RuntimeError, "Fake error"):
200
199
  _ = cluster_quanta(config, qgraph, "a_name")
201
200
 
@@ -209,7 +208,7 @@ class TestClusterQuanta(unittest.TestCase):
209
208
  "validateClusteredQgraph": False,
210
209
  }
211
210
  config = BpsConfig(settings, search_order=[])
212
- qgraph = QuantumGraph({}, universe=DimensionUniverse())
211
+ qgraph = InMemoryRepo().make_quantum_graph()
213
212
  _ = cluster_quanta(config, qgraph, "a_name")
214
213
 
215
214
 
@@ -1,2 +0,0 @@
1
- __all__ = ["__version__"]
2
- __version__ = "29.2025.3900"