Coverage for tests/test_separable_pipeline_executor.py: 100%

756 statements  

« prev     ^ index     » next       coverage.py v7.14.3, created at 2026-06-29 15:13 -0700

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/>. 

27 

28 

29import os 

30import tempfile 

31import unittest 

32 

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 

63 

64TESTDIR = os.path.abspath(os.path.dirname(__file__)) 

65 

66 

67class SeparablePipelineExecutorTests(lsst.utils.tests.TestCase): 

68 """Test the SeparablePipelineExecutor API with a trivial task.""" 

69 

70 pipeline_file = os.path.join(TESTDIR, "pipelines", "pipeline_separable.yaml") 

71 

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) 

77 

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 

92 

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 ) 

106 

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() 

112 

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) 

170 

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) 

177 

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) 

184 

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") 

190 

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, {})) 

199 

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) 

206 

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") 

212 

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, {})) 

221 

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) 

234 

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") 

240 

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, {})) 

249 

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) 

262 

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") 

268 

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, {})) 

277 

278 def test_pre_execute_qgraph_empty_old(self): 

279 executor = SeparablePipelineExecutor(self.butler) 

280 graph = QuantumGraph({}, universe=self.butler.dimensions) 

281 

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") 

287 

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, {})) 

296 

297 def test_pre_execute_qgraph_empty(self): 

298 executor = SeparablePipelineExecutor(self.butler) 

299 graph = self.build_empty_quantum_graph() 

300 

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") 

306 

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, {})) 

315 

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) 

321 

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, {})) 

335 

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) 

341 

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, {})) 

355 

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) 

362 

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") 

368 

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, {})) 

377 

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) 

384 

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") 

390 

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, {})) 

399 

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) 

405 

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") 

411 

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, {})) 

420 

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) 

426 

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") 

432 

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, {})) 

441 

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) 

446 

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) 

451 

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"}) 

461 

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"}) 

472 

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) 

476 

477 self.butler.put({"zero": 0}, "input") 

478 

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"}) 

484 

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) 

488 

489 self.butler.put({"zero": 0}, "input") 

490 

491 graph = executor.build_quantum_graph(pipeline) 

492 self.assertEqual(len(graph), 2) 

493 self.assertEqual(graph.quanta_by_task.keys(), {"a", "b"}) 

494 

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) 

498 

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") 

503 

504 with self.assertRaises(OutputExistsError): 

505 executor.build_quantum_graph(pipeline) 

506 

507 # TODO: need more complex task and Butler to test 

508 # make_quantum_graph(where=...) 

509 

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) 

517 

518 self.butler.put({"zero": 0}, "input") 

519 

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"}) 

525 

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"}) 

537 

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) 

545 

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") 

551 

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"}) 

557 

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) 

565 

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") 

571 

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) 

576 

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) 

584 

585 self.butler.put({"zero": 0}, "input") 

586 self.butler.put({"zero": 0}, "intermediate") 

587 

588 with self.assertRaises(OutputExistsError): 

589 executor.build_quantum_graph(pipeline) 

590 

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) 

594 

595 self.butler.put({"zero": 0}, "input") 

596 

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"}) 

602 

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"}) 

610 

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) 

614 

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") 

619 

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"}) 

625 

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"}) 

636 

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) 

644 

645 self.butler.put({"zero": 0}, "input") 

646 

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"}) 

652 

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"}) 

664 

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) 

672 

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") 

678 

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"}) 

684 

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) 

692 

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") 

698 

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"}) 

704 

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"}) 

717 

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"}) 

739 

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) 

757 

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) 

768 

769 butlerTests.addDatasetType(self.butler, "b_metadata", set(), "TaskMetadata") 

770 butlerTests.addDatasetType(self.butler, "b_config", set(), "Config") 

771 

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") 

776 

777 graph = executor.build_quantum_graph(pipeline) 

778 self.assertEqual(len(graph), 0) 

779 

780 def test_make_quantum_graph_noinput(self): 

781 executor = SeparablePipelineExecutor(self.butler) 

782 pipeline = Pipeline.fromFile(self.pipeline_file) 

783 

784 graph = executor.make_quantum_graph(pipeline) 

785 self.assertEqual(len(graph), 0) 

786 

787 def test_build_quantum_graph_noinput(self): 

788 executor = SeparablePipelineExecutor(self.butler) 

789 pipeline = Pipeline.fromFile(self.pipeline_file) 

790 

791 graph = executor.build_quantum_graph(pipeline) 

792 self.assertEqual(len(graph), 0) 

793 

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) 

797 

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") 

802 

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") 

812 

813 graph = executor.make_quantum_graph(pipeline) 

814 self.assertEqual(len(graph), 0) 

815 

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) 

819 

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") 

824 

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") 

834 

835 graph = executor.build_quantum_graph(pipeline) 

836 self.assertEqual(len(graph), 0) 

837 

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 ) 

849 

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() 

855 

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 ) 

867 

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() 

873 

874 def test_run_pipeline_noskip_noclobber_emptygraph_old(self): 

875 old_repo_size = self.butler.registry.queryDatasets(...).count() 

876 

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 ) 

885 

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() 

891 

892 def test_run_pipeline_noskip_noclobber_emptygraph(self): 

893 old_repo_size = self.butler.registry.queryDatasets(...).count() 

894 

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 ) 

903 

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() 

909 

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 ) 

925 

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}) 

930 

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 ) 

946 

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}) 

951 

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 ) 

971 

972 executor.run_pipeline(graph) 

973 self.butler.registry.refresh() 

974 self.assertEqual(self.butler.get("output"), {"zero": 0, "two": 2}) 

975 

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 ) 

995 

996 executor.run_pipeline(graph) 

997 self.butler.registry.refresh() 

998 self.assertEqual(self.butler.get("output"), {"zero": 0, "two": 2}) 

999 

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 ) 

1011 

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() 

1017 

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 ) 

1029 

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() 

1035 

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 ) 

1050 

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", {})) 

1056 

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 ) 

1071 

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() 

1078 

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 ) 

1094 

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}) 

1099 

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 ) 

1115 

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}) 

1120 

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 ) 

1140 

1141 executor.run_pipeline(graph) 

1142 self.butler.registry.refresh() 

1143 self.assertEqual(self.butler.get("output"), {"zero": 0, "two": 2}) 

1144 

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 ) 

1164 

1165 executor.run_pipeline(graph) 

1166 self.butler.registry.refresh() 

1167 self.assertEqual(self.butler.get("output"), {"zero": 0, "two": 2}) 

1168 

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 ) 

1185 

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", {})) 

1191 

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", {})) 

1213 

1214 

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 """ 

1219 

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")) 

1224 

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 

1262 

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) 

1289 

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) 

1320 

1321 

1322class MemoryTester(lsst.utils.tests.MemoryTestCase): 

1323 """Generic test for file leaks.""" 

1324 

1325 

1326def setup_module(module): 

1327 """Set up the module for pytest. 

1328 

1329 Parameters 

1330 ---------- 

1331 module : `~types.ModuleType` 

1332 Module to set up. 

1333 """ 

1334 lsst.utils.tests.init() 

1335 

1336 

1337if __name__ == "__main__": 

1338 lsst.utils.tests.init() 

1339 unittest.main()