lsst-ctrl-mpexec 29.2025.3400__py3-none-any.whl → 29.2025.3600__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (23) hide show
  1. lsst/ctrl/mpexec/__init__.py +0 -1
  2. lsst/ctrl/mpexec/cli/butler_factory.py +253 -95
  3. lsst/ctrl/mpexec/cli/cmd/commands.py +2 -2
  4. lsst/ctrl/mpexec/cli/opt/optionGroups.py +0 -1
  5. lsst/ctrl/mpexec/cli/opt/options.py +0 -7
  6. lsst/ctrl/mpexec/cli/script/pre_exec_init_qbb.py +25 -12
  7. lsst/ctrl/mpexec/cli/script/qgraph.py +177 -89
  8. lsst/ctrl/mpexec/cli/script/run.py +211 -99
  9. lsst/ctrl/mpexec/cli/script/run_qbb.py +166 -31
  10. lsst/ctrl/mpexec/cli/utils.py +49 -0
  11. lsst/ctrl/mpexec/showInfo.py +17 -15
  12. lsst/ctrl/mpexec/version.py +1 -1
  13. {lsst_ctrl_mpexec-29.2025.3400.dist-info → lsst_ctrl_mpexec-29.2025.3600.dist-info}/METADATA +1 -1
  14. {lsst_ctrl_mpexec-29.2025.3400.dist-info → lsst_ctrl_mpexec-29.2025.3600.dist-info}/RECORD +22 -23
  15. lsst/ctrl/mpexec/cmdLineFwk.py +0 -534
  16. {lsst_ctrl_mpexec-29.2025.3400.dist-info → lsst_ctrl_mpexec-29.2025.3600.dist-info}/WHEEL +0 -0
  17. {lsst_ctrl_mpexec-29.2025.3400.dist-info → lsst_ctrl_mpexec-29.2025.3600.dist-info}/entry_points.txt +0 -0
  18. {lsst_ctrl_mpexec-29.2025.3400.dist-info → lsst_ctrl_mpexec-29.2025.3600.dist-info}/licenses/COPYRIGHT +0 -0
  19. {lsst_ctrl_mpexec-29.2025.3400.dist-info → lsst_ctrl_mpexec-29.2025.3600.dist-info}/licenses/LICENSE +0 -0
  20. {lsst_ctrl_mpexec-29.2025.3400.dist-info → lsst_ctrl_mpexec-29.2025.3600.dist-info}/licenses/bsd_license.txt +0 -0
  21. {lsst_ctrl_mpexec-29.2025.3400.dist-info → lsst_ctrl_mpexec-29.2025.3600.dist-info}/licenses/gpl-v3.0.txt +0 -0
  22. {lsst_ctrl_mpexec-29.2025.3400.dist-info → lsst_ctrl_mpexec-29.2025.3600.dist-info}/top_level.txt +0 -0
  23. {lsst_ctrl_mpexec-29.2025.3400.dist-info → lsst_ctrl_mpexec-29.2025.3600.dist-info}/zip-safe +0 -0
@@ -25,53 +25,67 @@
25
25
  # You should have received a copy of the GNU General Public License
26
26
  # along with this program. If not, see <http://www.gnu.org/licenses/>.
27
27
 
28
- import logging
29
- from types import SimpleNamespace
28
+ from __future__ import annotations
30
29
 
31
- from lsst.pipe.base import TaskFactory
30
+ from collections.abc import Iterable
31
+ from typing import TYPE_CHECKING, Literal
32
+
33
+ import astropy.units as u
34
+
35
+ import lsst.utils.timer
36
+ from lsst.pipe.base import ExecutionResources, QuantumGraph, TaskFactory
37
+ from lsst.pipe.base.mp_graph_executor import MPGraphExecutor
38
+ from lsst.pipe.base.single_quantum_executor import SingleQuantumExecutor
39
+ from lsst.resources import ResourcePath, ResourcePathExpression
40
+ from lsst.utils.doImport import doImportType
41
+ from lsst.utils.iteration import ensure_iterable
42
+ from lsst.utils.logging import getLogger
32
43
  from lsst.utils.threads import disable_implicit_threading
33
44
 
34
- from ... import CmdLineFwk
35
-
36
- _log = logging.getLogger(__name__)
37
-
38
-
39
- def run( # type: ignore
40
- pdb,
41
- graph_fixup,
42
- init_only,
43
- no_versions,
44
- processes,
45
- start_method,
46
- profile,
47
- qgraphObj,
48
- register_dataset_types,
49
- skip_init_writes,
50
- timeout,
51
- butler_config,
52
- input,
53
- output,
54
- output_run,
55
- extend_run,
56
- replace_run,
57
- prune_replaced,
58
- data_query,
59
- skip_existing_in,
60
- skip_existing,
61
- debug,
62
- fail_fast,
63
- clobber_outputs,
64
- summary,
65
- mock,
66
- unmocked_dataset_types,
67
- mock_failure,
68
- enable_implicit_threading,
45
+ from ..butler_factory import ButlerFactory
46
+ from ..utils import MP_TIMEOUT
47
+
48
+ if TYPE_CHECKING:
49
+ from lsst.pipe.base.execution_graph_fixup import ExecutionGraphFixup
50
+
51
+ _LOG = getLogger(__name__)
52
+
53
+
54
+ def run(
55
+ qg: QuantumGraph,
56
+ *,
57
+ task_factory: TaskFactory | None = None,
58
+ pdb: str | None,
59
+ graph_fixup: str,
60
+ init_only: bool,
61
+ no_versions: bool,
62
+ processes: int,
63
+ start_method: Literal["spawn", "forkserver"] | None,
64
+ profile: str,
65
+ register_dataset_types: bool,
66
+ skip_init_writes: bool,
67
+ timeout: int | None,
68
+ butler_config: ResourcePathExpression,
69
+ input: Iterable[str] | str,
70
+ output: str | None,
71
+ output_run: str | None,
72
+ extend_run: bool,
73
+ replace_run: bool,
74
+ prune_replaced: str | None,
75
+ data_query: str | None,
76
+ skip_existing_in: Iterable[str] | None,
77
+ skip_existing: bool,
78
+ debug: bool,
79
+ fail_fast: bool,
80
+ clobber_outputs: bool,
81
+ summary: ResourcePathExpression | None,
82
+ enable_implicit_threading: bool,
69
83
  cores_per_quantum: int,
70
- memory_per_quantum: str,
71
- rebase,
84
+ memory_per_quantum: str | None,
85
+ rebase: bool,
72
86
  raise_on_partial_outputs: bool,
73
- **kwargs,
74
- ):
87
+ **kwargs: object,
88
+ ) -> None:
75
89
  """Implement the command line interface `pipetask run` subcommand.
76
90
 
77
91
  Should only be called by command line tools and unit test code that test
@@ -79,8 +93,13 @@ def run( # type: ignore
79
93
 
80
94
  Parameters
81
95
  ----------
82
- pdb : `bool`
83
- Drop into pdb on exception or not.
96
+ qg : `lsst.pipe.base.QuantumGraph`
97
+ A QuantumGraph generated by a previous subcommand.
98
+ task_factory : `lsst.pipe.base.TaskFactory`, optional
99
+ A custom task factory to use.
100
+ pdb : `str`, optional
101
+ Debugger to import and use (via the ``post_mortem`` function) in the
102
+ event of an exception.
84
103
  graph_fixup : `str`
85
104
  The name of the class or factory method which makes an instance used
86
105
  for execution graph fixup.
@@ -96,8 +115,6 @@ def run( # type: ignore
96
115
  one for current platform.
97
116
  profile : `str`
98
117
  File name to dump cProfile information to.
99
- qgraphObj : `lsst.pipe.base.QuantumGraph`
100
- A QuantumGraph generated by a previous subcommand.
101
118
  register_dataset_types : `bool`
102
119
  If true, register DatasetTypes that do not already exist in the
103
120
  Registry.
@@ -106,21 +123,18 @@ def run( # type: ignore
106
123
  schemas).
107
124
  timeout : `int`
108
125
  Timeout for multiprocessing; maximum wall time (sec).
109
- butler_config : `str`, `dict`, or `lsst.daf.butler.Config`
110
- If `str`, `butler_config` is the path location of the gen3
111
- butler/registry config file. If `dict`, `butler_config` is key value
112
- pairs used to init or update the `lsst.daf.butler.Config` instance. If
113
- `Config`, it is the object used to configure a Butler.
114
- input : `list` [ `str` ]
126
+ butler_config : convertible to `lsst.resources.ResourcePath`
127
+ Path to butler repository configuration.
128
+ input : `~collections.abc.Iterable` [ `str` ] or `None`
115
129
  List of names of the input collection(s).
116
- output : `str`
130
+ output : `str` or `None`
117
131
  Name of the output CHAINED collection. This may either be an existing
118
132
  CHAINED collection to use as both input and output (if `input` is
119
133
  `None`), or a new CHAINED collection created to include all inputs
120
134
  (if `input` is not `None`). In both cases, the collection's children
121
135
  will start with an output RUN collection that directly holds all new
122
136
  datasets (see `output_run`).
123
- output_run : `str`
137
+ output_run : `str` or `None`
124
138
  Name of the new output RUN collection. If not provided then `output`
125
139
  must be provided and a new RUN collection will be created by appending
126
140
  a timestamp to the value passed with `output`. If this collection
@@ -136,13 +150,14 @@ def run( # type: ignore
136
150
  development, but it does not delete the datasets associated with the
137
151
  replaced run unless `prune-replaced` is also True. Requires `output`,
138
152
  and `extend_run` must be `None`.
139
- prune_replaced : "unstore", "purge", or `None`
153
+ prune_replaced : `str` or `None`
140
154
  If not `None`, delete the datasets in the collection replaced by
141
155
  `replace_run`, either just from the datastore ("unstore") or by
142
- removing them and the RUN completely ("purge"). Requires `replace_run`.
156
+ removing them and the RUN completely ("purge"). Requires
157
+ ``replace_run`` to be `True`.
143
158
  data_query : `str`
144
159
  User query selection expression.
145
- skip_existing_in : `list` [ `str` ]
160
+ skip_existing_in : `~collections.abc.Iterable` [ `str` ] or `None`
146
161
  Accepts list of collections, if all Quantum outputs already exist in
147
162
  the specified list of collections then that Quantum will be excluded
148
163
  from the QuantumGraph.
@@ -160,14 +175,6 @@ def run( # type: ignore
160
175
  given.
161
176
  summary : `str`
162
177
  File path to store job report in JSON format.
163
- mock : `bool`, optional
164
- If `True` then run mock pipeline instead of real one. Ignored if an
165
- existing QuantumGraph is provided.
166
- unmocked_dataset_types : `collections.abc.Sequence` [ `str` ]
167
- List of overall-input dataset types that should not be mocked.
168
- Ignored if an existing QuantumGraph is provided.
169
- mock_failure : `~collections.abc.Sequence`, optional
170
- List of quanta that should raise exceptions.
171
178
  enable_implicit_threading : `bool`, optional
172
179
  If `True`, do not disable implicit threading by third-party libraries.
173
180
  Implicit threading is always disabled during actual quantum execution
@@ -183,58 +190,163 @@ def run( # type: ignore
183
190
  the ``inputs``.
184
191
  raise_on_partial_outputs : `bool`
185
192
  Consider partial outputs an error instead of a success.
186
- **kwargs : `dict` [`str`, `str`]
193
+ **kwargs : `object`
187
194
  Ignored; click commands may accept options for more than one script
188
195
  function and pass all the option kwargs to each of the script functions
189
196
  which ignore these unused kwargs.
190
197
  """
191
198
  # Fork option still exists for compatibility but we use spawn instead.
192
- if start_method == "fork":
193
- start_method = "spawn"
194
- _log.warning("Option --start-method=fork is unsafe and no longer supported, will use spawn instead.")
199
+ if start_method == "fork": # type: ignore[comparison-overlap]
200
+ start_method = "spawn" # type: ignore[unreachable]
201
+ _LOG.warning("Option --start-method=fork is unsafe and no longer supported, using spawn instead.")
195
202
 
196
203
  if not enable_implicit_threading:
197
204
  disable_implicit_threading()
198
205
 
199
- args = SimpleNamespace(
200
- pdb=pdb,
201
- graph_fixup=graph_fixup,
202
- init_only=init_only,
203
- no_versions=no_versions,
204
- processes=processes,
205
- start_method=start_method,
206
- profile=profile,
207
- skip_init_writes=skip_init_writes,
208
- timeout=timeout,
209
- register_dataset_types=register_dataset_types,
210
- butler_config=butler_config,
211
- input=input,
206
+ skip_existing_in = tuple(skip_existing_in) if skip_existing_in is not None else ()
207
+ if data_query is None:
208
+ data_query = ""
209
+ inputs = list(ensure_iterable(input)) if input else []
210
+ del input
211
+ enable_lsst_debug = debug
212
+ del debug
213
+
214
+ # If we have no output run specified, use the one from the graph rather
215
+ # than letting a new timestamped run be created.
216
+ if not output_run and qg.metadata and (output_run := qg.metadata.get("output_run")):
217
+ output_run = output_run
218
+
219
+ # Check that output run defined on command line is consistent with
220
+ # quantum graph.
221
+ if output_run and qg.metadata:
222
+ graph_output_run = qg.metadata.get("output_run", output_run)
223
+ if graph_output_run != output_run:
224
+ raise ValueError(
225
+ f"Output run defined on command line ({output_run}) has to be "
226
+ f"identical to graph metadata ({graph_output_run}). "
227
+ "To update graph metadata run `pipetask update-graph-run` command."
228
+ )
229
+
230
+ # Make sure that --extend-run always enables --skip-existing,
231
+ # clobbering should be disabled if --extend-run is not specified.
232
+ if extend_run:
233
+ skip_existing = True
234
+ else:
235
+ clobber_outputs = False
236
+
237
+ # Make butler instance. QuantumGraph should have an output run defined,
238
+ # but we ignore it here and let command line decide actual output run.
239
+ butler = ButlerFactory.make_write_butler(
240
+ butler_config,
241
+ qg.pipeline_graph,
212
242
  output=output,
213
243
  output_run=output_run,
244
+ inputs=inputs,
214
245
  extend_run=extend_run,
246
+ rebase=rebase,
215
247
  replace_run=replace_run,
216
248
  prune_replaced=prune_replaced,
217
- data_query=data_query,
249
+ )
250
+
251
+ assert butler.run is not None, "Guaranteed by make_write_butler."
252
+ if skip_existing:
253
+ skip_existing_in += (butler.run,)
254
+
255
+ # Enable lsstDebug debugging. Note that this is done once in the
256
+ # main process before PreExecInit and it is also repeated before
257
+ # running each task in SingleQuantumExecutor (which may not be
258
+ # needed if `multiprocessing` always uses fork start method).
259
+ if enable_lsst_debug:
260
+ try:
261
+ _LOG.debug("Will try to import debug.py")
262
+ import debug # type: ignore # noqa: F401
263
+ except ImportError:
264
+ _LOG.warning("No 'debug' module found.")
265
+
266
+ # Save all InitOutputs, configs, etc.
267
+ if register_dataset_types:
268
+ qg.pipeline_graph.register_dataset_types(butler, include_packages=not no_versions)
269
+ if not skip_init_writes:
270
+ qg.write_init_outputs(butler, skip_existing=skip_existing)
271
+ qg.write_configs(butler, compare_existing=extend_run)
272
+ if not no_versions:
273
+ qg.write_packages(butler, compare_existing=extend_run)
274
+
275
+ if init_only:
276
+ return
277
+
278
+ if task_factory is None:
279
+ task_factory = TaskFactory()
280
+ resources = ExecutionResources(
281
+ num_cores=cores_per_quantum, max_mem=memory_per_quantum, default_mem_units=u.MB
282
+ )
283
+ quantum_executor = SingleQuantumExecutor(
284
+ butler=butler,
285
+ task_factory=task_factory,
218
286
  skip_existing_in=skip_existing_in,
219
- skip_existing=skip_existing,
220
- enableLsstDebug=debug,
221
- fail_fast=fail_fast,
222
287
  clobber_outputs=clobber_outputs,
223
- summary=summary,
224
- # Mock options only used by qgraph.
225
- enable_implicit_threading=enable_implicit_threading,
226
- cores_per_quantum=cores_per_quantum,
227
- memory_per_quantum=memory_per_quantum,
228
- rebase=rebase,
288
+ enable_lsst_debug=enable_lsst_debug,
289
+ resources=resources,
229
290
  raise_on_partial_outputs=raise_on_partial_outputs,
230
291
  )
231
292
 
232
- f = CmdLineFwk()
233
- taskFactory = TaskFactory()
293
+ if timeout is None:
294
+ timeout = MP_TIMEOUT
295
+ executor = MPGraphExecutor(
296
+ num_proc=processes,
297
+ timeout=timeout,
298
+ start_method=start_method,
299
+ quantum_executor=quantum_executor,
300
+ fail_fast=fail_fast,
301
+ pdb=pdb,
302
+ execution_graph_fixup=_import_graph_fixup(graph_fixup),
303
+ )
304
+ # Have to reset connection pool to avoid sharing connections with
305
+ # forked processes.
306
+ butler.registry.resetConnectionPool()
307
+ try:
308
+ with lsst.utils.timer.profile(profile, _LOG):
309
+ executor.execute(qg)
310
+ finally:
311
+ if summary:
312
+ report = executor.getReport()
313
+ if report:
314
+ with ResourcePath(summary).open("w") as out:
315
+ # Do not save fields that are not set.
316
+ out.write(report.model_dump_json(exclude_none=True, indent=2))
234
317
 
235
- # If we have no output run specified, use the one from the graph rather
236
- # than letting a new timestamped run be created.
237
- if not args.output_run and qgraphObj.metadata and (output_run := qgraphObj.metadata.get("output_run")):
238
- args.output_run = output_run
239
318
 
240
- f.runPipeline(qgraphObj, taskFactory, args)
319
+ def _import_graph_fixup(graph_fixup: str) -> ExecutionGraphFixup | None:
320
+ """Import/instantiate graph fixup object.
321
+
322
+ Parameters
323
+ ----------
324
+ graph_fixup : `str`
325
+ Graph fixup command-line argument.
326
+
327
+ Returns
328
+ -------
329
+ fixup : `ExecutionGraphFixup` or `None`
330
+ Object that imposes additional ordering constraints on the graph.
331
+
332
+ Raises
333
+ ------
334
+ ValueError
335
+ Raised if import fails, method call raises exception, or returned
336
+ instance has unexpected type.
337
+ """
338
+ from lsst.pipe.base.execution_graph_fixup import ExecutionGraphFixup
339
+
340
+ if graph_fixup:
341
+ try:
342
+ factory = doImportType(graph_fixup)
343
+ except Exception as exc:
344
+ raise ValueError("Failed to import graph fixup class/method") from exc
345
+ try:
346
+ fixup = factory()
347
+ except Exception as exc:
348
+ raise ValueError("Failed to make instance of graph fixup") from exc
349
+ if not isinstance(fixup, ExecutionGraphFixup):
350
+ raise ValueError("Graph fixup is not an instance of ExecutionGraphFixup class")
351
+ return fixup
352
+ return None
@@ -25,36 +25,59 @@
25
25
  # You should have received a copy of the GNU General Public License
26
26
  # along with this program. If not, see <http://www.gnu.org/licenses/>.
27
27
 
28
- import logging
29
- from types import SimpleNamespace
28
+ from __future__ import annotations
30
29
 
31
- from lsst.pipe.base import TaskFactory
30
+ import pickle
31
+ import uuid
32
+ from collections.abc import Mapping
33
+ from typing import Literal
34
+
35
+ import astropy.units as u
36
+
37
+ import lsst.utils.timer
38
+ from lsst.daf.butler import (
39
+ DatasetType,
40
+ DimensionConfig,
41
+ DimensionUniverse,
42
+ LimitedButler,
43
+ Quantum,
44
+ QuantumBackedButler,
45
+ )
46
+ from lsst.pipe.base import BuildId, ExecutionResources, QuantumGraph, TaskFactory
47
+ from lsst.pipe.base.mp_graph_executor import MPGraphExecutor
48
+ from lsst.pipe.base.single_quantum_executor import SingleQuantumExecutor
49
+ from lsst.resources import ResourcePath, ResourcePathExpression
50
+ from lsst.utils.logging import VERBOSE, getLogger
32
51
  from lsst.utils.threads import disable_implicit_threading
33
52
 
34
- from ... import CmdLineFwk
53
+ from ..butler_factory import ButlerFactory
54
+ from ..utils import MP_TIMEOUT, summarize_quantum_graph
35
55
 
36
- _log = logging.getLogger(__name__)
56
+ _LOG = getLogger(__name__)
37
57
 
38
58
 
39
59
  def run_qbb(
40
- butler_config: str,
41
- qgraph: str,
60
+ *,
61
+ task_factory: TaskFactory | None = None,
62
+ butler_config: ResourcePathExpression,
63
+ qgraph: ResourcePathExpression,
42
64
  config_search_path: list[str] | None,
43
65
  qgraph_id: str | None,
44
- qgraph_node_id: list[int] | None,
66
+ qgraph_node_id: list[str | uuid.UUID] | None,
45
67
  processes: int,
46
68
  pdb: str | None,
47
69
  profile: str | None,
48
70
  debug: bool,
49
- start_method: str | None,
71
+ start_method: Literal["spawn", "forkserver"] | None,
50
72
  timeout: int | None,
51
73
  fail_fast: bool,
52
- summary: str | None,
74
+ summary: ResourcePathExpression | None,
53
75
  enable_implicit_threading: bool,
54
76
  cores_per_quantum: int,
55
77
  memory_per_quantum: str,
56
78
  raise_on_partial_outputs: bool,
57
79
  no_existing_outputs: bool,
80
+ **kwargs: object,
58
81
  ) -> None:
59
82
  """Implement the command line interface ``pipetask run-qbb`` subcommand.
60
83
 
@@ -63,6 +86,8 @@ def run_qbb(
63
86
 
64
87
  Parameters
65
88
  ----------
89
+ task_factory : `lsst.pipe.base.TaskFactory`, optional
90
+ A custom task factory to use.
66
91
  butler_config : `str`
67
92
  The path location of the gen3 butler/registry config file.
68
93
  qgraph : `str`
@@ -109,36 +134,146 @@ def run_qbb(
109
134
  no_existing_outputs : `bool`
110
135
  Whether to assume that no predicted outputs for these quanta already
111
136
  exist in the output run collection.
137
+ **kwargs : `object`
138
+ Ignored; click commands may accept options for more than one script
139
+ function and pass all the option kwargs to each of the script functions
140
+ which ignore these unused kwargs.
112
141
  """
113
142
  # Fork option still exists for compatibility but we use spawn instead.
114
- if start_method == "fork":
115
- start_method = "spawn"
116
- _log.warning("Option --start-method=fork is unsafe and no longer supported, will use spawn instead.")
143
+ if start_method == "fork": # type: ignore[comparison-overlap]
144
+ start_method = "spawn" # type: ignore[unreachable]
145
+ _LOG.warning("Option --start-method=fork is unsafe and no longer supported, using spawn instead.")
117
146
 
118
147
  if not enable_implicit_threading:
119
148
  disable_implicit_threading()
120
149
 
121
- args = SimpleNamespace(
150
+ # Load quantum graph.
151
+ nodes = qgraph_node_id or None
152
+ with lsst.utils.timer.time_this(
153
+ _LOG,
154
+ msg=f"Reading {str(len(nodes)) if nodes is not None else 'all'} quanta.",
155
+ level=VERBOSE,
156
+ ) as qg_read_time:
157
+ qg = QuantumGraph.loadUri(
158
+ qgraph, nodes=nodes, graphID=BuildId(qgraph_id) if qgraph_id is not None else None
159
+ )
160
+ job_metadata = {"qg_read_time": qg_read_time.duration, "qg_size": len(qg)}
161
+
162
+ if qg.metadata is None:
163
+ raise ValueError("QuantumGraph is missing metadata, cannot continue.")
164
+
165
+ summarize_quantum_graph(qg)
166
+
167
+ dataset_types = {dstype.name: dstype for dstype in qg.registryDatasetTypes()}
168
+
169
+ # Ensure that QBB uses shared datastore cache.
170
+ ButlerFactory.define_datastore_cache()
171
+
172
+ _butler_factory = _QBBFactory(
122
173
  butler_config=butler_config,
123
- qgraph=qgraph,
174
+ dimensions=qg.universe,
175
+ dataset_types=dataset_types,
124
176
  config_search_path=config_search_path,
125
- qgraph_id=qgraph_id,
126
- qgraph_node_id=qgraph_node_id,
127
- processes=processes,
128
- pdb=pdb,
129
- profile=profile,
130
- enableLsstDebug=debug,
131
- start_method=start_method,
177
+ )
178
+
179
+ # make special quantum executor
180
+ resources = ExecutionResources(
181
+ num_cores=cores_per_quantum, max_mem=memory_per_quantum, default_mem_units=u.MB
182
+ )
183
+ quantumExecutor = SingleQuantumExecutor(
184
+ butler=None,
185
+ task_factory=task_factory,
186
+ enable_lsst_debug=debug,
187
+ limited_butler_factory=_butler_factory,
188
+ resources=resources,
189
+ assume_no_existing_outputs=no_existing_outputs,
190
+ skip_existing=True,
191
+ clobber_outputs=True,
192
+ raise_on_partial_outputs=raise_on_partial_outputs,
193
+ job_metadata=job_metadata,
194
+ )
195
+
196
+ timeout = MP_TIMEOUT if timeout is None else timeout
197
+ executor = MPGraphExecutor(
198
+ num_proc=processes,
132
199
  timeout=timeout,
200
+ start_method=start_method,
201
+ quantum_executor=quantumExecutor,
133
202
  fail_fast=fail_fast,
134
- summary=summary,
135
- enable_implicit_threading=enable_implicit_threading,
136
- cores_per_quantum=cores_per_quantum,
137
- memory_per_quantum=memory_per_quantum,
138
- raise_on_partial_outputs=raise_on_partial_outputs,
139
- no_existing_outputs=no_existing_outputs,
203
+ pdb=pdb,
140
204
  )
205
+ try:
206
+ with lsst.utils.timer.profile(profile, _LOG):
207
+ executor.execute(qg)
208
+ finally:
209
+ if summary:
210
+ report = executor.getReport()
211
+ if report:
212
+ with ResourcePath(summary).open("w") as out:
213
+ # Do not save fields that are not set.
214
+ out.write(report.model_dump_json(exclude_none=True, indent=2))
215
+
216
+
217
+ class _QBBFactory:
218
+ """Class which is a callable for making QBB instances.
219
+
220
+ This class is also responsible for reconstructing correct dimension
221
+ universe after unpickling. When pickling multiple things that require
222
+ dimension universe, this class must be unpickled first. The logic in
223
+ MPGraphExecutor ensures that SingleQuantumExecutor is unpickled first in
224
+ the subprocess, which causes unpickling of this class.
225
+ """
226
+
227
+ def __init__(
228
+ self,
229
+ butler_config: ResourcePathExpression,
230
+ dimensions: DimensionUniverse,
231
+ dataset_types: Mapping[str, DatasetType],
232
+ config_search_path: list[str] | None,
233
+ ):
234
+ self.butler_config = butler_config
235
+ self.dimensions = dimensions
236
+ self.dataset_types = dataset_types
237
+ self.config_search_path = config_search_path
238
+
239
+ def __call__(self, quantum: Quantum) -> LimitedButler:
240
+ """Return freshly initialized `~lsst.daf.butler.QuantumBackedButler`.
241
+
242
+ Factory method to create QuantumBackedButler instances.
243
+ """
244
+ return QuantumBackedButler.initialize(
245
+ config=self.butler_config,
246
+ quantum=quantum,
247
+ dimensions=self.dimensions,
248
+ dataset_types=self.dataset_types,
249
+ )
250
+
251
+ @classmethod
252
+ def _unpickle(
253
+ cls,
254
+ butler_config: ResourcePathExpression,
255
+ dimensions_config: DimensionConfig | None,
256
+ dataset_types_pickle: bytes,
257
+ config_search_path: list[str] | None,
258
+ ) -> _QBBFactory:
259
+ universe = DimensionUniverse(dimensions_config)
260
+ dataset_types = pickle.loads(dataset_types_pickle)
261
+ return _QBBFactory(butler_config, universe, dataset_types, config_search_path)
141
262
 
142
- f = CmdLineFwk()
143
- task_factory = TaskFactory()
144
- f.runGraphQBB(task_factory, args)
263
+ def __reduce__(self) -> tuple:
264
+ # If dimension universe is not default one, we need to dump/restore
265
+ # its config.
266
+ config = self.dimensions.dimensionConfig
267
+ default = DimensionConfig()
268
+ # Only send configuration to other side if it is non-default, default
269
+ # will be instantiated from config=None.
270
+ if (config["namespace"], config["version"]) != (default["namespace"], default["version"]):
271
+ dimension_config = config
272
+ else:
273
+ dimension_config = None
274
+ # Dataset types need to be unpickled only after universe is made.
275
+ dataset_types_pickle = pickle.dumps(self.dataset_types)
276
+ return (
277
+ self._unpickle,
278
+ (self.butler_config, dimension_config, dataset_types_pickle, self.config_search_path),
279
+ )