Coverage for tests/test_separable_pipeline_executor.py: 100%
756 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-06-25 08:09 +0000
« prev ^ index » next coverage.py v7.14.3, created at 2026-06-25 08:09 +0000
1# This file is part of pipe_base.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This software is dual licensed under the GNU General Public License and also
10# under a 3-clause BSD license. Recipients may choose which of these licenses
11# to use; please see the files gpl-3.0.txt and/or bsd_license.txt,
12# respectively. If you choose the GPL option then the following text applies
13# (but note that there is still no warranty even if you opt for BSD instead):
14#
15# This program is free software: you can redistribute it and/or modify
16# it under the terms of the GNU General Public License as published by
17# the Free Software Foundation, either version 3 of the License, or
18# (at your option) any later version.
19#
20# This program is distributed in the hope that it will be useful,
21# but WITHOUT ANY WARRANTY; without even the implied warranty of
22# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23# GNU General Public License for more details.
24#
25# You should have received a copy of the GNU General Public License
26# along with this program. If not, see <https://www.gnu.org/licenses/>.
29import os
30import tempfile
31import unittest
33import lsst.daf.butler
34import lsst.daf.butler.tests as butlerTests
35import lsst.pex.config
36import lsst.utils.tests
37from lsst.daf.butler.registry import RegistryDefaults
38from lsst.pipe.base import (
39 Instrument,
40 Pipeline,
41 PipelineGraph,
42 QuantumAttemptStatus,
43 QuantumGraph,
44 QuantumSuccessCaveats,
45 TaskMetadata,
46)
47from lsst.pipe.base.all_dimensions_quantum_graph_builder import AllDimensionsQuantumGraphBuilder
48from lsst.pipe.base.automatic_connection_constants import (
49 PACKAGES_INIT_OUTPUT_NAME,
50 PROVENANCE_DATASET_TYPE_NAME,
51 PROVENANCE_STORAGE_CLASS,
52)
53from lsst.pipe.base.mp_graph_executor import MPGraphExecutorError
54from lsst.pipe.base.quantum_graph import ProvenanceQuantumGraph
55from lsst.pipe.base.quantum_graph_builder import OutputExistsError
56from lsst.pipe.base.separable_pipeline_executor import SeparablePipelineExecutor
57from lsst.pipe.base.tests.mocks import (
58 DirectButlerRepo,
59 DynamicTestPipelineTaskConfig,
60)
61from lsst.resources import ResourcePath
62from lsst.utils.packages import Packages
64TESTDIR = os.path.abspath(os.path.dirname(__file__))
67class SeparablePipelineExecutorTests(lsst.utils.tests.TestCase):
68 """Test the SeparablePipelineExecutor API with a trivial task."""
70 pipeline_file = os.path.join(TESTDIR, "pipelines", "pipeline_separable.yaml")
72 def setUp(self):
73 repodir = tempfile.TemporaryDirectory()
74 # TemporaryDirectory warns on leaks; addCleanup also keeps it from
75 # getting garbage-collected.
76 self.addCleanup(tempfile.TemporaryDirectory.cleanup, repodir)
78 # standalone parameter forces the returned config to also include
79 # the information from the search paths.
80 config = lsst.daf.butler.Butler.makeRepo(
81 repodir.name, standalone=True, searchPaths=[os.path.join(TESTDIR, "config")]
82 )
83 butler = lsst.daf.butler.Butler.from_config(config, writeable=True)
84 self.enterContext(butler)
85 output = "fake"
86 output_run = f"{output}/{Instrument.makeCollectionTimestamp()}"
87 butler.registry.registerCollection(output_run, lsst.daf.butler.CollectionType.RUN)
88 butler.registry.registerCollection(output, lsst.daf.butler.CollectionType.CHAINED)
89 butler.registry.setCollectionChain(output, [output_run])
90 butler.registry.defaults = RegistryDefaults(collections=[output], run=output_run)
91 self.butler = butler
93 butlerTests.addDatasetType(self.butler, "input", set(), "StructuredDataDict")
94 butlerTests.addDatasetType(self.butler, "intermediate", set(), "StructuredDataDict")
95 butlerTests.addDatasetType(self.butler, "a_log", set(), "ButlerLogRecords")
96 butlerTests.addDatasetType(self.butler, "a_metadata", set(), "TaskMetadata")
97 butlerTests.addDatasetType(self.butler, "a_config", set(), "Config")
98 provenance_dataset_type = butlerTests.addDatasetType(
99 self.butler, PROVENANCE_DATASET_TYPE_NAME, set(), PROVENANCE_STORAGE_CLASS
100 )
101 self.provenance_ref = lsst.daf.butler.DatasetRef(
102 provenance_dataset_type,
103 lsst.daf.butler.DataCoordinate.make_empty(self.butler.dimensions),
104 run=butler.run,
105 )
107 def build_empty_quantum_graph(self) -> None:
108 pipeline_graph = PipelineGraph(universe=self.butler.dimensions)
109 pipeline_graph.resolve(self.butler.registry)
110 builder = AllDimensionsQuantumGraphBuilder(pipeline_graph, self.butler)
111 return builder.finish(attach_datastore_records=False).assemble()
113 def check_provenance_fullgraph(self):
114 provenance_qg = self.butler.get(self.provenance_ref)
115 empty_data_id = lsst.daf.butler.DataCoordinate.make_empty(self.butler.dimensions)
116 self.assertCountEqual(provenance_qg.quanta_by_task.keys(), {"a", "b"})
117 self.assertCountEqual(
118 provenance_qg.datasets_by_type.keys(),
119 {
120 "input",
121 "intermediate",
122 "output",
123 "a_config",
124 "b_config",
125 "a_metadata",
126 "b_metadata",
127 "a_log",
128 "b_log",
129 },
130 )
131 a_id = provenance_qg.quanta_by_task["a"][empty_data_id]
132 b_id = provenance_qg.quanta_by_task["b"][empty_data_id]
133 input_id = provenance_qg.datasets_by_type["input"][empty_data_id]
134 intermediate_id = provenance_qg.datasets_by_type["intermediate"][empty_data_id]
135 output_id = provenance_qg.datasets_by_type["output"][empty_data_id]
136 a_metadata_id = provenance_qg.datasets_by_type["a_metadata"][empty_data_id]
137 a_log_id = provenance_qg.datasets_by_type["a_log"][empty_data_id]
138 b_metadata_id = provenance_qg.datasets_by_type["b_metadata"][empty_data_id]
139 b_log_id = provenance_qg.datasets_by_type["b_log"][empty_data_id]
140 self.assertEqual(
141 provenance_qg.bipartite_xgraph.nodes[a_id]["status"], QuantumAttemptStatus.SUCCESSFUL
142 )
143 self.assertEqual(
144 provenance_qg.bipartite_xgraph.nodes[b_id]["status"], QuantumAttemptStatus.SUCCESSFUL
145 )
146 self.assertEqual(list(provenance_qg.bipartite_xgraph.predecessors(a_id)), [input_id])
147 self.assertEqual(
148 list(provenance_qg.bipartite_xgraph.successors(a_id)), [intermediate_id, a_metadata_id, a_log_id]
149 )
150 self.assertEqual(list(provenance_qg.bipartite_xgraph.predecessors(b_id)), [intermediate_id])
151 self.assertEqual(
152 list(provenance_qg.bipartite_xgraph.successors(b_id)), [output_id, b_metadata_id, b_log_id]
153 )
154 for datasets_by_data_id in provenance_qg.datasets_by_type.values():
155 for dataset_id in datasets_by_data_id.values():
156 self.assertTrue(provenance_qg.bipartite_xgraph.nodes[dataset_id]["produced"])
157 logs_pqg = self.butler.get(
158 self.provenance_ref.makeComponentRef("logs"),
159 parameters={"quanta": (a_id,), "datasets": (b_log_id,)},
160 )
161 self.assertEqual(list(logs_pqg[a_id][-1]), list(self.butler.get("a_log", empty_data_id)))
162 self.assertEqual(list(logs_pqg[b_log_id][-1]), list(self.butler.get("b_log", empty_data_id)))
163 metadata_pqg = self.butler.get(
164 self.provenance_ref.makeComponentRef("metadata"),
165 parameters={"quanta": (b_id,), "datasets": (a_metadata_id,)},
166 )
167 self.assertEqual(metadata_pqg[a_metadata_id][-1], self.butler.get("a_metadata", empty_data_id))
168 self.assertEqual(metadata_pqg[b_id][-1], self.butler.get("b_metadata", empty_data_id))
169 self.assertIsInstance(self.butler.get("run_provenance.packages"), Packages)
171 def check_provenance_emptygraph(self):
172 provenance_qg = self.butler.get(self.provenance_ref)
173 self.assertFalse(provenance_qg.bipartite_xgraph)
174 self.assertFalse(any(provenance_qg.quanta_by_task.values()))
175 self.assertFalse(any(provenance_qg.datasets_by_type.values()))
176 self.assertIsInstance(self.butler.get("run_provenance.packages"), Packages)
178 def test_pre_execute_qgraph_old(self):
179 # Too hard to make a quantum graph from scratch.
180 executor = SeparablePipelineExecutor(self.butler)
181 pipeline = Pipeline.fromFile(self.pipeline_file)
182 self.butler.put({"zero": 0}, "input")
183 graph = executor.make_quantum_graph(pipeline)
185 butlerTests.addDatasetType(self.butler, "output", set(), "StructuredDataDict")
186 butlerTests.addDatasetType(self.butler, "b_config", set(), "Config")
187 butlerTests.addDatasetType(self.butler, "b_log", set(), "ButlerLogRecords")
188 butlerTests.addDatasetType(self.butler, "b_metadata", set(), "TaskMetadata")
189 butlerTests.addDatasetType(self.butler, PACKAGES_INIT_OUTPUT_NAME, set(), "Packages")
191 executor.pre_execute_qgraph(
192 graph,
193 register_dataset_types=False,
194 save_init_outputs=False,
195 save_versions=False,
196 )
197 self.assertFalse(self.butler.exists("a_config", {}, collections=[self.butler.run]))
198 self.assertFalse(self.butler.exists(PACKAGES_INIT_OUTPUT_NAME, {}))
200 def test_pre_execute_qgraph(self):
201 # Too hard to make a quantum graph from scratch.
202 executor = SeparablePipelineExecutor(self.butler)
203 pipeline = Pipeline.fromFile(self.pipeline_file)
204 self.butler.put({"zero": 0}, "input")
205 graph = executor.build_quantum_graph(pipeline)
207 butlerTests.addDatasetType(self.butler, "output", set(), "StructuredDataDict")
208 butlerTests.addDatasetType(self.butler, "b_config", set(), "Config")
209 butlerTests.addDatasetType(self.butler, "b_log", set(), "ButlerLogRecords")
210 butlerTests.addDatasetType(self.butler, "b_metadata", set(), "TaskMetadata")
211 butlerTests.addDatasetType(self.butler, PACKAGES_INIT_OUTPUT_NAME, set(), "Packages")
213 executor.pre_execute_qgraph(
214 graph,
215 register_dataset_types=False,
216 save_init_outputs=False,
217 save_versions=False,
218 )
219 self.assertFalse(self.butler.exists("a_config", {}, collections=[self.butler.run]))
220 self.assertFalse(self.butler.exists(PACKAGES_INIT_OUTPUT_NAME, {}))
222 def test_pre_execute_qgraph_unconnected_old(self):
223 # Unconnected graph; see
224 # test_make_quantum_graph_nowhere_skippartial_clobber.
225 executor = SeparablePipelineExecutor(
226 self.butler,
227 skip_existing_in=[self.butler.run],
228 clobber_output=True,
229 )
230 pipeline = Pipeline.fromFile(self.pipeline_file)
231 self.butler.put({"zero": 0}, "input")
232 self.butler.put({"zero": 0}, "intermediate")
233 graph = executor.make_quantum_graph(pipeline)
235 butlerTests.addDatasetType(self.butler, "output", set(), "StructuredDataDict")
236 butlerTests.addDatasetType(self.butler, "b_config", set(), "Config")
237 butlerTests.addDatasetType(self.butler, "b_log", set(), "ButlerLogRecords")
238 butlerTests.addDatasetType(self.butler, "b_metadata", set(), "TaskMetadata")
239 butlerTests.addDatasetType(self.butler, PACKAGES_INIT_OUTPUT_NAME, set(), "Packages")
241 executor.pre_execute_qgraph(
242 graph,
243 register_dataset_types=False,
244 save_init_outputs=False,
245 save_versions=False,
246 )
247 self.assertFalse(self.butler.exists("a_config", {}, collections=[self.butler.run]))
248 self.assertFalse(self.butler.exists(PACKAGES_INIT_OUTPUT_NAME, {}))
250 def test_pre_execute_qgraph_unconnected(self):
251 # Unconnected graph; see
252 # test_make_quantum_graph_nowhere_skippartial_clobber.
253 executor = SeparablePipelineExecutor(
254 self.butler,
255 skip_existing_in=[self.butler.run],
256 clobber_output=True,
257 )
258 pipeline = Pipeline.fromFile(self.pipeline_file)
259 self.butler.put({"zero": 0}, "input")
260 self.butler.put({"zero": 0}, "intermediate")
261 graph = executor.build_quantum_graph(pipeline)
263 butlerTests.addDatasetType(self.butler, "output", set(), "StructuredDataDict")
264 butlerTests.addDatasetType(self.butler, "b_config", set(), "Config")
265 butlerTests.addDatasetType(self.butler, "b_log", set(), "ButlerLogRecords")
266 butlerTests.addDatasetType(self.butler, "b_metadata", set(), "TaskMetadata")
267 butlerTests.addDatasetType(self.butler, PACKAGES_INIT_OUTPUT_NAME, set(), "Packages")
269 executor.pre_execute_qgraph(
270 graph,
271 register_dataset_types=False,
272 save_init_outputs=False,
273 save_versions=False,
274 )
275 self.assertFalse(self.butler.exists("a_config", {}, collections=[self.butler.run]))
276 self.assertFalse(self.butler.exists(PACKAGES_INIT_OUTPUT_NAME, {}))
278 def test_pre_execute_qgraph_empty_old(self):
279 executor = SeparablePipelineExecutor(self.butler)
280 graph = QuantumGraph({}, universe=self.butler.dimensions)
282 butlerTests.addDatasetType(self.butler, "output", set(), "StructuredDataDict")
283 butlerTests.addDatasetType(self.butler, "b_config", set(), "Config")
284 butlerTests.addDatasetType(self.butler, "b_log", set(), "ButlerLogRecords")
285 butlerTests.addDatasetType(self.butler, "b_metadata", set(), "TaskMetadata")
286 butlerTests.addDatasetType(self.butler, PACKAGES_INIT_OUTPUT_NAME, set(), "Packages")
288 executor.pre_execute_qgraph(
289 graph,
290 register_dataset_types=False,
291 save_init_outputs=False,
292 save_versions=False,
293 )
294 self.assertFalse(self.butler.exists("a_config", {}, collections=[self.butler.run]))
295 self.assertFalse(self.butler.exists(PACKAGES_INIT_OUTPUT_NAME, {}))
297 def test_pre_execute_qgraph_empty(self):
298 executor = SeparablePipelineExecutor(self.butler)
299 graph = self.build_empty_quantum_graph()
301 butlerTests.addDatasetType(self.butler, "output", set(), "StructuredDataDict")
302 butlerTests.addDatasetType(self.butler, "b_config", set(), "Config")
303 butlerTests.addDatasetType(self.butler, "b_log", set(), "ButlerLogRecords")
304 butlerTests.addDatasetType(self.butler, "b_metadata", set(), "TaskMetadata")
305 butlerTests.addDatasetType(self.butler, PACKAGES_INIT_OUTPUT_NAME, set(), "Packages")
307 executor.pre_execute_qgraph(
308 graph,
309 register_dataset_types=False,
310 save_init_outputs=False,
311 save_versions=False,
312 )
313 self.assertFalse(self.butler.exists("a_config", {}, collections=[self.butler.run]))
314 self.assertFalse(self.butler.exists(PACKAGES_INIT_OUTPUT_NAME, {}))
316 def test_pre_execute_qgraph_register_old(self):
317 executor = SeparablePipelineExecutor(self.butler)
318 pipeline = Pipeline.fromFile(self.pipeline_file)
319 self.butler.put({"zero": 0}, "input")
320 graph = executor.make_quantum_graph(pipeline)
322 executor.pre_execute_qgraph(
323 graph,
324 register_dataset_types=True,
325 save_init_outputs=False,
326 save_versions=False,
327 )
328 self.assertEqual({d.name for d in self.butler.registry.queryDatasetTypes("output")}, {"output"})
329 self.assertEqual(
330 {d.name for d in self.butler.registry.queryDatasetTypes("b_*")},
331 {"b_config", "b_log", "b_metadata"},
332 )
333 self.assertFalse(self.butler.exists("a_config", {}, collections=[self.butler.run]))
334 self.assertFalse(self.butler.exists(PACKAGES_INIT_OUTPUT_NAME, {}))
336 def test_pre_execute_qgraph_register(self):
337 executor = SeparablePipelineExecutor(self.butler)
338 pipeline = Pipeline.fromFile(self.pipeline_file)
339 self.butler.put({"zero": 0}, "input")
340 graph = executor.build_quantum_graph(pipeline)
342 executor.pre_execute_qgraph(
343 graph,
344 register_dataset_types=True,
345 save_init_outputs=False,
346 save_versions=False,
347 )
348 self.assertEqual({d.name for d in self.butler.registry.queryDatasetTypes("output")}, {"output"})
349 self.assertEqual(
350 {d.name for d in self.butler.registry.queryDatasetTypes("b_*")},
351 {"b_config", "b_log", "b_metadata"},
352 )
353 self.assertFalse(self.butler.exists("a_config", {}, collections=[self.butler.run]))
354 self.assertFalse(self.butler.exists(PACKAGES_INIT_OUTPUT_NAME, {}))
356 def test_pre_execute_qgraph_init_outputs_old(self):
357 # Too hard to make a quantum graph from scratch.
358 executor = SeparablePipelineExecutor(self.butler)
359 pipeline = Pipeline.fromFile(self.pipeline_file)
360 self.butler.put({"zero": 0}, "input")
361 graph = executor.make_quantum_graph(pipeline)
363 butlerTests.addDatasetType(self.butler, "output", set(), "StructuredDataDict")
364 butlerTests.addDatasetType(self.butler, "b_config", set(), "Config")
365 butlerTests.addDatasetType(self.butler, "b_log", set(), "ButlerLogRecords")
366 butlerTests.addDatasetType(self.butler, "b_metadata", set(), "TaskMetadata")
367 butlerTests.addDatasetType(self.butler, PACKAGES_INIT_OUTPUT_NAME, set(), "Packages")
369 executor.pre_execute_qgraph(
370 graph,
371 register_dataset_types=False,
372 save_init_outputs=True,
373 save_versions=False,
374 )
375 self.assertTrue(self.butler.exists("a_config", {}, collections=[self.butler.run]))
376 self.assertFalse(self.butler.exists(PACKAGES_INIT_OUTPUT_NAME, {}))
378 def test_pre_execute_qgraph_init_outputs(self):
379 # Too hard to make a quantum graph from scratch.
380 executor = SeparablePipelineExecutor(self.butler)
381 pipeline = Pipeline.fromFile(self.pipeline_file)
382 self.butler.put({"zero": 0}, "input")
383 graph = executor.build_quantum_graph(pipeline)
385 butlerTests.addDatasetType(self.butler, "output", set(), "StructuredDataDict")
386 butlerTests.addDatasetType(self.butler, "b_config", set(), "Config")
387 butlerTests.addDatasetType(self.butler, "b_log", set(), "ButlerLogRecords")
388 butlerTests.addDatasetType(self.butler, "b_metadata", set(), "TaskMetadata")
389 butlerTests.addDatasetType(self.butler, PACKAGES_INIT_OUTPUT_NAME, set(), "Packages")
391 executor.pre_execute_qgraph(
392 graph,
393 register_dataset_types=False,
394 save_init_outputs=True,
395 save_versions=False,
396 )
397 self.assertTrue(self.butler.exists("a_config", {}, collections=[self.butler.run]))
398 self.assertFalse(self.butler.exists(PACKAGES_INIT_OUTPUT_NAME, {}))
400 def test_pre_execute_qgraph_versions_old(self):
401 executor = SeparablePipelineExecutor(self.butler)
402 pipeline = Pipeline.fromFile(self.pipeline_file)
403 self.butler.put({"zero": 0}, "input")
404 graph = executor.make_quantum_graph(pipeline)
406 butlerTests.addDatasetType(self.butler, "output", set(), "StructuredDataDict")
407 butlerTests.addDatasetType(self.butler, "b_config", set(), "Config")
408 butlerTests.addDatasetType(self.butler, "b_log", set(), "ButlerLogRecords")
409 butlerTests.addDatasetType(self.butler, "b_metadata", set(), "TaskMetadata")
410 butlerTests.addDatasetType(self.butler, PACKAGES_INIT_OUTPUT_NAME, set(), "Packages")
412 executor.pre_execute_qgraph(
413 graph,
414 register_dataset_types=False,
415 save_init_outputs=True,
416 save_versions=True,
417 )
418 self.assertTrue(self.butler.exists("a_config", {}, collections=[self.butler.run]))
419 self.assertTrue(self.butler.exists(PACKAGES_INIT_OUTPUT_NAME, {}))
421 def test_pre_execute_qgraph_versions(self):
422 executor = SeparablePipelineExecutor(self.butler)
423 pipeline = Pipeline.fromFile(self.pipeline_file)
424 self.butler.put({"zero": 0}, "input")
425 graph = executor.build_quantum_graph(pipeline)
427 butlerTests.addDatasetType(self.butler, "output", set(), "StructuredDataDict")
428 butlerTests.addDatasetType(self.butler, "b_config", set(), "Config")
429 butlerTests.addDatasetType(self.butler, "b_log", set(), "ButlerLogRecords")
430 butlerTests.addDatasetType(self.butler, "b_metadata", set(), "TaskMetadata")
431 butlerTests.addDatasetType(self.butler, PACKAGES_INIT_OUTPUT_NAME, set(), "Packages")
433 executor.pre_execute_qgraph(
434 graph,
435 register_dataset_types=False,
436 save_init_outputs=True,
437 save_versions=True,
438 )
439 self.assertTrue(self.butler.exists("a_config", {}, collections=[self.butler.run]))
440 self.assertTrue(self.butler.exists(PACKAGES_INIT_OUTPUT_NAME, {}))
442 def test_init_badinput(self):
443 with lsst.daf.butler.Butler.from_config(butler=self.butler, collections=[], run="foo") as butler:
444 with self.assertRaises(ValueError):
445 SeparablePipelineExecutor(butler)
447 def test_init_badoutput(self):
448 with lsst.daf.butler.Butler.from_config(butler=self.butler, collections=["foo"]) as butler:
449 with self.assertRaises(ValueError):
450 SeparablePipelineExecutor(butler)
452 def test_make_pipeline_full(self):
453 executor = SeparablePipelineExecutor(self.butler)
454 for uri in [
455 self.pipeline_file,
456 ResourcePath(self.pipeline_file),
457 ResourcePath(self.pipeline_file).geturl(),
458 ]:
459 pipeline_graph = executor.make_pipeline(uri).to_graph()
460 self.assertEqual(set(pipeline_graph.tasks), {"a", "b"})
462 def test_make_pipeline_subset(self):
463 executor = SeparablePipelineExecutor(self.butler)
464 path = self.pipeline_file + "#a"
465 for uri in [
466 path,
467 ResourcePath(path),
468 ResourcePath(path).geturl(),
469 ]:
470 pipeline_graph = executor.make_pipeline(uri).to_graph()
471 self.assertEqual(set(pipeline_graph.tasks), {"a"})
473 def test_build_quantum_graph_nowhere_noskip_noclobber(self):
474 executor = SeparablePipelineExecutor(self.butler, skip_existing_in=None, clobber_output=False)
475 pipeline = Pipeline.fromFile(self.pipeline_file)
477 self.butler.put({"zero": 0}, "input")
479 graph = executor.make_quantum_graph(pipeline)
480 self.assertTrue(graph.isConnected)
481 self.assertEqual(len(graph), 2)
482 self.assertEqual({q.taskDef.label for q in graph.inputQuanta}, {"a"})
483 self.assertEqual({q.taskDef.label for q in graph.outputQuanta}, {"b"})
485 def test_make_quantum_graph_nowhere_noskip_noclobber(self):
486 executor = SeparablePipelineExecutor(self.butler, skip_existing_in=None, clobber_output=False)
487 pipeline = Pipeline.fromFile(self.pipeline_file)
489 self.butler.put({"zero": 0}, "input")
491 graph = executor.build_quantum_graph(pipeline)
492 self.assertEqual(len(graph), 2)
493 self.assertEqual(graph.quanta_by_task.keys(), {"a", "b"})
495 def test_make_quantum_graph_nowhere_noskip_noclobber_conflict(self):
496 executor = SeparablePipelineExecutor(self.butler, skip_existing_in=None, clobber_output=False)
497 pipeline = Pipeline.fromFile(self.pipeline_file)
499 self.butler.put({"zero": 0}, "input")
500 self.butler.put({"zero": 0}, "intermediate")
501 self.butler.put(lsst.daf.butler.ButlerLogRecords.from_records([]), "a_log")
502 self.butler.put(TaskMetadata(), "a_metadata")
504 with self.assertRaises(OutputExistsError):
505 executor.build_quantum_graph(pipeline)
507 # TODO: need more complex task and Butler to test
508 # make_quantum_graph(where=...)
510 def test_build_quantum_graph_nowhere_skipnone_noclobber(self):
511 executor = SeparablePipelineExecutor(
512 self.butler,
513 skip_existing_in=[self.butler.run],
514 clobber_output=False,
515 )
516 pipeline = Pipeline.fromFile(self.pipeline_file)
518 self.butler.put({"zero": 0}, "input")
520 graph = executor.make_quantum_graph(pipeline)
521 self.assertTrue(graph.isConnected)
522 self.assertEqual(len(graph), 2)
523 self.assertEqual({q.taskDef.label for q in graph.inputQuanta}, {"a"})
524 self.assertEqual({q.taskDef.label for q in graph.outputQuanta}, {"b"})
526 def test_make_quantum_graph_nowhere_skipnone_noclobber(self):
527 executor = SeparablePipelineExecutor(
528 self.butler,
529 skip_existing_in=[self.butler.run],
530 clobber_output=False,
531 )
532 pipeline = Pipeline.fromFile(self.pipeline_file)
533 self.butler.put({"zero": 0}, "input")
534 graph = executor.build_quantum_graph(pipeline)
535 self.assertEqual(len(graph), 2)
536 self.assertEqual(graph.quanta_by_task.keys(), {"a", "b"})
538 def test_build_quantum_graph_nowhere_skiptotal_noclobber(self):
539 executor = SeparablePipelineExecutor(
540 self.butler,
541 skip_existing_in=[self.butler.run],
542 clobber_output=False,
543 )
544 pipeline = Pipeline.fromFile(self.pipeline_file)
546 self.butler.put({"zero": 0}, "input")
547 self.butler.put({"zero": 0}, "intermediate")
548 self.butler.put(lsst.daf.butler.ButlerLogRecords.from_records([]), "a_log")
549 self.butler.put(TaskMetadata(), "a_metadata")
550 self.butler.put(lsst.pex.config.Config(), "a_config")
552 graph = executor.make_quantum_graph(pipeline)
553 self.assertTrue(graph.isConnected)
554 self.assertEqual(len(graph), 1)
555 self.assertEqual({q.taskDef.label for q in graph.inputQuanta}, {"b"})
556 self.assertEqual({q.taskDef.label for q in graph.outputQuanta}, {"b"})
558 def test_make_quantum_graph_nowhere_skiptotal_noclobber(self):
559 executor = SeparablePipelineExecutor(
560 self.butler,
561 skip_existing_in=[self.butler.run],
562 clobber_output=False,
563 )
564 pipeline = Pipeline.fromFile(self.pipeline_file)
566 self.butler.put({"zero": 0}, "input")
567 self.butler.put({"zero": 0}, "intermediate")
568 self.butler.put(lsst.daf.butler.ButlerLogRecords.from_records([]), "a_log")
569 self.butler.put(TaskMetadata(), "a_metadata")
570 self.butler.put(lsst.pex.config.Config(), "a_config")
572 graph = executor.build_quantum_graph(pipeline)
573 self.assertEqual(len(graph), 1)
574 self.assertEqual(graph.header.n_task_quanta["a"], 0)
575 self.assertEqual(graph.header.n_task_quanta["b"], 1)
577 def test_make_quantum_graph_nowhere_skippartial_noclobber(self):
578 executor = SeparablePipelineExecutor(
579 self.butler,
580 skip_existing_in=[self.butler.run],
581 clobber_output=False,
582 )
583 pipeline = Pipeline.fromFile(self.pipeline_file)
585 self.butler.put({"zero": 0}, "input")
586 self.butler.put({"zero": 0}, "intermediate")
588 with self.assertRaises(OutputExistsError):
589 executor.build_quantum_graph(pipeline)
591 def test_build_quantum_graph_nowhere_noskip_clobber(self):
592 executor = SeparablePipelineExecutor(self.butler, skip_existing_in=None, clobber_output=True)
593 pipeline = Pipeline.fromFile(self.pipeline_file)
595 self.butler.put({"zero": 0}, "input")
597 graph = executor.make_quantum_graph(pipeline)
598 self.assertTrue(graph.isConnected)
599 self.assertEqual(len(graph), 2)
600 self.assertEqual({q.taskDef.label for q in graph.inputQuanta}, {"a"})
601 self.assertEqual({q.taskDef.label for q in graph.outputQuanta}, {"b"})
603 def test_make_quantum_graph_nowhere_noskip_clobber(self):
604 executor = SeparablePipelineExecutor(self.butler, skip_existing_in=None, clobber_output=True)
605 pipeline = Pipeline.fromFile(self.pipeline_file)
606 self.butler.put({"zero": 0}, "input")
607 graph = executor.build_quantum_graph(pipeline)
608 self.assertEqual(len(graph), 2)
609 self.assertEqual(graph.quanta_by_task.keys(), {"a", "b"})
611 def test_build_quantum_graph_nowhere_noskip_clobber_conflict(self):
612 executor = SeparablePipelineExecutor(self.butler, skip_existing_in=None, clobber_output=True)
613 pipeline = Pipeline.fromFile(self.pipeline_file)
615 self.butler.put({"zero": 0}, "input")
616 self.butler.put({"zero": 0}, "intermediate")
617 self.butler.put(lsst.daf.butler.ButlerLogRecords.from_records([]), "a_log")
618 self.butler.put(TaskMetadata(), "a_metadata")
620 graph = executor.make_quantum_graph(pipeline)
621 self.assertTrue(graph.isConnected)
622 self.assertEqual(len(graph), 2)
623 self.assertEqual({q.taskDef.label for q in graph.inputQuanta}, {"a"})
624 self.assertEqual({q.taskDef.label for q in graph.outputQuanta}, {"b"})
626 def test_make_quantum_graph_nowhere_noskip_clobber_conflict(self):
627 executor = SeparablePipelineExecutor(self.butler, skip_existing_in=None, clobber_output=True)
628 pipeline = Pipeline.fromFile(self.pipeline_file)
629 self.butler.put({"zero": 0}, "input")
630 self.butler.put({"zero": 0}, "intermediate")
631 self.butler.put(lsst.daf.butler.ButlerLogRecords.from_records([]), "a_log")
632 self.butler.put(TaskMetadata(), "a_metadata")
633 graph = executor.build_quantum_graph(pipeline)
634 self.assertEqual(len(graph), 2)
635 self.assertEqual(graph.quanta_by_task.keys(), {"a", "b"})
637 def test_build_quantum_graph_nowhere_skipnone_clobber(self):
638 executor = SeparablePipelineExecutor(
639 self.butler,
640 skip_existing_in=[self.butler.run],
641 clobber_output=True,
642 )
643 pipeline = Pipeline.fromFile(self.pipeline_file)
645 self.butler.put({"zero": 0}, "input")
647 graph = executor.make_quantum_graph(pipeline)
648 self.assertTrue(graph.isConnected)
649 self.assertEqual(len(graph), 2)
650 self.assertEqual({q.taskDef.label for q in graph.inputQuanta}, {"a"})
651 self.assertEqual({q.taskDef.label for q in graph.outputQuanta}, {"b"})
653 def test_make_quantum_graph_nowhere_skipnone_clobber(self):
654 executor = SeparablePipelineExecutor(
655 self.butler,
656 skip_existing_in=[self.butler.run],
657 clobber_output=True,
658 )
659 pipeline = Pipeline.fromFile(self.pipeline_file)
660 self.butler.put({"zero": 0}, "input")
661 graph = executor.build_quantum_graph(pipeline)
662 self.assertEqual(len(graph), 2)
663 self.assertEqual(graph.quanta_by_task.keys(), {"a", "b"})
665 def test_make_quantum_graph_nowhere_skiptotal_clobber(self):
666 executor = SeparablePipelineExecutor(
667 self.butler,
668 skip_existing_in=[self.butler.run],
669 clobber_output=True,
670 )
671 pipeline = Pipeline.fromFile(self.pipeline_file)
673 self.butler.put({"zero": 0}, "input")
674 self.butler.put({"zero": 0}, "intermediate")
675 self.butler.put(lsst.daf.butler.ButlerLogRecords.from_records([]), "a_log")
676 self.butler.put(TaskMetadata(), "a_metadata")
677 self.butler.put(lsst.pex.config.Config(), "a_config")
679 graph = executor.make_quantum_graph(pipeline)
680 self.assertTrue(graph.isConnected)
681 self.assertEqual(len(graph), 1)
682 self.assertEqual({q.taskDef.label for q in graph.inputQuanta}, {"b"})
683 self.assertEqual({q.taskDef.label for q in graph.outputQuanta}, {"b"})
685 def test_build_quantum_graph_nowhere_skiptotal_clobber(self):
686 executor = SeparablePipelineExecutor(
687 self.butler,
688 skip_existing_in=[self.butler.run],
689 clobber_output=True,
690 )
691 pipeline = Pipeline.fromFile(self.pipeline_file)
693 self.butler.put({"zero": 0}, "input")
694 self.butler.put({"zero": 0}, "intermediate")
695 self.butler.put(lsst.daf.butler.ButlerLogRecords.from_records([]), "a_log")
696 self.butler.put(TaskMetadata(), "a_metadata")
697 self.butler.put(lsst.pex.config.Config(), "a_config")
699 graph = executor.make_quantum_graph(pipeline)
700 self.assertTrue(graph.isConnected)
701 self.assertEqual(len(graph), 1)
702 self.assertEqual({q.taskDef.label for q in graph.inputQuanta}, {"b"})
703 self.assertEqual({q.taskDef.label for q in graph.outputQuanta}, {"b"})
705 def test_make_quantum_graph_nowhere_skippartial_clobber(self):
706 executor = SeparablePipelineExecutor(
707 self.butler,
708 skip_existing_in=[self.butler.run],
709 clobber_output=True,
710 )
711 pipeline = Pipeline.fromFile(self.pipeline_file)
712 self.butler.put({"zero": 0}, "input")
713 self.butler.put({"zero": 0}, "intermediate")
714 graph = executor.build_quantum_graph(pipeline)
715 self.assertEqual(len(graph), 2)
716 self.assertEqual(graph.quanta_by_task.keys(), {"a", "b"})
718 def test_make_quantum_graph_nowhere_retained_forces_upstream_rerun(self):
719 """When task b must run and 'intermediate' is not retained,
720 retained_dataset_types forces task a to rerun to regenerate the
721 missing intermediate.
722 """
723 prior_run = "prior_run"
724 self.butler.registry.registerCollection(prior_run, lsst.daf.butler.CollectionType.RUN)
725 executor = SeparablePipelineExecutor(
726 self.butler,
727 skip_existing_in=[prior_run],
728 # Only metadata types are retained; 'intermediate' is not retained.
729 retained_dataset_types=["*_metadata"],
730 )
731 pipeline = Pipeline.fromFile(self.pipeline_file)
732 self.butler.put({"zero": 0}, "input")
733 # Task a metadata present in prior run but 'intermediate' was not
734 # retained.
735 self.butler.put(TaskMetadata(), "a_metadata", run=prior_run)
736 graph = executor.build_quantum_graph(pipeline)
737 self.assertEqual(len(graph), 2)
738 self.assertEqual(graph.quanta_by_task.keys(), {"a", "b"})
740 def test_make_quantum_graph_nowhere_retained_metainway(self):
741 """When an ancestor is forced to rerun but its metadata is already in
742 the output run, OutputExistsError is raised without clobber_output.
743 """
744 executor = SeparablePipelineExecutor(
745 self.butler,
746 skip_existing_in=[self.butler.run],
747 retained_dataset_types=["*_metadata"],
748 )
749 pipeline = Pipeline.fromFile(self.pipeline_file)
750 self.butler.put({"zero": 0}, "input")
751 self.butler.put(TaskMetadata(), "a_metadata")
752 # a_metadata in output run, task b must run (no b_metadata), and
753 # 'intermediate' is not retained -> force task a to run
754 # no clobber -> error.
755 with self.assertRaises(OutputExistsError):
756 executor.build_quantum_graph(pipeline)
758 def test_make_quantum_graph_nowhere_retained_both_skipped(self):
759 """Both tasks are skipped when both have metadata, regardless of which
760 outputs are not retained.
761 """
762 executor = SeparablePipelineExecutor(
763 self.butler,
764 skip_existing_in=[self.butler.run],
765 retained_dataset_types=["*_metadata"],
766 )
767 pipeline = Pipeline.fromFile(self.pipeline_file)
769 butlerTests.addDatasetType(self.butler, "b_metadata", set(), "TaskMetadata")
770 butlerTests.addDatasetType(self.butler, "b_config", set(), "Config")
772 self.butler.put(TaskMetadata(), "a_metadata")
773 self.butler.put(lsst.pex.config.Config(), "a_config")
774 self.butler.put(TaskMetadata(), "b_metadata")
775 self.butler.put(lsst.pex.config.Config(), "b_config")
777 graph = executor.build_quantum_graph(pipeline)
778 self.assertEqual(len(graph), 0)
780 def test_make_quantum_graph_noinput(self):
781 executor = SeparablePipelineExecutor(self.butler)
782 pipeline = Pipeline.fromFile(self.pipeline_file)
784 graph = executor.make_quantum_graph(pipeline)
785 self.assertEqual(len(graph), 0)
787 def test_build_quantum_graph_noinput(self):
788 executor = SeparablePipelineExecutor(self.butler)
789 pipeline = Pipeline.fromFile(self.pipeline_file)
791 graph = executor.build_quantum_graph(pipeline)
792 self.assertEqual(len(graph), 0)
794 def test_make_quantum_graph_alloutput_skip(self):
795 executor = SeparablePipelineExecutor(self.butler, skip_existing_in=[self.butler.run])
796 pipeline = Pipeline.fromFile(self.pipeline_file)
798 butlerTests.addDatasetType(self.butler, "output", set(), "StructuredDataDict")
799 butlerTests.addDatasetType(self.butler, "b_log", set(), "ButlerLogRecords")
800 butlerTests.addDatasetType(self.butler, "b_metadata", set(), "TaskMetadata")
801 butlerTests.addDatasetType(self.butler, "b_config", set(), "Config")
803 self.butler.put({"zero": 0}, "input")
804 self.butler.put({"zero": 0}, "intermediate")
805 self.butler.put(lsst.daf.butler.ButlerLogRecords.from_records([]), "a_log")
806 self.butler.put(TaskMetadata(), "a_metadata")
807 self.butler.put({"zero": 0}, "output")
808 self.butler.put(lsst.daf.butler.ButlerLogRecords.from_records([]), "b_log")
809 self.butler.put(TaskMetadata(), "b_metadata")
810 self.butler.put(lsst.pex.config.Config(), "a_config")
811 self.butler.put(lsst.pex.config.Config(), "b_config")
813 graph = executor.make_quantum_graph(pipeline)
814 self.assertEqual(len(graph), 0)
816 def test_build_quantum_graph_alloutput_skip(self):
817 executor = SeparablePipelineExecutor(self.butler, skip_existing_in=[self.butler.run])
818 pipeline = Pipeline.fromFile(self.pipeline_file)
820 butlerTests.addDatasetType(self.butler, "output", set(), "StructuredDataDict")
821 butlerTests.addDatasetType(self.butler, "b_log", set(), "ButlerLogRecords")
822 butlerTests.addDatasetType(self.butler, "b_metadata", set(), "TaskMetadata")
823 butlerTests.addDatasetType(self.butler, "b_config", set(), "Config")
825 self.butler.put({"zero": 0}, "input")
826 self.butler.put({"zero": 0}, "intermediate")
827 self.butler.put(lsst.daf.butler.ButlerLogRecords.from_records([]), "a_log")
828 self.butler.put(TaskMetadata(), "a_metadata")
829 self.butler.put({"zero": 0}, "output")
830 self.butler.put(lsst.daf.butler.ButlerLogRecords.from_records([]), "b_log")
831 self.butler.put(TaskMetadata(), "b_metadata")
832 self.butler.put(lsst.pex.config.Config(), "a_config")
833 self.butler.put(lsst.pex.config.Config(), "b_config")
835 graph = executor.build_quantum_graph(pipeline)
836 self.assertEqual(len(graph), 0)
838 def test_run_pipeline_noskip_noclobber_fullgraph(self):
839 executor = SeparablePipelineExecutor(self.butler, skip_existing_in=None, clobber_output=False)
840 pipeline = Pipeline.fromFile(self.pipeline_file)
841 self.butler.put({"zero": 0}, "input")
842 graph = executor.make_quantum_graph(pipeline)
843 executor.pre_execute_qgraph(
844 graph,
845 register_dataset_types=True,
846 save_init_outputs=True,
847 save_versions=False,
848 )
850 executor.run_pipeline(graph, provenance_dataset_ref=self.provenance_ref)
851 self.butler.registry.refresh()
852 self.assertEqual(self.butler.get("intermediate"), {"zero": 0, "one": 1})
853 self.assertEqual(self.butler.get("output"), {"zero": 0, "one": 1, "two": 2})
854 self.check_provenance_fullgraph()
856 def test_run_pipeline_noskip_noclobber_fullgraph_old(self):
857 executor = SeparablePipelineExecutor(self.butler, skip_existing_in=None, clobber_output=False)
858 pipeline = Pipeline.fromFile(self.pipeline_file)
859 self.butler.put({"zero": 0}, "input")
860 graph = executor.build_quantum_graph(pipeline)
861 executor.pre_execute_qgraph(
862 graph,
863 register_dataset_types=True,
864 save_init_outputs=True,
865 save_versions=False,
866 )
868 executor.run_pipeline(graph, provenance_dataset_ref=self.provenance_ref)
869 self.butler.registry.refresh()
870 self.assertEqual(self.butler.get("intermediate"), {"zero": 0, "one": 1})
871 self.assertEqual(self.butler.get("output"), {"zero": 0, "one": 1, "two": 2})
872 self.check_provenance_fullgraph()
874 def test_run_pipeline_noskip_noclobber_emptygraph_old(self):
875 old_repo_size = self.butler.registry.queryDatasets(...).count()
877 executor = SeparablePipelineExecutor(self.butler, skip_existing_in=None, clobber_output=False)
878 graph = QuantumGraph({}, universe=self.butler.dimensions)
879 executor.pre_execute_qgraph(
880 graph,
881 register_dataset_types=True,
882 save_init_outputs=True,
883 save_versions=False,
884 )
886 executor.run_pipeline(graph, provenance_dataset_ref=self.provenance_ref)
887 self.butler.registry.refresh()
888 # Empty graph execution should do nothing but write provenance.
889 self.assertEqual(self.butler.registry.queryDatasets(...).count(), old_repo_size + 1)
890 self.check_provenance_emptygraph()
892 def test_run_pipeline_noskip_noclobber_emptygraph(self):
893 old_repo_size = self.butler.registry.queryDatasets(...).count()
895 executor = SeparablePipelineExecutor(self.butler, skip_existing_in=None, clobber_output=False)
896 graph = self.build_empty_quantum_graph()
897 executor.pre_execute_qgraph(
898 graph,
899 register_dataset_types=True,
900 save_init_outputs=True,
901 save_versions=False,
902 )
904 executor.run_pipeline(graph, provenance_dataset_ref=self.provenance_ref)
905 self.butler.registry.refresh()
906 # Empty graph execution should do nothing but write provenance.
907 self.assertEqual(self.butler.registry.queryDatasets(...).count(), old_repo_size + 1)
908 self.check_provenance_emptygraph()
910 def test_run_pipeline_skipnone_noclobber_old(self):
911 executor = SeparablePipelineExecutor(
912 self.butler,
913 skip_existing_in=[self.butler.run],
914 clobber_output=False,
915 )
916 pipeline = Pipeline.fromFile(self.pipeline_file)
917 self.butler.put({"zero": 0}, "input")
918 graph = executor.make_quantum_graph(pipeline)
919 executor.pre_execute_qgraph(
920 graph,
921 register_dataset_types=True,
922 save_init_outputs=True,
923 save_versions=False,
924 )
926 executor.run_pipeline(graph)
927 self.butler.registry.refresh()
928 self.assertEqual(self.butler.get("intermediate"), {"zero": 0, "one": 1})
929 self.assertEqual(self.butler.get("output"), {"zero": 0, "one": 1, "two": 2})
931 def test_run_pipeline_skipnone_noclobber(self):
932 executor = SeparablePipelineExecutor(
933 self.butler,
934 skip_existing_in=[self.butler.run],
935 clobber_output=False,
936 )
937 pipeline = Pipeline.fromFile(self.pipeline_file)
938 self.butler.put({"zero": 0}, "input")
939 graph = executor.build_quantum_graph(pipeline)
940 executor.pre_execute_qgraph(
941 graph,
942 register_dataset_types=True,
943 save_init_outputs=True,
944 save_versions=False,
945 )
947 executor.run_pipeline(graph)
948 self.butler.registry.refresh()
949 self.assertEqual(self.butler.get("intermediate"), {"zero": 0, "one": 1})
950 self.assertEqual(self.butler.get("output"), {"zero": 0, "one": 1, "two": 2})
952 def test_run_pipeline_skiptotal_noclobber_old(self):
953 executor = SeparablePipelineExecutor(
954 self.butler,
955 skip_existing_in=[self.butler.run],
956 clobber_output=False,
957 )
958 pipeline = Pipeline.fromFile(self.pipeline_file)
959 self.butler.put({"zero": 0}, "input")
960 self.butler.put({"zero": 0}, "intermediate")
961 self.butler.put(lsst.daf.butler.ButlerLogRecords.from_records([]), "a_log")
962 self.butler.put(TaskMetadata(), "a_metadata")
963 self.butler.put(lsst.pex.config.Config(), "a_config")
964 graph = executor.make_quantum_graph(pipeline)
965 executor.pre_execute_qgraph(
966 graph,
967 register_dataset_types=True,
968 save_init_outputs=True,
969 save_versions=False,
970 )
972 executor.run_pipeline(graph)
973 self.butler.registry.refresh()
974 self.assertEqual(self.butler.get("output"), {"zero": 0, "two": 2})
976 def test_run_pipeline_skiptotal_noclobber(self):
977 executor = SeparablePipelineExecutor(
978 self.butler,
979 skip_existing_in=[self.butler.run],
980 clobber_output=False,
981 )
982 pipeline = Pipeline.fromFile(self.pipeline_file)
983 self.butler.put({"zero": 0}, "input")
984 self.butler.put({"zero": 0}, "intermediate")
985 self.butler.put(lsst.daf.butler.ButlerLogRecords.from_records([]), "a_log")
986 self.butler.put(TaskMetadata(), "a_metadata")
987 self.butler.put(lsst.pex.config.Config(), "a_config")
988 graph = executor.build_quantum_graph(pipeline)
989 executor.pre_execute_qgraph(
990 graph,
991 register_dataset_types=True,
992 save_init_outputs=True,
993 save_versions=False,
994 )
996 executor.run_pipeline(graph)
997 self.butler.registry.refresh()
998 self.assertEqual(self.butler.get("output"), {"zero": 0, "two": 2})
1000 def test_run_pipeline_noskip_clobber_connected_old(self):
1001 executor = SeparablePipelineExecutor(self.butler, skip_existing_in=None, clobber_output=True)
1002 pipeline = Pipeline.fromFile(self.pipeline_file)
1003 self.butler.put({"zero": 0}, "input")
1004 graph = executor.make_quantum_graph(pipeline)
1005 executor.pre_execute_qgraph(
1006 graph,
1007 register_dataset_types=True,
1008 save_init_outputs=True,
1009 save_versions=False,
1010 )
1012 executor.run_pipeline(graph, provenance_dataset_ref=self.provenance_ref)
1013 self.butler.registry.refresh()
1014 self.assertEqual(self.butler.get("intermediate"), {"zero": 0, "one": 1})
1015 self.assertEqual(self.butler.get("output"), {"zero": 0, "one": 1, "two": 2})
1016 self.check_provenance_fullgraph()
1018 def test_run_pipeline_noskip_clobber_connected(self):
1019 executor = SeparablePipelineExecutor(self.butler, skip_existing_in=None, clobber_output=True)
1020 pipeline = Pipeline.fromFile(self.pipeline_file)
1021 self.butler.put({"zero": 0}, "input")
1022 graph = executor.build_quantum_graph(pipeline)
1023 executor.pre_execute_qgraph(
1024 graph,
1025 register_dataset_types=True,
1026 save_init_outputs=True,
1027 save_versions=False,
1028 )
1030 executor.run_pipeline(graph, provenance_dataset_ref=self.provenance_ref)
1031 self.butler.registry.refresh()
1032 self.assertEqual(self.butler.get("intermediate"), {"zero": 0, "one": 1})
1033 self.assertEqual(self.butler.get("output"), {"zero": 0, "one": 1, "two": 2})
1034 self.check_provenance_fullgraph()
1036 def test_run_pipeline_noskip_clobber_unconnected_old(self):
1037 executor = SeparablePipelineExecutor(self.butler, skip_existing_in=None, clobber_output=True)
1038 pipeline = Pipeline.fromFile(self.pipeline_file)
1039 self.butler.put({"zero": 0}, "input")
1040 self.butler.put({"zero": 0}, "intermediate")
1041 self.butler.put(lsst.daf.butler.ButlerLogRecords.from_records([]), "a_log")
1042 self.butler.put(TaskMetadata(), "a_metadata")
1043 graph = executor.make_quantum_graph(pipeline)
1044 executor.pre_execute_qgraph(
1045 graph,
1046 register_dataset_types=True,
1047 save_init_outputs=True,
1048 save_versions=False,
1049 )
1051 executor.run_pipeline(graph, provenance_dataset_ref=self.provenance_ref)
1052 self.butler.registry.refresh()
1053 self.assertEqual(self.butler.get("intermediate"), {"zero": 0, "one": 1})
1054 # The value of output is undefined; it depends on which task ran first.
1055 self.assertTrue(self.butler.exists("output", {}))
1057 def test_run_pipeline_noskip_clobber_unconnected(self):
1058 executor = SeparablePipelineExecutor(self.butler, skip_existing_in=None, clobber_output=True)
1059 pipeline = Pipeline.fromFile(self.pipeline_file)
1060 self.butler.put({"zero": 0}, "input")
1061 self.butler.put({"zero": 0}, "intermediate")
1062 self.butler.put(lsst.daf.butler.ButlerLogRecords.from_records([]), "a_log")
1063 self.butler.put(TaskMetadata(), "a_metadata")
1064 graph = executor.build_quantum_graph(pipeline)
1065 executor.pre_execute_qgraph(
1066 graph,
1067 register_dataset_types=True,
1068 save_init_outputs=True,
1069 save_versions=False,
1070 )
1072 executor.run_pipeline(graph, provenance_dataset_ref=self.provenance_ref)
1073 self.butler.registry.refresh()
1074 self.assertEqual(self.butler.get("intermediate"), {"zero": 0, "one": 1})
1075 # The value of output is undefined; it depends on which task ran first.
1076 self.assertTrue(self.butler.exists("output", {}))
1077 self.check_provenance_fullgraph()
1079 def test_run_pipeline_skipnone_clobber_old(self):
1080 executor = SeparablePipelineExecutor(
1081 self.butler,
1082 skip_existing_in=[self.butler.run],
1083 clobber_output=True,
1084 )
1085 pipeline = Pipeline.fromFile(self.pipeline_file)
1086 self.butler.put({"zero": 0}, "input")
1087 graph = executor.make_quantum_graph(pipeline)
1088 executor.pre_execute_qgraph(
1089 graph,
1090 register_dataset_types=True,
1091 save_init_outputs=True,
1092 save_versions=False,
1093 )
1095 executor.run_pipeline(graph)
1096 self.butler.registry.refresh()
1097 self.assertEqual(self.butler.get("intermediate"), {"zero": 0, "one": 1})
1098 self.assertEqual(self.butler.get("output"), {"zero": 0, "one": 1, "two": 2})
1100 def test_run_pipeline_skipnone_clobber(self):
1101 executor = SeparablePipelineExecutor(
1102 self.butler,
1103 skip_existing_in=[self.butler.run],
1104 clobber_output=True,
1105 )
1106 pipeline = Pipeline.fromFile(self.pipeline_file)
1107 self.butler.put({"zero": 0}, "input")
1108 graph = executor.build_quantum_graph(pipeline)
1109 executor.pre_execute_qgraph(
1110 graph,
1111 register_dataset_types=True,
1112 save_init_outputs=True,
1113 save_versions=False,
1114 )
1116 executor.run_pipeline(graph)
1117 self.butler.registry.refresh()
1118 self.assertEqual(self.butler.get("intermediate"), {"zero": 0, "one": 1})
1119 self.assertEqual(self.butler.get("output"), {"zero": 0, "one": 1, "two": 2})
1121 def test_run_pipeline_skiptotal_clobber_connected_old(self):
1122 executor = SeparablePipelineExecutor(
1123 self.butler,
1124 skip_existing_in=[self.butler.run],
1125 clobber_output=True,
1126 )
1127 pipeline = Pipeline.fromFile(self.pipeline_file)
1128 self.butler.put({"zero": 0}, "input")
1129 self.butler.put({"zero": 0}, "intermediate")
1130 self.butler.put(lsst.daf.butler.ButlerLogRecords.from_records([]), "a_log")
1131 self.butler.put(TaskMetadata(), "a_metadata")
1132 self.butler.put(lsst.pex.config.Config(), "a_config")
1133 graph = executor.make_quantum_graph(pipeline)
1134 executor.pre_execute_qgraph(
1135 graph,
1136 register_dataset_types=True,
1137 save_init_outputs=True,
1138 save_versions=False,
1139 )
1141 executor.run_pipeline(graph)
1142 self.butler.registry.refresh()
1143 self.assertEqual(self.butler.get("output"), {"zero": 0, "two": 2})
1145 def test_run_pipeline_skiptotal_clobber_connected(self):
1146 executor = SeparablePipelineExecutor(
1147 self.butler,
1148 skip_existing_in=[self.butler.run],
1149 clobber_output=True,
1150 )
1151 pipeline = Pipeline.fromFile(self.pipeline_file)
1152 self.butler.put({"zero": 0}, "input")
1153 self.butler.put({"zero": 0}, "intermediate")
1154 self.butler.put(lsst.daf.butler.ButlerLogRecords.from_records([]), "a_log")
1155 self.butler.put(TaskMetadata(), "a_metadata")
1156 self.butler.put(lsst.pex.config.Config(), "a_config")
1157 graph = executor.build_quantum_graph(pipeline)
1158 executor.pre_execute_qgraph(
1159 graph,
1160 register_dataset_types=True,
1161 save_init_outputs=True,
1162 save_versions=False,
1163 )
1165 executor.run_pipeline(graph)
1166 self.butler.registry.refresh()
1167 self.assertEqual(self.butler.get("output"), {"zero": 0, "two": 2})
1169 def test_run_pipeline_skippartial_clobber_unconnected_old(self):
1170 executor = SeparablePipelineExecutor(
1171 self.butler,
1172 skip_existing_in=[self.butler.run],
1173 clobber_output=True,
1174 )
1175 pipeline = Pipeline.fromFile(self.pipeline_file)
1176 self.butler.put({"zero": 0}, "input")
1177 self.butler.put({"zero": 0}, "intermediate")
1178 graph = executor.make_quantum_graph(pipeline)
1179 executor.pre_execute_qgraph(
1180 graph,
1181 register_dataset_types=True,
1182 save_init_outputs=True,
1183 save_versions=False,
1184 )
1186 executor.run_pipeline(graph)
1187 self.butler.registry.refresh()
1188 self.assertEqual(self.butler.get("intermediate"), {"zero": 0, "one": 1})
1189 # The value of output is undefined; it depends on which task ran first.
1190 self.assertTrue(self.butler.exists("output", {}))
1192 def test_run_pipeline_skippartial_clobber_unconnected(self):
1193 executor = SeparablePipelineExecutor(
1194 self.butler,
1195 skip_existing_in=[self.butler.run],
1196 clobber_output=True,
1197 )
1198 pipeline = Pipeline.fromFile(self.pipeline_file)
1199 self.butler.put({"zero": 0}, "input")
1200 self.butler.put({"zero": 0}, "intermediate")
1201 graph = executor.build_quantum_graph(pipeline)
1202 executor.pre_execute_qgraph(
1203 graph,
1204 register_dataset_types=True,
1205 save_init_outputs=True,
1206 save_versions=False,
1207 )
1208 executor.run_pipeline(graph)
1209 self.butler.registry.refresh()
1210 self.assertEqual(self.butler.get("intermediate"), {"zero": 0, "one": 1})
1211 # The value of output is undefined; it depends on which task ran first.
1212 self.assertTrue(self.butler.exists("output", {}))
1215class SeparablePipelineExecutorMockTests(lsst.utils.tests.TestCase):
1216 """Additional tests for SeparablePipelineExecutor API that use
1217 the lsst.pipe.base.tests.mocks system to define complex pipelines.
1218 """
1220 def setUp(self):
1221 # 'base.yaml' adds an instrument, 'Cam1', with four detectors and
1222 # two physical filters.
1223 self.helper, _ = self.enterContext(DirectButlerRepo.make_temporary("base.yaml"))
1225 def run_base_test(
1226 self, b_config: DynamicTestPipelineTaskConfig, expected_error: type[Exception] | None
1227 ) -> ProvenanceQuantumGraph:
1228 """Build and run a quantum graph with three tasks and four data IDs,
1229 with customization of the middle task.
1230 """
1231 self.helper.add_task("a", dimensions=["detector"])
1232 self.helper.add_task("b", dimensions=["detector"], config=b_config)
1233 self.helper.add_task("c", dimensions=["detector"])
1234 qg = self.helper.make_quantum_graph()
1235 self.helper.butler.collections.register(qg.header.output_run)
1236 qg.init_output_run(self.helper.butler, existing=False)
1237 executor = SeparablePipelineExecutor(
1238 self.helper.butler.clone(collections=qg.header.inputs, run=qg.header.output_run)
1239 )
1240 provenance_type = lsst.daf.butler.DatasetType(
1241 PROVENANCE_DATASET_TYPE_NAME,
1242 self.helper.butler.dimensions.empty,
1243 PROVENANCE_STORAGE_CLASS,
1244 )
1245 self.helper.butler.registry.registerDatasetType(provenance_type)
1246 provenance_ref = lsst.daf.butler.DatasetRef(
1247 provenance_type,
1248 lsst.daf.butler.DataCoordinate.make_empty(self.helper.butler.dimensions),
1249 run=qg.header.output_run,
1250 )
1251 if expected_error is None:
1252 executor.run_pipeline(qg, provenance_dataset_ref=provenance_ref)
1253 else:
1254 with self.assertRaises(expected_error):
1255 executor.run_pipeline(qg, provenance_dataset_ref=provenance_ref)
1256 provenance_graph = self.helper.butler.get(provenance_ref)
1257 self.assertEqual(len(provenance_graph.quanta_by_task), 3)
1258 self.assertEqual(len(provenance_graph.quanta_by_task["a"]), 4)
1259 self.assertEqual(len(provenance_graph.quanta_by_task["b"]), 4)
1260 self.assertEqual(len(provenance_graph.quanta_by_task["c"]), 4)
1261 return provenance_graph
1263 def test_no_work_chain_provenance(self):
1264 """Test provenance recording when a NoWorkFound error chains to
1265 downstream tasks during execution.
1266 """
1267 b_config = DynamicTestPipelineTaskConfig()
1268 b_config.fail_exception = "lsst.pipe.base.NoWorkFound"
1269 b_config.fail_condition = "detector=2"
1270 provenance_graph = self.run_base_test(b_config, expected_error=None)
1271 xgraph = provenance_graph.quantum_only_xgraph
1272 for quantum_id in provenance_graph.quanta_by_task["a"].values():
1273 self.assertEqual(xgraph.nodes[quantum_id]["status"], QuantumAttemptStatus.SUCCESSFUL)
1274 self.assertEqual(xgraph.nodes[quantum_id]["caveats"], QuantumSuccessCaveats.NO_CAVEATS)
1275 for data_id, quantum_id in provenance_graph.quanta_by_task["b"].items():
1276 self.assertEqual(xgraph.nodes[quantum_id]["status"], QuantumAttemptStatus.SUCCESSFUL)
1277 if data_id["detector"] == 2:
1278 self.assertTrue(xgraph.nodes[quantum_id]["caveats"] & QuantumSuccessCaveats.NO_WORK)
1279 else:
1280 self.assertEqual(xgraph.nodes[quantum_id]["caveats"], QuantumSuccessCaveats.NO_CAVEATS)
1281 for data_id, quantum_id in provenance_graph.quanta_by_task["c"].items():
1282 self.assertEqual(xgraph.nodes[quantum_id]["status"], QuantumAttemptStatus.SUCCESSFUL)
1283 if data_id["detector"] == 2:
1284 self.assertTrue(
1285 xgraph.nodes[quantum_id]["caveats"] & QuantumSuccessCaveats.ADJUST_QUANTUM_RAISED
1286 )
1287 else:
1288 self.assertEqual(xgraph.nodes[quantum_id]["caveats"], QuantumSuccessCaveats.NO_CAVEATS)
1290 def test_failure_block_provenance(self):
1291 """Test provenance recording when an exception blocks one branch of a
1292 QG but not another.
1293 """
1294 # 'base.yaml' adds an instrument, 'Cam1', with four detectors and
1295 # two physical filters.
1296 b_config = DynamicTestPipelineTaskConfig()
1297 b_config.fail_exception = "builtins.RuntimeError"
1298 b_config.fail_condition = "detector=2"
1299 provenance_graph = self.run_base_test(b_config, MPGraphExecutorError)
1300 self.assertEqual(len(provenance_graph.quanta_by_task), 3)
1301 self.assertEqual(len(provenance_graph.quanta_by_task["a"]), 4)
1302 self.assertEqual(len(provenance_graph.quanta_by_task["b"]), 4)
1303 self.assertEqual(len(provenance_graph.quanta_by_task["c"]), 4)
1304 xgraph = provenance_graph.quantum_only_xgraph
1305 for quantum_id in provenance_graph.quanta_by_task["a"].values():
1306 self.assertEqual(xgraph.nodes[quantum_id]["status"], QuantumAttemptStatus.SUCCESSFUL)
1307 self.assertEqual(xgraph.nodes[quantum_id]["caveats"], QuantumSuccessCaveats.NO_CAVEATS)
1308 for data_id, quantum_id in provenance_graph.quanta_by_task["b"].items():
1309 if data_id["detector"] == 2:
1310 self.assertEqual(xgraph.nodes[quantum_id]["status"], QuantumAttemptStatus.FAILED)
1311 else:
1312 self.assertEqual(xgraph.nodes[quantum_id]["status"], QuantumAttemptStatus.SUCCESSFUL)
1313 self.assertEqual(xgraph.nodes[quantum_id]["caveats"], QuantumSuccessCaveats.NO_CAVEATS)
1314 for data_id, quantum_id in provenance_graph.quanta_by_task["c"].items():
1315 if data_id["detector"] == 2:
1316 self.assertEqual(xgraph.nodes[quantum_id]["status"], QuantumAttemptStatus.BLOCKED)
1317 else:
1318 self.assertEqual(xgraph.nodes[quantum_id]["status"], QuantumAttemptStatus.SUCCESSFUL)
1319 self.assertEqual(xgraph.nodes[quantum_id]["caveats"], QuantumSuccessCaveats.NO_CAVEATS)
1322class MemoryTester(lsst.utils.tests.MemoryTestCase):
1323 """Generic test for file leaks."""
1326def setup_module(module):
1327 """Set up the module for pytest.
1329 Parameters
1330 ----------
1331 module : `~types.ModuleType`
1332 Module to set up.
1333 """
1334 lsst.utils.tests.init()
1337if __name__ == "__main__":
1338 lsst.utils.tests.init()
1339 unittest.main()