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.
- lsst/ctrl/bps/bps_reports.py +131 -43
- lsst/ctrl/bps/bps_utils.py +1 -1
- lsst/ctrl/bps/clustered_quantum_graph.py +48 -61
- lsst/ctrl/bps/drivers.py +3 -3
- lsst/ctrl/bps/etc/bps_defaults.yaml +2 -2
- lsst/ctrl/bps/pre_transform.py +18 -8
- lsst/ctrl/bps/quantum_clustering_funcs.py +96 -83
- lsst/ctrl/bps/report.py +38 -19
- lsst/ctrl/bps/transform.py +6 -18
- lsst/ctrl/bps/version.py +1 -1
- {lsst_ctrl_bps-29.2025.3800.dist-info → lsst_ctrl_bps-29.2025.4000.dist-info}/METADATA +1 -1
- {lsst_ctrl_bps-29.2025.3800.dist-info → lsst_ctrl_bps-29.2025.4000.dist-info}/RECORD +20 -20
- {lsst_ctrl_bps-29.2025.3800.dist-info → lsst_ctrl_bps-29.2025.4000.dist-info}/WHEEL +0 -0
- {lsst_ctrl_bps-29.2025.3800.dist-info → lsst_ctrl_bps-29.2025.4000.dist-info}/entry_points.txt +0 -0
- {lsst_ctrl_bps-29.2025.3800.dist-info → lsst_ctrl_bps-29.2025.4000.dist-info}/licenses/COPYRIGHT +0 -0
- {lsst_ctrl_bps-29.2025.3800.dist-info → lsst_ctrl_bps-29.2025.4000.dist-info}/licenses/LICENSE +0 -0
- {lsst_ctrl_bps-29.2025.3800.dist-info → lsst_ctrl_bps-29.2025.4000.dist-info}/licenses/bsd_license.txt +0 -0
- {lsst_ctrl_bps-29.2025.3800.dist-info → lsst_ctrl_bps-29.2025.4000.dist-info}/licenses/gpl-v3.0.txt +0 -0
- {lsst_ctrl_bps-29.2025.3800.dist-info → lsst_ctrl_bps-29.2025.4000.dist-info}/top_level.txt +0 -0
- {lsst_ctrl_bps-29.2025.3800.dist-info → lsst_ctrl_bps-29.2025.4000.dist-info}/zip-safe +0 -0
lsst/ctrl/bps/bps_reports.py
CHANGED
|
@@ -27,7 +27,14 @@
|
|
|
27
27
|
|
|
28
28
|
"""Classes and functions used in reporting run status."""
|
|
29
29
|
|
|
30
|
-
__all__ = [
|
|
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
|
|
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
|
|
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
|
-
|
|
238
|
-
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
268
|
+
missing_labels = set()
|
|
255
269
|
for label in labels:
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
|
|
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) ->
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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):
|
lsst/ctrl/bps/bps_utils.py
CHANGED
|
@@ -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}.
|
|
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
|
-
|
|
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
|
|
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
|
|
83
|
-
|
|
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
|
-
|
|
88
|
-
|
|
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 =
|
|
98
|
-
|
|
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"] =
|
|
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(
|
|
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 : `
|
|
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.
|
|
189
|
-
The
|
|
182
|
+
qgraph : `lsst.pipe.base.quantum_graph.PredictedQuantumGraph`
|
|
183
|
+
The quantum graph to be clustered.
|
|
190
184
|
qgraph_filename : `str`
|
|
191
|
-
Filename for given
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
290
|
-
"""Retrieve a
|
|
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_ : `
|
|
295
|
-
ID of the
|
|
293
|
+
id_ : `uuid.UUID`
|
|
294
|
+
ID of the quantum to retrieve.
|
|
296
295
|
|
|
297
296
|
Returns
|
|
298
297
|
-------
|
|
299
|
-
|
|
300
|
-
|
|
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
|
|
305
|
+
a quantum with given ID.
|
|
307
306
|
"""
|
|
308
|
-
|
|
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
|
-
|
|
418
|
-
|
|
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
|
-
|
|
510
|
-
cgraph.
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
cgraph._quantum_graph =
|
|
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
|
|
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,
|
|
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.
|
|
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.
|
|
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}.
|
|
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
|
|
lsst/ctrl/bps/pre_transform.py
CHANGED
|
@@ -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
|
|
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,
|
|
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.
|
|
66
|
-
A
|
|
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
|
-
|
|
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.
|
|
248
|
-
Original full
|
|
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
|
|
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(
|
|
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.
|
|
55
|
-
|
|
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
|
|
80
|
-
|
|
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":
|
|
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":
|
|
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[
|
|
103
|
+
cached_template[task_label] = template
|
|
100
104
|
|
|
101
|
-
cluster = QuantaCluster.
|
|
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[
|
|
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
|
|
113
|
+
for quantum_id in cqgraph.qxgraph:
|
|
110
114
|
# Get child nodes.
|
|
111
|
-
children =
|
|
115
|
+
children = cqgraph.qxgraph.successors(quantum_id)
|
|
112
116
|
for child in children:
|
|
113
|
-
cqgraph.add_dependency(number_to_name[
|
|
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(
|
|
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.
|
|
220
|
-
|
|
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:
|
|
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.
|
|
280
|
-
|
|
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
|
-
|
|
308
|
-
|
|
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[
|
|
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:
|
|
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
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
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
|
-
|
|
557
|
-
parents =
|
|
558
|
-
for
|
|
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[
|
|
561
|
-
cqgraph.add_dependency(quantum_to_cluster[
|
|
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
|
-
|
|
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
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
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:
|
|
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
|
-
|
|
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.
|
|
595
|
-
|
|
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 =
|
|
621
|
+
find_possible_nodes = cqgraph.qxgraph.successors
|
|
622
622
|
case "sink":
|
|
623
|
-
find_possible_nodes =
|
|
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
|
-
|
|
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
|
|
639
|
+
if quantum_id in quanta_visited:
|
|
642
640
|
continue
|
|
643
641
|
|
|
644
|
-
cluster_name, info =
|
|
645
|
-
|
|
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(
|
|
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 = [
|
|
661
|
+
nodes_to_use = [quantum_id]
|
|
658
662
|
while nodes_to_use:
|
|
659
663
|
node_to_use = nodes_to_use.pop()
|
|
660
|
-
|
|
661
|
-
for
|
|
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
|
|
667
|
+
if possible_node_id in quanta_visited:
|
|
664
668
|
continue
|
|
665
|
-
quanta_visited.add(
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
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(
|
|
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
|
-
|
|
679
|
-
|
|
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
|
|
750
|
-
|
|
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
|
-
|
|
762
|
-
|
|
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":
|
|
784
|
-
"node_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
|
-
|
|
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 {
|
|
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
|
|
42
|
-
|
|
43
|
-
from .bps_reports import
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
lsst/ctrl/bps/transform.py
CHANGED
|
@@ -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
|
-
|
|
675
|
+
quantum_info = cqgraph.get_quantum_info(node_id)
|
|
686
676
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
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.
|
|
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.
|
|
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=
|
|
8
|
-
lsst/ctrl/bps/bps_utils.py,sha256
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
20
|
-
lsst/ctrl/bps/report.py,sha256=
|
|
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=
|
|
25
|
-
lsst/ctrl/bps/version.py,sha256=
|
|
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=
|
|
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.
|
|
39
|
-
lsst_ctrl_bps-29.2025.
|
|
40
|
-
lsst_ctrl_bps-29.2025.
|
|
41
|
-
lsst_ctrl_bps-29.2025.
|
|
42
|
-
lsst_ctrl_bps-29.2025.
|
|
43
|
-
lsst_ctrl_bps-29.2025.
|
|
44
|
-
lsst_ctrl_bps-29.2025.
|
|
45
|
-
lsst_ctrl_bps-29.2025.
|
|
46
|
-
lsst_ctrl_bps-29.2025.
|
|
47
|
-
lsst_ctrl_bps-29.2025.
|
|
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,,
|
|
File without changes
|
{lsst_ctrl_bps-29.2025.3800.dist-info → lsst_ctrl_bps-29.2025.4000.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{lsst_ctrl_bps-29.2025.3800.dist-info → lsst_ctrl_bps-29.2025.4000.dist-info}/licenses/COPYRIGHT
RENAMED
|
File without changes
|
{lsst_ctrl_bps-29.2025.3800.dist-info → lsst_ctrl_bps-29.2025.4000.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|
{lsst_ctrl_bps-29.2025.3800.dist-info → lsst_ctrl_bps-29.2025.4000.dist-info}/licenses/gpl-v3.0.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|