lsst-ctrl-bps 29.1.0rc5__tar.gz → 30.0.0rc3__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 (74) hide show
  1. {lsst_ctrl_bps-29.1.0rc5/python/lsst_ctrl_bps.egg-info → lsst_ctrl_bps-30.0.0rc3}/PKG-INFO +3 -3
  2. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3}/doc/lsst.ctrl.bps/CHANGES.rst +30 -0
  3. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3}/doc/lsst.ctrl.bps/quickstart.rst +103 -24
  4. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3}/pyproject.toml +3 -3
  5. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3}/python/lsst/ctrl/bps/bps_config.py +61 -0
  6. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3}/python/lsst/ctrl/bps/bps_reports.py +131 -43
  7. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3}/python/lsst/ctrl/bps/bps_utils.py +35 -1
  8. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3}/python/lsst/ctrl/bps/clustered_quantum_graph.py +48 -61
  9. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3}/python/lsst/ctrl/bps/construct.py +108 -5
  10. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3}/python/lsst/ctrl/bps/drivers.py +32 -17
  11. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3}/python/lsst/ctrl/bps/etc/bps_defaults.yaml +9 -3
  12. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3}/python/lsst/ctrl/bps/generic_workflow.py +12 -0
  13. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3}/python/lsst/ctrl/bps/initialize.py +3 -0
  14. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3}/python/lsst/ctrl/bps/pre_transform.py +18 -8
  15. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3}/python/lsst/ctrl/bps/quantum_clustering_funcs.py +96 -83
  16. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3}/python/lsst/ctrl/bps/report.py +45 -24
  17. lsst_ctrl_bps-30.0.0rc3/python/lsst/ctrl/bps/tests/config_test_utils.py +114 -0
  18. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3}/python/lsst/ctrl/bps/transform.py +7 -25
  19. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3}/python/lsst/ctrl/bps/version.py +1 -1
  20. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3/python/lsst_ctrl_bps.egg-info}/PKG-INFO +3 -3
  21. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3}/python/lsst_ctrl_bps.egg-info/SOURCES.txt +3 -0
  22. lsst_ctrl_bps-29.1.0rc5/tests/test_report.py → lsst_ctrl_bps-30.0.0rc3/tests/test_bps_reports.py +118 -39
  23. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3}/tests/test_bps_utils.py +12 -1
  24. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3}/tests/test_bpsconfig.py +133 -1
  25. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3}/tests/test_clustered_quantum_graph.py +18 -22
  26. lsst_ctrl_bps-30.0.0rc3/tests/test_construct.py +371 -0
  27. lsst_ctrl_bps-30.0.0rc3/tests/test_drivers.py +329 -0
  28. lsst_ctrl_bps-30.0.0rc3/tests/test_initialize.py +180 -0
  29. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3}/tests/test_pre_transform.py +8 -7
  30. lsst_ctrl_bps-30.0.0rc3/tests/test_report.py +72 -0
  31. lsst_ctrl_bps-29.1.0rc5/tests/test_construct.py +0 -122
  32. lsst_ctrl_bps-29.1.0rc5/tests/test_drivers.py +0 -146
  33. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3}/COPYRIGHT +0 -0
  34. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3}/LICENSE +0 -0
  35. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3}/MANIFEST.in +0 -0
  36. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3}/README.md +0 -0
  37. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3}/bsd_license.txt +0 -0
  38. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3}/doc/lsst.ctrl.bps/index.rst +0 -0
  39. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3}/gpl-v3.0.txt +0 -0
  40. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3}/python/lsst/__init__.py +0 -0
  41. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3}/python/lsst/ctrl/__init__.py +0 -0
  42. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3}/python/lsst/ctrl/bps/__init__.py +0 -0
  43. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3}/python/lsst/ctrl/bps/_exceptions.py +0 -0
  44. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3}/python/lsst/ctrl/bps/bps_draw.py +0 -0
  45. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3}/python/lsst/ctrl/bps/cancel.py +0 -0
  46. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3}/python/lsst/ctrl/bps/cli/__init__.py +0 -0
  47. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3}/python/lsst/ctrl/bps/cli/bps.py +0 -0
  48. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3}/python/lsst/ctrl/bps/cli/cmd/__init__.py +0 -0
  49. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3}/python/lsst/ctrl/bps/cli/cmd/commands.py +0 -0
  50. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3}/python/lsst/ctrl/bps/cli/opt/__init__.py +0 -0
  51. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3}/python/lsst/ctrl/bps/cli/opt/arguments.py +0 -0
  52. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3}/python/lsst/ctrl/bps/cli/opt/option_groups.py +0 -0
  53. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3}/python/lsst/ctrl/bps/cli/opt/options.py +0 -0
  54. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3}/python/lsst/ctrl/bps/constants.py +0 -0
  55. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3}/python/lsst/ctrl/bps/ping.py +0 -0
  56. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3}/python/lsst/ctrl/bps/prepare.py +0 -0
  57. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3}/python/lsst/ctrl/bps/restart.py +0 -0
  58. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3}/python/lsst/ctrl/bps/status.py +0 -0
  59. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3}/python/lsst/ctrl/bps/submit.py +0 -0
  60. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3}/python/lsst/ctrl/bps/tests/gw_test_utils.py +0 -0
  61. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3}/python/lsst/ctrl/bps/wms_service.py +0 -0
  62. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3}/python/lsst_ctrl_bps.egg-info/dependency_links.txt +0 -0
  63. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3}/python/lsst_ctrl_bps.egg-info/entry_points.txt +0 -0
  64. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3}/python/lsst_ctrl_bps.egg-info/requires.txt +0 -0
  65. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3}/python/lsst_ctrl_bps.egg-info/top_level.txt +0 -0
  66. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3}/python/lsst_ctrl_bps.egg-info/zip-safe +0 -0
  67. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3}/setup.cfg +0 -0
  68. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3}/tests/test_cli_commands.py +0 -0
  69. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3}/tests/test_generic_workflow.py +0 -0
  70. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3}/tests/test_ping.py +0 -0
  71. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3}/tests/test_quantum_clustering_funcs.py +0 -0
  72. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3}/tests/test_status.py +0 -0
  73. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3}/tests/test_transform.py +0 -0
  74. {lsst_ctrl_bps-29.1.0rc5 → lsst_ctrl_bps-30.0.0rc3}/tests/test_wms_service.py +0 -0
@@ -1,18 +1,18 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lsst-ctrl-bps
3
- Version: 29.1.0rc5
3
+ Version: 30.0.0rc3
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
- License: BSD 3-Clause License
6
+ License-Expression: BSD-3-Clause OR GPL-3.0-or-later
7
7
  Project-URL: Homepage, https://github.com/lsst/ctrl_bps
8
8
  Keywords: lsst
9
9
  Classifier: Intended Audience :: Science/Research
10
- Classifier: License :: OSI Approved :: BSD License
11
10
  Classifier: Operating System :: OS Independent
12
11
  Classifier: Programming Language :: Python :: 3
13
12
  Classifier: Programming Language :: Python :: 3.11
14
13
  Classifier: Programming Language :: Python :: 3.12
15
14
  Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
16
  Classifier: Topic :: Scientific/Engineering :: Astronomy
17
17
  Requires-Python: >=3.11.0
18
18
  Description-Content-Type: text/markdown
@@ -1,3 +1,33 @@
1
+ lsst-ctrl-bps v30.0.0 (2026-01-16)
2
+ ==================================
3
+
4
+ New Features
5
+ ------------
6
+
7
+ - Added support for transferring input files to the execution site and bringing the produced output files back to the submit site when using ``bps submitcmd``. (`DM-48479 <https://rubinobs.atlassian.net/browse/DM-48479>`_)
8
+ - Added ``bpsGenerateConfig`` and ``bpsEval`` to run functions to produce config values. ``bpsGenerateConfig`` is used when inserting or updating one or more key/value pairs. ``bpsEval`` is for replacing part of a string value. (`DM-50616 <https://rubinobs.atlassian.net/browse/DM-50616>`_)
9
+
10
+ Bug Fixes
11
+ ---------
12
+
13
+ - Fixed bug where the ``return_exit_codes`` command line value was not passed to the plugin's report function. While it was correctly used when displaying the report, not having the value didn't allow the plugin to optimize its report function. (`DM-52791 <https://rubinobs.atlassian.net/browse/DM-52791>`_)
14
+
15
+ Performance Enhancement
16
+ -----------------------
17
+
18
+ - Switched the default ``finalJob`` implementation to the new ``aggregate-graph`` command, which makes use of multiple cores much more effectively than ``transfer-from-graph``. (`DM-52360 <https://rubinobs.atlassian.net/browse/DM-52360>`_)
19
+
20
+
21
+ Other Changes and Additions
22
+ ---------------------------
23
+
24
+ - Removed exit code 1 from the default for ``finalJob``\ 's ``retryUnlessExit`` as the majority of these are related to system issues which could be transient. (`DM-51313 <https://rubinobs.atlassian.net/browse/DM-51313>`_)
25
+ - Modified the BPS report driver so it compiles exit code summary only when necessary, i.e., when ``--return-exit-codes`` option was used with ``bps report``. (`DM-52898 <https://rubinobs.atlassian.net/browse/DM-52898>`_)
26
+ - Added a custom __new__ method to the GenericWorkflow class so **ctrl_bps** can work when using NetworkX 3.6. (`DM-53492 <https://rubinobs.atlassian.net/browse/DM-53492>`_)
27
+ - Made the BPS reporting mechanism a bit more robust. If the BPS plugin does not explicitly includes jobs labels for which there are no failures in the run's exit code summary, it will try to use the run's job summary to do that. (`DM-51261 <https://rubinobs.atlassian.net/browse/DM-51261>`_)
28
+ - Used the new ``PredictedQuantumGraph`` class internally and default to the new QG file format via the "``.qg``" extension. (`DM-52339 <https://rubinobs.atlassian.net/browse/DM-52339>`_)
29
+
30
+
1
31
  lsst-ctrl-bps v29.1.0 (2025-06-13)
2
32
  ==================================
3
33
 
@@ -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
 
@@ -265,7 +265,7 @@ arguments, e.g.:
265
265
  .. code-block:: yaml
266
266
 
267
267
  customJob:
268
- executable: "${HOME}/scripts/sleep.sh"
268
+ executable: "${HOME}/scripts/do_stuff.sh"
269
269
  arguments: "2"
270
270
 
271
271
  # Uncomment settings below to disable automatic memory scaling and retries
@@ -276,16 +276,62 @@ arguments, e.g.:
276
276
 
277
277
  where ``executable`` specifies the path to the executable to run and
278
278
  ``arguments`` is a list of arguments to be supplied to the executable as part
279
- of the command line.
279
+ of the command line. If your executable does not take any command line
280
+ arguments set ``arguments`` to an empty string.
280
281
 
281
282
  .. note::
282
283
 
283
- If your executable does not take any command line arguments set
284
- ``arguments`` to an empty string.
284
+ The script specified by ``customJob.executable`` is copied to the run's
285
+ submit directory and this copy (not the original script) is being submitted
286
+ for execution. As a result, making any changes to the original script after
287
+ the run has been submitted will have no effect even if the run is still
288
+ in the WMS work queue waiting for execution.
289
+
290
+ If the script requires any input files that should be transferred to the
291
+ execution site as well and/or produces output files that should be brought back
292
+ specify them as follows:
293
+
294
+ .. code-block:: yaml
295
+
296
+ customJob:
297
+ executable: "${HOME}/scripts/do_stuff.sh"
298
+ arguments: "-o {outfile} {infile}"
299
+ inputs:
300
+ infile: path/to/input/file
301
+ outputs:
302
+ outfile: path/to/output/file
303
+
304
+ # Uncomment settings below to disable automatic memory scaling and retries
305
+ # which BPS enables by default.
306
+ #
307
+ # memoryMultiplier: 1
308
+ # numberOfRetries: 1
309
+
310
+ The paths in ``inputs`` specify files as they are accessed on the submit site.
311
+ They can be absolute or relative to the current working directory as the run is
312
+ submitted. The input files will be copied to the run's submit directory.
313
+ These copies (not the original files) will be submitted along with the script's
314
+ copy for execution. During the execution BPS will transfer these copies into a
315
+ single flat directory -- job's scratch directory on the execute machine.
316
+
317
+ The paths in ``outputs`` specifies the paths on the submit site the output
318
+ files will be copied to after job's completion. As input paths they can be
319
+ either absolute or relative. However, on the execution site, the script is
320
+ expected to write all its output files directly to job's scratch directory
321
+ (assumed to be the current working directory when the job starts unless stated
322
+ otherwise in the WMS-specific documentation).
323
+
324
+ As a result, both input and output files base names *must* be unique.
325
+
326
+ .. note::
327
+
328
+ Currently, BPS itself doesn't verify if the file declared in ``outputs`` was
329
+ produced by the script. Whether a missing output file will be considered an
330
+ error depends entirely on WMS in use.
285
331
 
286
- This config file will instruct BPS to create a special single-job *workflow* to
287
- run your script. That workflow will be submitted for execution as any other
288
- workflow.
332
+ The config files shown above will instruct BPS to create a special single-job
333
+ *workflow* to run your script. That workflow will be submitted for execution
334
+ as any other workflow.
289
335
 
290
336
  As a result, the submission process for a custom script looks quite similar
291
337
  to the submission process of regular payload jobs (i.e. jobs running
@@ -305,15 +351,6 @@ There are few things you need to keep in mind though:
305
351
  instructions exist in the submit YAML. If you need the quantum graph, use
306
352
  ``bps submit``.
307
353
 
308
- #. At the moment, the mechanism does not support transferring files other than
309
- executable.
310
-
311
- #. The script specified by ``customJob.executable`` is copied to the run's
312
- submit directory and this copy (not the original script) is being submitted
313
- for execution. As a result, making any changes to the original script after
314
- the run has been submitted will have no effect even if the run is still in
315
- the WMS work queue waiting for execution.
316
-
317
354
  #. Some BPS plugins may require inclusion of plugin-specific settings for this
318
355
  mechanism to work. Consult the documentation of the plugin you use for
319
356
  details.
@@ -809,7 +846,7 @@ Supported settings
809
846
  When to output job QuantumGraph files (default = TRANSFORM).
810
847
 
811
848
  * NEVER = all jobs will use full QuantumGraph file. (Warning: make sure
812
- runQuantumCommand has ``--qgraph-id {qgraphId} --qgraph-node-id {qgraphNodeId}``.)
849
+ runQuantumCommand has ``--qgraph-node-id {qgraphNodeId}``.)
813
850
  * TRANSFORM = Output QuantumGraph files after creating GenericWorkflow.
814
851
  * PREPARE = QuantumGraph files are output after creating WMS submission.
815
852
 
@@ -867,7 +904,7 @@ Reserved keywords
867
904
  However, contrary to YAML specification, it is currently not portable.
868
905
 
869
906
  **qgraphId**
870
- Internal ID for the full QuantumGraph (passed as ``--qgraph-id`` on pipetask command line).
907
+ Ignored; accepted for backwards compatibility.
871
908
 
872
909
  **qgraphNodeId**
873
910
  Comma-separated list of internal QuantumGraph node numbers to be
@@ -1042,13 +1079,12 @@ single full QuantumGraph file plus node numbers for each job. The default is
1042
1079
  using per-job QuantumGraph files.
1043
1080
 
1044
1081
  To use full QuantumGraph file, the submit YAML must set ``whenSaveJobQgraph`` to
1045
- "NEVER" and the ``pipetask run`` command must include ``--qgraph-id {qgraphId}
1046
- --qgraph-node-id {qgraphNodeId}``. For example:
1082
+ "NEVER" and the ``pipetask run`` command must include ``--qgraph-node-id {qgraphNodeId}``. For example:
1047
1083
 
1048
1084
  .. code::
1049
1085
 
1050
1086
  whenSaveJobQgraph: "NEVER"
1051
- 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"
1052
1088
 
1053
1089
 
1054
1090
  .. warning::
@@ -1125,7 +1161,7 @@ New YAML Section
1125
1161
  implementation: JOB
1126
1162
  concurrencyLimit: db_limit
1127
1163
  command1: >-
1128
- ${DAF_BUTLER_DIR}/bin/butler transfer-from-graph
1164
+ ${DAF_BUTLER_DIR}/bin/butler aggregate-graph
1129
1165
  {fileDistributionEndPoint}{qgraphFile}
1130
1166
  {butlerConfig}
1131
1167
  --register-dataset-types
@@ -1207,7 +1243,7 @@ The major differences to users are:
1207
1243
  the output run in the provided pre-existing quantum graph.
1208
1244
  - ``final_post_finalJob.out``: An internal file for debugging incorrect
1209
1245
  reporting of final run status.
1210
- - ``<qgraph_filename>_orig.qgraph``: A backup copy of the original
1246
+ - ``<qgraph_filename>_orig.qg``: A backup copy of the original
1211
1247
  pre-existing quantum graph file that was used for submitting the run. Note
1212
1248
  that this file will *not* be present in the submit directory if the
1213
1249
  pipeline YAML specification was used during the submission instead.
@@ -1497,6 +1533,49 @@ invisible to the user. ``bps report`` will still show same labels and
1497
1533
  total counts as without ordering. ``cancel`` and ``restart`` will still
1498
1534
  work the same.
1499
1535
 
1536
+ .. _bps-config-generation:
1537
+
1538
+ Config Generation
1539
+ -----------------
1540
+
1541
+ In some rare use cases, the submit yaml depends upon what happened in
1542
+ previous runs (e.g., passing pipeline configuration values to the HiPS
1543
+ QuantumGraph generation command depending upon colors of generated outputs
1544
+ of previous run). One can wait until a run finishes, query the results,
1545
+ and then manually modify the submit yaml for the next run. To help make
1546
+ this easier to automate, two special mechanisms, ``bpsGenerateConfig`` and
1547
+ ``bpsEval`` have been added to ``bps``. While different syntax, both take
1548
+ two pieces of information. The first piece describes what to import and
1549
+ execute and the second the parameters to pass which typically will
1550
+ be config variables (e.g., "{butlerConfig}").
1551
+
1552
+ ``bpsGenerateConfig`` is a key/value pair where the function returns a
1553
+ Mapping to update the config. It can be used at the root level and can
1554
+ return nested dictionaries to replace values across multiple sections.
1555
+ It can also be used inside sections (e.g., inside a specific pipetask
1556
+ section). The function is not run when loading the config. Instead it
1557
+ is run during the initialization in the ``bps submit`` (before saving
1558
+ the config yaml to the submit directory and before running QuantumGraph
1559
+ generation). Example:
1560
+
1561
+ ..code::
1562
+
1563
+ bpsGenerateConfig: "lsst.my.package.my_func_1('{butlerConfig}', param3='{output}')"
1564
+
1565
+
1566
+ ``bpsEval`` is a placeholder in an submit yaml value. It is executed when
1567
+ the corresponding key is requested from the config. Its function needs
1568
+ to return a value whose string representation can replace ``bpsEval``. Example:
1569
+
1570
+ ..code::
1571
+
1572
+ extraQgraphOptions: "--dataset-query-constraint off bpsEval(lsst.my.package.my_func_2, '{butlerConfig}', '{output}')
1573
+
1574
+ .. warning::
1575
+
1576
+ Quotes must be placed around variables that return strings (e.g., '{butlerConfig}').
1577
+ Forgetting the quotes typically results in an invalid syntax error.
1578
+
1500
1579
  .. _bps-softlink:
1501
1580
 
1502
1581
  WMS-id softlink
@@ -6,19 +6,20 @@ build-backend = "setuptools.build_meta"
6
6
  name = "lsst-ctrl-bps"
7
7
  requires-python = ">=3.11.0"
8
8
  description = "Pluggable execution of workflow graphs from Rubin pipelines."
9
- license = {text = "BSD 3-Clause License"}
9
+ license = "BSD-3-Clause OR GPL-3.0-or-later"
10
+ license-files = ["COPYRIGHT", "LICENSE", "bsd_license.txt", "gpl-v3.0.txt"]
10
11
  readme = "README.md"
11
12
  authors = [
12
13
  {name="Rubin Observatory Data Management", email="dm-admin@lists.lsst.org"},
13
14
  ]
14
15
  classifiers = [
15
16
  "Intended Audience :: Science/Research",
16
- "License :: OSI Approved :: BSD License",
17
17
  "Operating System :: OS Independent",
18
18
  "Programming Language :: Python :: 3",
19
19
  "Programming Language :: Python :: 3.11",
20
20
  "Programming Language :: Python :: 3.12",
21
21
  "Programming Language :: Python :: 3.13",
22
+ "Programming Language :: Python :: 3.14",
22
23
  "Topic :: Scientific/Engineering :: Astronomy",
23
24
  ]
24
25
  keywords = ["lsst"]
@@ -49,7 +50,6 @@ where = ["python"]
49
50
 
50
51
  [tool.setuptools]
51
52
  zip-safe = true
52
- license-files = ["COPYRIGHT", "LICENSE", "bsd_license.txt", "gpl-v3.0.txt"]
53
53
 
54
54
  [tool.setuptools.package-data]
55
55
  "lsst.ctrl.bps" = ["etc/*.yaml"]
@@ -44,6 +44,8 @@ from lsst.daf.butler import Config
44
44
  from lsst.resources import ResourcePath
45
45
  from lsst.utils import doImport
46
46
 
47
+ from .bps_utils import bps_eval
48
+
47
49
  _LOG = logging.getLogger(__name__)
48
50
 
49
51
  # Using lsst.daf.butler.Config to resolve possible includes.
@@ -417,4 +419,63 @@ class BpsConfig(Config):
417
419
  if default != _NO_SEARCH_DEFAULT_VALUE:
418
420
  opt["default"] = default
419
421
 
422
+ # check for bpsEval
423
+ value = re.sub(
424
+ r"bpsEval\(([^,)]+), ([^)]+)\)", lambda m: str(bps_eval(m.group(1), m.group(2))), value
425
+ )
426
+ if "bpsEval" in value:
427
+ raise ValueError(f"Unparsable bpsEval in '{value}'")
428
+
420
429
  return value
430
+
431
+ def generate_config(self) -> None:
432
+ """Update config with values generated by bpsGenerateConfig
433
+ entries.
434
+ """
435
+ _LOG.debug("generate_config before: %s", self)
436
+ self._recursive_generate_config("", self)
437
+ _LOG.debug("generate_config after: %s", self)
438
+
439
+ def _recursive_generate_config(self, recursive_key: str, sub_config: Config) -> None:
440
+ """Update config with values generated by bpsGenerateConfig
441
+ entries.
442
+
443
+ Parameters
444
+ ----------
445
+ recursive_key : `str`
446
+ Corresponds to a new subconfig in which to search
447
+ and replace bpsGenerateConfig.
448
+
449
+ sub_config : `lsst.daf.butler.Config`
450
+ The nested config corresponding to the recursive_key.
451
+
452
+ Raises
453
+ ------
454
+ ValueError
455
+ If bpsGenerateConfig value isn't parseable.
456
+ ImportError
457
+ If problems importing bpsGenerateConfig's method.
458
+ """
459
+ _LOG.debug("recursive_key = '%s'", recursive_key)
460
+ genkey = "bpsGenerateConfig" # to make it easier to change
461
+
462
+ # Save to avoid dictionary changed size during iteration error.
463
+ orig_keys = list(sub_config)
464
+ for key in orig_keys:
465
+ value = Config.__getitem__(sub_config, key)
466
+ _LOG.debug("key = %s, type(value) = %s", key, type(value))
467
+ if isinstance(value, Config):
468
+ self._recursive_generate_config(f"{recursive_key}.{key}", value)
469
+ elif key == genkey:
470
+ value = self.replace_vars(value, {"searchobj": sub_config})
471
+
472
+ m = re.match(r"(\S+)\((.+)\)", value)
473
+ if m:
474
+ results = bps_eval(m.group(1), m.group(2))
475
+ del sub_config[genkey]
476
+ sub_config.update(results)
477
+ if recursive_key:
478
+ self[recursive_key] = sub_config
479
+ _LOG.debug("After config = %s", self)
480
+ else:
481
+ raise ValueError(f"Unparsable {genkey} value='{value}'")
@@ -27,7 +27,14 @@
27
27
 
28
28
  """Classes and functions used in reporting run status."""
29
29
 
30
- __all__ = ["BaseRunReport", "DetailedRunReport", "ExitCodesReport", "SummaryRunReport", "compile_job_summary"]
30
+ __all__ = [
31
+ "BaseRunReport",
32
+ "DetailedRunReport",
33
+ "ExitCodesReport",
34
+ "SummaryRunReport",
35
+ "compile_code_summary",
36
+ "compile_job_summary",
37
+ ]
31
38
 
32
39
  import abc
33
40
  import logging
@@ -55,7 +62,7 @@ class BaseRunReport(abc.ABC):
55
62
 
56
63
  def __eq__(self, other):
57
64
  if isinstance(other, BaseRunReport):
58
- return all(self._table == other._table)
65
+ return self._table.pformat() == other._table.pformat()
59
66
  return False
60
67
 
61
68
  def __len__(self):
@@ -195,7 +202,7 @@ class DetailedRunReport(BaseRunReport):
195
202
  job_summary = run_report.job_summary
196
203
  if job_summary is None:
197
204
  id_ = run_report.global_wms_id if use_global_id else run_report.wms_id
198
- self._msg = f"WARNING: Job summary for run '{id_}' not available, report maybe incomplete."
205
+ self._msg = f"WARNING: Job summary for run '{id_}' not available, report may be incomplete."
199
206
  return
200
207
 
201
208
  if by_label_expected:
@@ -231,44 +238,60 @@ class ExitCodesReport(BaseRunReport):
231
238
  error handling from the wms service.
232
239
  """
233
240
 
234
- def add(self, run_report, use_global_id=False):
241
+ def add(self, run_report: WmsRunReport, use_global_id: bool = False) -> None:
235
242
  # Docstring inherited from the base class.
236
243
 
237
- # Use label ordering from the run summary as it should reflect
238
- # the ordering of the pipetasks in the pipeline.
244
+ exit_code_summary = run_report.exit_code_summary
245
+ if not exit_code_summary:
246
+ id_ = run_report.global_wms_id if use_global_id else run_report.wms_id
247
+ self._msg = f"WARNING: Exit code summary for run '{id_}' not available, report may be incomplete."
248
+ return
249
+
250
+ warnings = []
251
+
252
+ # If available, use label ordering from the run summary as it should
253
+ # reflect the ordering of the pipetasks in the pipeline.
239
254
  labels = []
240
255
  if run_report.run_summary:
241
256
  for part in run_report.run_summary.split(";"):
242
257
  label, _ = part.split(":")
243
258
  labels.append(label)
244
- else:
245
- id_ = run_report.global_wms_id if use_global_id else run_report.wms_id
246
- self._msg = f"WARNING: Job summary for run '{id_}' not available, report maybe incomplete."
247
- return
259
+ if not labels:
260
+ labels = sorted(exit_code_summary)
261
+ warnings.append("WARNING: Could not determine order of pipeline, instead sorted alphabetically.")
248
262
 
249
263
  # Payload (e.g. pipetask) error codes:
250
264
  # * 1: general failure,
251
265
  # * 2: command line error (e.g. unknown command and/or option).
252
266
  pyld_error_codes = {1, 2}
253
267
 
254
- exit_code_summary = run_report.exit_code_summary
268
+ missing_labels = set()
255
269
  for label in labels:
256
- exit_codes = exit_code_summary[label]
257
-
258
- pyld_errors = [code for code in exit_codes if code in pyld_error_codes]
259
- pyld_error_count = len(pyld_errors)
260
- pyld_error_summary = (
261
- ", ".join(sorted(str(code) for code in set(pyld_errors))) if pyld_errors else "None"
262
- )
263
-
264
- infra_errors = [code for code in exit_codes if code not in pyld_error_codes]
265
- infra_error_count = len(infra_errors)
266
- infra_error_summary = (
267
- ", ".join(sorted(str(code) for code in set(infra_errors))) if infra_errors else "None"
270
+ try:
271
+ exit_codes = exit_code_summary[label]
272
+ except KeyError:
273
+ missing_labels.add(label)
274
+ else:
275
+ pyld_errors = [code for code in exit_codes if code in pyld_error_codes]
276
+ pyld_error_count = len(pyld_errors)
277
+ pyld_error_summary = (
278
+ ", ".join(sorted(str(code) for code in set(pyld_errors))) if pyld_errors else "None"
279
+ )
280
+
281
+ infra_errors = [code for code in exit_codes if code not in pyld_error_codes]
282
+ infra_error_count = len(infra_errors)
283
+ infra_error_summary = (
284
+ ", ".join(sorted(str(code) for code in set(infra_errors))) if infra_errors else "None"
285
+ )
286
+
287
+ run = [label, pyld_error_count, pyld_error_summary, infra_error_count, infra_error_summary]
288
+ self._table.add_row(run)
289
+ if missing_labels:
290
+ warnings.append(
291
+ f"WARNING: Exit code summary was not available for job labels: {', '.join(missing_labels)}"
268
292
  )
269
-
270
- run = [label, pyld_error_count, pyld_error_summary, infra_error_count, infra_error_summary]
271
- self._table.add_row(run)
293
+ if warnings:
294
+ self._msg = "\n".join(warnings)
272
295
 
273
296
  def __str__(self):
274
297
  alignments = ["<"] + [">"] * (len(self._table.colnames) - 1)
@@ -276,7 +299,7 @@ class ExitCodesReport(BaseRunReport):
276
299
  return str("\n".join(lines))
277
300
 
278
301
 
279
- def compile_job_summary(report: WmsRunReport) -> None:
302
+ def compile_job_summary(report: WmsRunReport) -> list[str]:
280
303
  """Add a job summary to the run report if necessary.
281
304
 
282
305
  If the job summary is not provided, the function will attempt to compile
@@ -289,24 +312,89 @@ def compile_job_summary(report: WmsRunReport) -> None:
289
312
  report : `lsst.ctrl.bps.WmsRunReport`
290
313
  Information about a single run.
291
314
 
292
- Raises
293
- ------
294
- ValueError
295
- Raised if the job summary *and* information about individual jobs
296
- is not available.
315
+ Returns
316
+ -------
317
+ warnings : `list` [`str`]
318
+ List of messages describing any non-critical issues encountered during
319
+ processing. Empty if none.
297
320
  """
321
+ warnings: list[str] = []
322
+
323
+ # If the job summary already exists, exit early.
298
324
  if report.job_summary:
299
- return
300
- if not report.jobs:
301
- raise ValueError("job summary cannot be compiled: information about individual jobs not available.")
302
- job_summary = {}
303
- by_label = group_jobs_by_label(report.jobs)
304
- for label, job_group in by_label.items():
305
- by_label_state = group_jobs_by_state(job_group)
306
- _LOG.debug("by_label_state = %s", by_label_state)
307
- counts = {state: len(jobs) for state, jobs in by_label_state.items()}
308
- job_summary[label] = counts
309
- report.job_summary = job_summary
325
+ return warnings
326
+
327
+ if report.jobs:
328
+ job_summary = {}
329
+ by_label = group_jobs_by_label(report.jobs)
330
+ for label, job_group in by_label.items():
331
+ by_label_state = group_jobs_by_state(job_group)
332
+ _LOG.debug("by_label_state = %s", by_label_state)
333
+ counts = {state: len(jobs) for state, jobs in by_label_state.items()}
334
+ job_summary[label] = counts
335
+ report.job_summary = job_summary
336
+ else:
337
+ warnings.append("information about individual jobs not available")
338
+
339
+ return warnings
340
+
341
+
342
+ def compile_code_summary(report: WmsRunReport) -> list[str]:
343
+ """Add missing entries to the exit code summary if necessary.
344
+
345
+ A WMS plugin may exclude job labels for which there are no failures from
346
+ the exit code summary. The function will attempt to use the job summary,
347
+ if available, to add missing entries for these labels.
348
+
349
+ Parameters
350
+ ----------
351
+ report : `lsst.ctrl.bps.WmsRunReport`
352
+ Information about a single run.
353
+
354
+ Returns
355
+ -------
356
+ warnings : `list` [`str`]
357
+ List of messages describing any non-critical issues encountered during
358
+ processing. Empty if none.
359
+ """
360
+ warnings: list[str] = []
361
+
362
+ # If the job summary is not available, exit early.
363
+ if not report.job_summary:
364
+ return warnings
365
+
366
+ # A shallow copy is enough here because we won't be modifying the existing
367
+ # entries, only adding new ones if necessary.
368
+ exit_code_summary = dict(report.exit_code_summary) if report.exit_code_summary else {}
369
+
370
+ # Use the job summary to add the entries for labels with no failures
371
+ # *without* modifying already existing entries.
372
+ failure_summary = {label: states[WmsStates.FAILED] for label, states in report.job_summary.items()}
373
+ for label, count in failure_summary.items():
374
+ if count == 0:
375
+ exit_code_summary.setdefault(label, [])
376
+
377
+ # Check if there are any discrepancies between the data in the exit code
378
+ # summary and the job summary.
379
+ code_summary_labels = set(exit_code_summary)
380
+ failure_summary_labels = set(failure_summary)
381
+ mismatches = {
382
+ label
383
+ for label in failure_summary_labels & code_summary_labels
384
+ if len(exit_code_summary[label]) != failure_summary[label]
385
+ }
386
+ if mismatches:
387
+ warnings.append(
388
+ f"number of exit codes differs from number of failures for job labels: {', '.join(mismatches)}"
389
+ )
390
+ missing = failure_summary_labels - code_summary_labels
391
+ if missing:
392
+ warnings.append(f"exit codes not available for job labels: {', '.join(missing)}")
393
+
394
+ if exit_code_summary:
395
+ report.exit_code_summary = exit_code_summary
396
+
397
+ return warnings
310
398
 
311
399
 
312
400
  def group_jobs_by_state(jobs):
@@ -31,6 +31,7 @@ __all__ = [
31
31
  "_dump_env_info",
32
32
  "_dump_pkg_info",
33
33
  "_make_id_link",
34
+ "bps_eval",
34
35
  "chdir",
35
36
  "create_count_summary",
36
37
  "create_job_quantum_graph_filename",
@@ -52,6 +53,7 @@ from typing import Any
52
53
 
53
54
  import yaml
54
55
 
56
+ from lsst.utils import doImport
55
57
  from lsst.utils.packages import Packages
56
58
 
57
59
  _LOG = logging.getLogger(__name__)
@@ -144,7 +146,7 @@ def create_job_quantum_graph_filename(config, job, out_prefix=None):
144
146
  found, subdir = config.search("subDirTemplate", opt={"curvals": curvals})
145
147
  if not found:
146
148
  subdir = "{job.label}"
147
- full_filename = Path("inputs") / subdir / f"quantum_{job.name}.qgraph"
149
+ full_filename = Path("inputs") / subdir / f"quantum_{job.name}.qg"
148
150
 
149
151
  if out_prefix is not None:
150
152
  full_filename = Path(out_prefix) / full_filename
@@ -353,3 +355,35 @@ def subset_dimension_values(
353
355
  f"{desc_what} missing dimensions ({', '.join(sorted(missing_dims))}) required for {desc_for}"
354
356
  )
355
357
  return dim_values
358
+
359
+
360
+ def bps_eval(func: str, args: str) -> Any:
361
+ """Evaluate user provided expression/function.
362
+
363
+ Parameters
364
+ ----------
365
+ func : `str`
366
+ Importable string or built-in function name.
367
+ args : `str`
368
+ Parameters to pass to the function.
369
+
370
+ Returns
371
+ -------
372
+ results : `~typing.Any`
373
+ Results of running eval.
374
+
375
+ Raises
376
+ ------
377
+ ImportError
378
+ If problems importing.
379
+ """
380
+ if "." in func:
381
+ genfunc = doImport(func) # noqa: F841
382
+ func_reference = "genfunc"
383
+ else:
384
+ func_reference = func
385
+ eval_str = f"{func_reference}({args})"
386
+ _LOG.debug("String passed to eval: '%s'", eval_str)
387
+ results = eval(eval_str)
388
+
389
+ return results