Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# This file is part of ctrl_mpexec. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (http://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 program is free software: you can redistribute it and/or modify 

10# it under the terms of the GNU General Public License as published by 

11# the Free Software Foundation, either version 3 of the License, or 

12# (at your option) any later version. 

13# 

14# This program is distributed in the hope that it will be useful, 

15# but WITHOUT ANY WARRANTY; without even the implied warranty of 

16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

17# GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License 

20# along with this program. If not, see <http://www.gnu.org/licenses/>. 

21 

22"""Module defining CmdLineFwk class and related methods. 

23""" 

24 

25__all__ = ['CmdLineFwk'] 

26 

27# ------------------------------- 

28# Imports of standard modules -- 

29# ------------------------------- 

30import argparse 

31import copy 

32import datetime 

33import fnmatch 

34import getpass 

35import logging 

36import re 

37import sys 

38from typing import Optional, Tuple 

39import warnings 

40 

41# ----------------------------- 

42# Imports for other modules -- 

43# ----------------------------- 

44from lsst.daf.butler import ( 

45 Butler, 

46 CollectionSearch, 

47 CollectionType, 

48 Registry, 

49) 

50from lsst.daf.butler.registry import MissingCollectionError, RegistryDefaults 

51import lsst.pex.config as pexConfig 

52from lsst.pipe.base import GraphBuilder, Pipeline, QuantumGraph, buildExecutionButler 

53from lsst.obs.base import Instrument 

54from .dotTools import graph2dot, pipeline2dot 

55from .executionGraphFixup import ExecutionGraphFixup 

56from .mpGraphExecutor import MPGraphExecutor 

57from .preExecInit import PreExecInit 

58from .singleQuantumExecutor import SingleQuantumExecutor 

59from . import util 

60from lsst.utils import doImport 

61 

62# ---------------------------------- 

63# Local non-exported definitions -- 

64# ---------------------------------- 

65 

66_LOG = logging.getLogger(__name__.partition(".")[2]) 

67 

68 

69class _OutputChainedCollectionInfo: 

70 """A helper class for handling command-line arguments related to an output 

71 `~lsst.daf.butler.CollectionType.CHAINED` collection. 

72 

73 Parameters 

74 ---------- 

75 registry : `lsst.daf.butler.Registry` 

76 Butler registry that collections will be added to and/or queried from. 

77 name : `str` 

78 Name of the collection given on the command line. 

79 """ 

80 def __init__(self, registry: Registry, name: str): 

81 self.name = name 

82 try: 

83 self.chain = tuple(registry.getCollectionChain(name)) 

84 self.exists = True 

85 except MissingCollectionError: 

86 self.chain = () 

87 self.exists = False 

88 

89 def __str__(self): 

90 return self.name 

91 

92 name: str 

93 """Name of the collection provided on the command line (`str`). 

94 """ 

95 

96 exists: bool 

97 """Whether this collection already exists in the registry (`bool`). 

98 """ 

99 

100 chain: Tuple[str, ...] 

101 """The definition of the collection, if it already exists (`tuple` [`str`]). 

102 

103 Empty if the collection does not already exist. 

104 """ 

105 

106 

107class _OutputRunCollectionInfo: 

108 """A helper class for handling command-line arguments related to an output 

109 `~lsst.daf.butler.CollectionType.RUN` collection. 

110 

111 Parameters 

112 ---------- 

113 registry : `lsst.daf.butler.Registry` 

114 Butler registry that collections will be added to and/or queried from. 

115 name : `str` 

116 Name of the collection given on the command line. 

117 """ 

118 def __init__(self, registry: Registry, name: str): 

119 self.name = name 

120 try: 

121 actualType = registry.getCollectionType(name) 

122 if actualType is not CollectionType.RUN: 

123 raise TypeError(f"Collection '{name}' exists but has type {actualType.name}, not RUN.") 

124 self.exists = True 

125 except MissingCollectionError: 

126 self.exists = False 

127 

128 name: str 

129 """Name of the collection provided on the command line (`str`). 

130 """ 

131 

132 exists: bool 

133 """Whether this collection already exists in the registry (`bool`). 

134 """ 

135 

136 

137class _ButlerFactory: 

138 """A helper class for processing command-line arguments related to input 

139 and output collections. 

140 

141 Parameters 

142 ---------- 

143 registry : `lsst.daf.butler.Registry` 

144 Butler registry that collections will be added to and/or queried from. 

145 

146 args : `argparse.Namespace` 

147 Parsed command-line arguments. The following attributes are used, 

148 either at construction or in later methods. 

149 

150 ``output`` 

151 The name of a `~lsst.daf.butler.CollectionType.CHAINED` 

152 input/output collection. 

153 

154 ``output_run`` 

155 The name of a `~lsst.daf.butler.CollectionType.RUN` input/output 

156 collection. 

157 

158 ``extend_run`` 

159 A boolean indicating whether ``output_run`` should already exist 

160 and be extended. 

161 

162 ``replace_run`` 

163 A boolean indicating that (if `True`) ``output_run`` should already 

164 exist but will be removed from the output chained collection and 

165 replaced with a new one. 

166 

167 ``prune_replaced`` 

168 A boolean indicating whether to prune the replaced run (requires 

169 ``replace_run``). 

170 

171 ``inputs`` 

172 Input collections of any type; may be any type handled by 

173 `lsst.daf.butler.registry.CollectionSearch.fromExpression`. 

174 

175 ``butler_config`` 

176 Path to a data repository root or configuration file. 

177 

178 writeable : `bool` 

179 If `True`, a `Butler` is being initialized in a context where actual 

180 writes should happens, and hence no output run is necessary. 

181 

182 Raises 

183 ------ 

184 ValueError 

185 Raised if ``writeable is True`` but there are no output collections. 

186 """ 

187 def __init__(self, registry: Registry, args: argparse.Namespace, writeable: bool): 

188 if args.output is not None: 

189 self.output = _OutputChainedCollectionInfo(registry, args.output) 

190 else: 

191 self.output = None 

192 if args.output_run is not None: 

193 self.outputRun = _OutputRunCollectionInfo(registry, args.output_run) 

194 elif self.output is not None: 

195 if args.extend_run: 

196 runName = self.output.chain[0] 

197 else: 

198 runName = "{}/{}".format(self.output, Instrument.makeCollectionTimestamp()) 

199 self.outputRun = _OutputRunCollectionInfo(registry, runName) 

200 elif not writeable: 

201 # If we're not writing yet, ok to have no output run. 

202 self.outputRun = None 

203 else: 

204 raise ValueError("Cannot write without at least one of (--output, --output-run).") 

205 # Recursively flatten any input CHAINED collections. We do this up 

206 # front so we can tell if the user passes the same inputs on subsequent 

207 # calls, even though we also flatten when we define the output CHAINED 

208 # collection. 

209 self.inputs = tuple(registry.queryCollections(args.input, flattenChains=True)) if args.input else () 

210 

211 def check(self, args: argparse.Namespace): 

212 """Check command-line options for consistency with each other and the 

213 data repository. 

214 

215 Parameters 

216 ---------- 

217 args : `argparse.Namespace` 

218 Parsed command-line arguments. See class documentation for the 

219 construction parameter of the same name. 

220 """ 

221 assert not (args.extend_run and args.replace_run), "In mutually-exclusive group in ArgumentParser." 

222 if self.inputs and self.output is not None and self.output.exists: 

223 # Passing the same inputs that were used to initialize the output 

224 # collection is allowed; this means they must _end_ with the same 

225 # collections, because we push new runs to the front of the chain. 

226 for c1, c2 in zip(self.inputs[::-1], self.output.chain[::-1]): 

227 if c1 != c2: 

228 raise ValueError( 

229 f"Output CHAINED collection {self.output.name!r} exists, but it ends with " 

230 "a different sequence of input collections than those given: " 

231 f"{c1!r} != {c2!r} in inputs={self.inputs} vs " 

232 f"{self.output.name}={self.output.chain}." 

233 ) 

234 if len(self.inputs) > len(self.output.chain): 

235 nNew = len(self.inputs) - len(self.output.chain) 

236 raise ValueError( 

237 f"Cannot add new input collections {self.inputs[:nNew]} after " 

238 "output collection is first created." 

239 ) 

240 if args.extend_run and self.outputRun is None: 

241 raise ValueError("Cannot --extend-run when no output collection is given.") 

242 if args.extend_run and not self.outputRun.exists: 

243 raise ValueError(f"Cannot --extend-run; output collection " 

244 f"'{self.outputRun.name}' does not exist.") 

245 if not args.extend_run and self.outputRun is not None and self.outputRun.exists: 

246 raise ValueError(f"Output run '{self.outputRun.name}' already exists, but " 

247 f"--extend-run was not given.") 

248 if args.prune_replaced and not args.replace_run: 

249 raise ValueError("--prune-replaced requires --replace-run.") 

250 if args.replace_run and (self.output is None or not self.output.exists): 

251 raise ValueError("--output must point to an existing CHAINED collection for --replace-run.") 

252 

253 @classmethod 

254 def _makeReadParts(cls, args: argparse.Namespace): 

255 """Common implementation for `makeReadButler` and 

256 `makeRegistryAndCollections`. 

257 

258 Parameters 

259 ---------- 

260 args : `argparse.Namespace` 

261 Parsed command-line arguments. See class documentation for the 

262 construction parameter of the same name. 

263 

264 Returns 

265 ------- 

266 butler : `lsst.daf.butler.Butler` 

267 A read-only butler constructed from the repo at 

268 ``args.butler_config``, but with no default collections. 

269 inputs : `lsst.daf.butler.registry.CollectionSearch` 

270 A collection search path constructed according to ``args``. 

271 self : `_ButlerFactory` 

272 A new `_ButlerFactory` instance representing the processed version 

273 of ``args``. 

274 """ 

275 butler = Butler(args.butler_config, writeable=False) 

276 self = cls(butler.registry, args, writeable=False) 

277 self.check(args) 

278 if self.output and self.output.exists: 

279 if args.replace_run: 

280 replaced = self.output.chain[0] 

281 inputs = self.output.chain[1:] 

282 _LOG.debug("Simulating collection search in '%s' after removing '%s'.", 

283 self.output.name, replaced) 

284 else: 

285 inputs = [self.output.name] 

286 else: 

287 inputs = list(self.inputs) 

288 if args.extend_run: 

289 inputs.insert(0, self.outputRun.name) 

290 inputs = CollectionSearch.fromExpression(inputs) 

291 return butler, inputs, self 

292 

293 @classmethod 

294 def makeReadButler(cls, args: argparse.Namespace) -> Butler: 

295 """Construct a read-only butler according to the given command-line 

296 arguments. 

297 

298 Parameters 

299 ---------- 

300 args : `argparse.Namespace` 

301 Parsed command-line arguments. See class documentation for the 

302 construction parameter of the same name. 

303 

304 Returns 

305 ------- 

306 butler : `lsst.daf.butler.Butler` 

307 A read-only butler initialized with the collections specified by 

308 ``args``. 

309 """ 

310 butler, inputs, _ = cls._makeReadParts(args) 

311 _LOG.debug("Preparing butler to read from %s.", inputs) 

312 return Butler(butler=butler, collections=inputs) 

313 

314 @classmethod 

315 def makeRegistryAndCollections(cls, args: argparse.Namespace) -> \ 

316 Tuple[Registry, CollectionSearch, Optional[str]]: 

317 """Return a read-only registry, a collection search path, and the name 

318 of the run to be used for future writes. 

319 

320 Parameters 

321 ---------- 

322 args : `argparse.Namespace` 

323 Parsed command-line arguments. See class documentation for the 

324 construction parameter of the same name. 

325 

326 Returns 

327 ------- 

328 registry : `lsst.daf.butler.Registry` 

329 Butler registry that collections will be added to and/or queried 

330 from. 

331 inputs : `lsst.daf.butler.registry.CollectionSearch` 

332 Collections to search for datasets. 

333 run : `str` or `None` 

334 Name of the output `~lsst.daf.butler.CollectionType.RUN` collection 

335 if it already exists, or `None` if it does not. 

336 """ 

337 butler, inputs, self = cls._makeReadParts(args) 

338 run = self.outputRun.name if args.extend_run else None 

339 _LOG.debug("Preparing registry to read from %s and expect future writes to '%s'.", inputs, run) 

340 return butler.registry, inputs, run 

341 

342 @classmethod 

343 def makeWriteButler(cls, args: argparse.Namespace) -> Butler: 

344 """Return a read-write butler initialized to write to and read from 

345 the collections specified by the given command-line arguments. 

346 

347 Parameters 

348 ---------- 

349 args : `argparse.Namespace` 

350 Parsed command-line arguments. See class documentation for the 

351 construction parameter of the same name. 

352 

353 Returns 

354 ------- 

355 butler : `lsst.daf.butler.Butler` 

356 A read-write butler initialized according to the given arguments. 

357 """ 

358 butler = Butler(args.butler_config, writeable=True) 

359 self = cls(butler.registry, args, writeable=True) 

360 self.check(args) 

361 if self.output is not None: 

362 chainDefinition = list(self.output.chain if self.output.exists else self.inputs) 

363 if args.replace_run: 

364 replaced = chainDefinition.pop(0) 

365 if args.prune_replaced == "unstore": 

366 # Remove datasets from datastore 

367 with butler.transaction(): 

368 refs = butler.registry.queryDatasets(..., collections=replaced) 

369 butler.pruneDatasets(refs, unstore=True, run=replaced, disassociate=False) 

370 elif args.prune_replaced == "purge": 

371 # Erase entire collection and all datasets, need to remove 

372 # collection from its chain collection first. 

373 with butler.transaction(): 

374 butler.registry.setCollectionChain(self.output.name, chainDefinition, flatten=True) 

375 butler.pruneCollection(replaced, purge=True, unstore=True) 

376 elif args.prune_replaced is not None: 

377 raise NotImplementedError( 

378 f"Unsupported --prune-replaced option '{args.prune_replaced}'." 

379 ) 

380 if not self.output.exists: 

381 butler.registry.registerCollection(self.output.name, CollectionType.CHAINED) 

382 if not args.extend_run: 

383 butler.registry.registerCollection(self.outputRun.name, CollectionType.RUN) 

384 chainDefinition.insert(0, self.outputRun.name) 

385 butler.registry.setCollectionChain(self.output.name, chainDefinition, flatten=True) 

386 _LOG.debug("Preparing butler to write to '%s' and read from '%s'=%s", 

387 self.outputRun.name, self.output.name, chainDefinition) 

388 butler.registry.defaults = RegistryDefaults(run=self.outputRun.name, collections=self.output.name) 

389 else: 

390 inputs = CollectionSearch.fromExpression((self.outputRun.name,) + self.inputs) 

391 _LOG.debug("Preparing butler to write to '%s' and read from %s.", self.outputRun.name, inputs) 

392 butler.registry.defaults = RegistryDefaults(run=self.outputRun.name, collections=inputs) 

393 return butler 

394 

395 output: Optional[_OutputChainedCollectionInfo] 

396 """Information about the output chained collection, if there is or will be 

397 one (`_OutputChainedCollectionInfo` or `None`). 

398 """ 

399 

400 outputRun: Optional[_OutputRunCollectionInfo] 

401 """Information about the output run collection, if there is or will be 

402 one (`_OutputRunCollectionInfo` or `None`). 

403 """ 

404 

405 inputs: Tuple[str, ...] 

406 """Input collections provided directly by the user (`tuple` [ `str` ]). 

407 """ 

408 

409 

410class _FilteredStream: 

411 """A file-like object that filters some config fields. 

412 

413 Note 

414 ---- 

415 This class depends on implementation details of ``Config.saveToStream`` 

416 methods, in particular that that method uses single call to write() 

417 method to save information about single config field, and that call 

418 combines comments string(s) for a field and field path and value. 

419 This class will not work reliably on the "import" strings, so imports 

420 should be disabled by passing ``skipImports=True`` to ``saveToStream()``. 

421 """ 

422 def __init__(self, pattern): 

423 # obey case if pattern isn't lowercase or requests NOIGNORECASE 

424 mat = re.search(r"(.*):NOIGNORECASE$", pattern) 

425 

426 if mat: 

427 pattern = mat.group(1) 

428 self._pattern = re.compile(fnmatch.translate(pattern)) 

429 else: 

430 if pattern != pattern.lower(): 

431 print(f"Matching \"{pattern}\" without regard to case " 

432 "(append :NOIGNORECASE to prevent this)", file=sys.stdout) 

433 self._pattern = re.compile(fnmatch.translate(pattern), re.IGNORECASE) 

434 

435 def write(self, showStr): 

436 # Strip off doc string line(s) and cut off at "=" for string matching 

437 matchStr = showStr.rstrip().split("\n")[-1].split("=")[0] 

438 if self._pattern.search(matchStr): 

439 sys.stdout.write(showStr) 

440 

441# ------------------------ 

442# Exported definitions -- 

443# ------------------------ 

444 

445 

446class CmdLineFwk: 

447 """PipelineTask framework which executes tasks from command line. 

448 

449 In addition to executing tasks this activator provides additional methods 

450 for task management like dumping configuration or execution chain. 

451 """ 

452 

453 MP_TIMEOUT = 9999 # Default timeout (sec) for multiprocessing 

454 

455 def __init__(self): 

456 pass 

457 

458 def makePipeline(self, args): 

459 """Build a pipeline from command line arguments. 

460 

461 Parameters 

462 ---------- 

463 args : `argparse.Namespace` 

464 Parsed command line 

465 

466 Returns 

467 ------- 

468 pipeline : `~lsst.pipe.base.Pipeline` 

469 """ 

470 if args.pipeline: 

471 pipeline = Pipeline.from_uri(args.pipeline) 

472 else: 

473 pipeline = Pipeline("anonymous") 

474 

475 # loop over all pipeline actions and apply them in order 

476 for action in args.pipeline_actions: 

477 if action.action == "add_instrument": 

478 

479 pipeline.addInstrument(action.value) 

480 

481 elif action.action == "new_task": 

482 

483 pipeline.addTask(action.value, action.label) 

484 

485 elif action.action == "delete_task": 

486 

487 pipeline.removeTask(action.label) 

488 

489 elif action.action == "config": 

490 

491 # action value string is "field=value", split it at '=' 

492 field, _, value = action.value.partition("=") 

493 pipeline.addConfigOverride(action.label, field, value) 

494 

495 elif action.action == "configfile": 

496 

497 pipeline.addConfigFile(action.label, action.value) 

498 

499 else: 

500 

501 raise ValueError(f"Unexpected pipeline action: {action.action}") 

502 

503 if args.save_pipeline: 

504 pipeline.write_to_uri(args.save_pipeline) 

505 

506 if args.pipeline_dot: 

507 pipeline2dot(pipeline, args.pipeline_dot) 

508 

509 return pipeline 

510 

511 def makeGraph(self, pipeline, args): 

512 """Build a graph from command line arguments. 

513 

514 Parameters 

515 ---------- 

516 pipeline : `~lsst.pipe.base.Pipeline` 

517 Pipeline, can be empty or ``None`` if graph is read from a file. 

518 args : `argparse.Namespace` 

519 Parsed command line 

520 

521 Returns 

522 ------- 

523 graph : `~lsst.pipe.base.QuantumGraph` or `None` 

524 If resulting graph is empty then `None` is returned. 

525 """ 

526 

527 registry, collections, run = _ButlerFactory.makeRegistryAndCollections(args) 

528 

529 if args.qgraph: 

530 # click passes empty tuple as default value for qgraph_node_id 

531 nodes = args.qgraph_node_id or None 

532 qgraph = QuantumGraph.loadUri(args.qgraph, registry.dimensions, 

533 nodes=nodes, graphID=args.qgraph_id) 

534 

535 # pipeline can not be provided in this case 

536 if pipeline: 

537 raise ValueError("Pipeline must not be given when quantum graph is read from file.") 

538 

539 else: 

540 

541 # make execution plan (a.k.a. DAG) for pipeline 

542 graphBuilder = GraphBuilder(registry, 

543 skipExisting=args.skip_existing, 

544 clobberOutputs=args.clobber_outputs) 

545 # accumulate metadata 

546 metadata = {"input": args.input, "output": args.output, "butler_argument": args.butler_config, 

547 "output_run": args.output_run, "extend_run": args.extend_run, 

548 "skip_existing": args.skip_existing, "data_query": args.data_query, 

549 "user": getpass.getuser(), "time": f"{datetime.datetime.now()}"} 

550 qgraph = graphBuilder.makeGraph(pipeline, collections, run, args.data_query, metadata=metadata) 

551 

552 # count quanta in graph and give a warning if it's empty and return None 

553 nQuanta = len(qgraph) 

554 if nQuanta == 0: 

555 warnings.warn("QuantumGraph is empty", stacklevel=2) 

556 return None 

557 else: 

558 _LOG.info("QuantumGraph contains %d quanta for %d tasks, graph ID: %r", 

559 nQuanta, len(qgraph.taskGraph), qgraph.graphID) 

560 

561 if args.save_qgraph: 

562 qgraph.saveUri(args.save_qgraph) 

563 

564 if args.save_single_quanta: 

565 for quantumNode in qgraph: 

566 sqgraph = qgraph.subset(quantumNode) 

567 uri = args.save_single_quanta.format(quantumNode.nodeId.number) 

568 sqgraph.saveUri(uri) 

569 

570 if args.qgraph_dot: 

571 graph2dot(qgraph, args.qgraph_dot) 

572 

573 if args.execution_butler_location: 

574 butler = Butler(args.butler_config) 

575 newArgs = copy.deepcopy(args) 

576 

577 def builderShim(butler): 

578 newArgs.butler_config = butler._config 

579 # Calling makeWriteButler is done for the side effects of 

580 # calling that method, maining parsing all the args into 

581 # collection names, creating collections, etc. 

582 newButler = _ButlerFactory.makeWriteButler(newArgs) 

583 return newButler 

584 

585 buildExecutionButler(butler, qgraph, args.execution_butler_location, run, 

586 butlerModifier=builderShim, collections=args.input, 

587 clobber=args.clobber_execution_butler) 

588 

589 return qgraph 

590 

591 def runPipeline(self, graph, taskFactory, args, butler=None): 

592 """Execute complete QuantumGraph. 

593 

594 Parameters 

595 ---------- 

596 graph : `QuantumGraph` 

597 Execution graph. 

598 taskFactory : `~lsst.pipe.base.TaskFactory` 

599 Task factory 

600 args : `argparse.Namespace` 

601 Parsed command line 

602 butler : `~lsst.daf.butler.Butler`, optional 

603 Data Butler instance, if not defined then new instance is made 

604 using command line options. 

605 """ 

606 # make butler instance 

607 if butler is None: 

608 butler = _ButlerFactory.makeWriteButler(args) 

609 

610 # Enable lsstDebug debugging. Note that this is done once in the 

611 # main process before PreExecInit and it is also repeated before 

612 # running each task in SingleQuantumExecutor (which may not be 

613 # needed if `multipocessing` always uses fork start method). 

614 if args.enableLsstDebug: 

615 try: 

616 _LOG.debug("Will try to import debug.py") 

617 import debug # noqa:F401 

618 except ImportError: 

619 _LOG.warn("No 'debug' module found.") 

620 

621 # --skip-existing should have no effect unless --extend-run is passed 

622 # so we make PreExecInit's skipExisting depend on the latter as well. 

623 preExecInit = PreExecInit(butler, taskFactory, skipExisting=(args.skip_existing and args.extend_run)) 

624 preExecInit.initialize(graph, 

625 saveInitOutputs=not args.skip_init_writes, 

626 registerDatasetTypes=args.register_dataset_types, 

627 saveVersions=not args.no_versions) 

628 

629 if not args.init_only: 

630 graphFixup = self._importGraphFixup(args) 

631 quantumExecutor = SingleQuantumExecutor(taskFactory, 

632 skipExisting=args.skip_existing, 

633 clobberOutputs=args.clobber_outputs, 

634 enableLsstDebug=args.enableLsstDebug, 

635 exitOnKnownError=args.fail_fast) 

636 timeout = self.MP_TIMEOUT if args.timeout is None else args.timeout 

637 executor = MPGraphExecutor(numProc=args.processes, timeout=timeout, 

638 startMethod=args.start_method, 

639 quantumExecutor=quantumExecutor, 

640 failFast=args.fail_fast, 

641 executionGraphFixup=graphFixup) 

642 with util.profile(args.profile, _LOG): 

643 executor.execute(graph, butler) 

644 

645 def showInfo(self, args, pipeline, graph=None): 

646 """Display useful info about pipeline and environment. 

647 

648 Parameters 

649 ---------- 

650 args : `argparse.Namespace` 

651 Parsed command line 

652 pipeline : `Pipeline` 

653 Pipeline definition 

654 graph : `QuantumGraph`, optional 

655 Execution graph 

656 """ 

657 showOpts = args.show 

658 for what in showOpts: 

659 showCommand, _, showArgs = what.partition("=") 

660 

661 if showCommand in ["pipeline", "config", "history", "tasks"]: 

662 if not pipeline: 

663 _LOG.warning("Pipeline is required for --show=%s", showCommand) 

664 continue 

665 

666 if showCommand in ["graph", "workflow", "uri"]: 

667 if not graph: 

668 _LOG.warning("QuantumGraph is required for --show=%s", showCommand) 

669 continue 

670 

671 if showCommand == "pipeline": 

672 print(pipeline) 

673 elif showCommand == "config": 

674 self._showConfig(pipeline, showArgs, False) 

675 elif showCommand == "dump-config": 

676 self._showConfig(pipeline, showArgs, True) 

677 elif showCommand == "history": 

678 self._showConfigHistory(pipeline, showArgs) 

679 elif showCommand == "tasks": 

680 self._showTaskHierarchy(pipeline) 

681 elif showCommand == "graph": 

682 if graph: 

683 self._showGraph(graph) 

684 elif showCommand == "uri": 

685 if graph: 

686 self._showUri(graph, args) 

687 elif showCommand == "workflow": 

688 if graph: 

689 self._showWorkflow(graph, args) 

690 else: 

691 print("Unknown value for show: %s (choose from '%s')" % 

692 (what, "', '".join("pipeline config[=XXX] history=XXX tasks graph".split())), 

693 file=sys.stderr) 

694 sys.exit(1) 

695 

696 def _showConfig(self, pipeline, showArgs, dumpFullConfig): 

697 """Show task configuration 

698 

699 Parameters 

700 ---------- 

701 pipeline : `Pipeline` 

702 Pipeline definition 

703 showArgs : `str` 

704 Defines what to show 

705 dumpFullConfig : `bool` 

706 If true then dump complete task configuration with all imports. 

707 """ 

708 stream = sys.stdout 

709 if dumpFullConfig: 

710 # Task label can be given with this option 

711 taskName = showArgs 

712 else: 

713 # The argument can have form [TaskLabel::][pattern:NOIGNORECASE] 

714 matConfig = re.search(r"^(?:(\w+)::)?(?:config.)?(.+)?", showArgs) 

715 taskName = matConfig.group(1) 

716 pattern = matConfig.group(2) 

717 if pattern: 

718 stream = _FilteredStream(pattern) 

719 

720 tasks = util.filterTasks(pipeline, taskName) 

721 if not tasks: 

722 print("Pipeline has no tasks named {}".format(taskName), file=sys.stderr) 

723 sys.exit(1) 

724 

725 for taskDef in tasks: 

726 print("### Configuration for task `{}'".format(taskDef.label)) 

727 taskDef.config.saveToStream(stream, root="config", skipImports=not dumpFullConfig) 

728 

729 def _showConfigHistory(self, pipeline, showArgs): 

730 """Show history for task configuration 

731 

732 Parameters 

733 ---------- 

734 pipeline : `Pipeline` 

735 Pipeline definition 

736 showArgs : `str` 

737 Defines what to show 

738 """ 

739 

740 taskName = None 

741 pattern = None 

742 matHistory = re.search(r"^(?:(\w+)::)?(?:config[.])?(.+)", showArgs) 

743 if matHistory: 

744 taskName = matHistory.group(1) 

745 pattern = matHistory.group(2) 

746 if not pattern: 

747 print("Please provide a value with --show history (e.g. history=Task::param)", file=sys.stderr) 

748 sys.exit(1) 

749 

750 tasks = util.filterTasks(pipeline, taskName) 

751 if not tasks: 

752 print(f"Pipeline has no tasks named {taskName}", file=sys.stderr) 

753 sys.exit(1) 

754 

755 found = False 

756 for taskDef in tasks: 

757 

758 config = taskDef.config 

759 

760 # Look for any matches in the config hierarchy for this name 

761 for nmatch, thisName in enumerate(fnmatch.filter(config.names(), pattern)): 

762 if nmatch > 0: 

763 print("") 

764 

765 cpath, _, cname = thisName.rpartition(".") 

766 try: 

767 if not cpath: 

768 # looking for top-level field 

769 hconfig = taskDef.config 

770 else: 

771 hconfig = eval("config." + cpath, {}, {"config": config}) 

772 except AttributeError: 

773 print(f"Error: Unable to extract attribute {cpath} from task {taskDef.label}", 

774 file=sys.stderr) 

775 hconfig = None 

776 

777 # Sometimes we end up with a non-Config so skip those 

778 if isinstance(hconfig, (pexConfig.Config, pexConfig.ConfigurableInstance)) and \ 

779 hasattr(hconfig, cname): 

780 print(f"### Configuration field for task `{taskDef.label}'") 

781 print(pexConfig.history.format(hconfig, cname)) 

782 found = True 

783 

784 if not found: 

785 print(f"None of the tasks has field matching {pattern}", file=sys.stderr) 

786 sys.exit(1) 

787 

788 def _showTaskHierarchy(self, pipeline): 

789 """Print task hierarchy to stdout 

790 

791 Parameters 

792 ---------- 

793 pipeline: `Pipeline` 

794 """ 

795 for taskDef in pipeline.toExpandedPipeline(): 

796 print("### Subtasks for task `{}'".format(taskDef.taskName)) 

797 

798 for configName, taskName in util.subTaskIter(taskDef.config): 

799 print("{}: {}".format(configName, taskName)) 

800 

801 def _showGraph(self, graph): 

802 """Print quanta information to stdout 

803 

804 Parameters 

805 ---------- 

806 graph : `QuantumGraph` 

807 Execution graph. 

808 """ 

809 for taskNode in graph.taskGraph: 

810 print(taskNode) 

811 

812 for iq, quantum in enumerate(graph.getQuantaForTask(taskNode)): 

813 print(" Quantum {}:".format(iq)) 

814 print(" inputs:") 

815 for key, refs in quantum.inputs.items(): 

816 dataIds = ["DataId({})".format(ref.dataId) for ref in refs] 

817 print(" {}: [{}]".format(key, ", ".join(dataIds))) 

818 print(" outputs:") 

819 for key, refs in quantum.outputs.items(): 

820 dataIds = ["DataId({})".format(ref.dataId) for ref in refs] 

821 print(" {}: [{}]".format(key, ", ".join(dataIds))) 

822 

823 def _showWorkflow(self, graph, args): 

824 """Print quanta information and dependency to stdout 

825 

826 Parameters 

827 ---------- 

828 graph : `QuantumGraph` 

829 Execution graph. 

830 args : `argparse.Namespace` 

831 Parsed command line 

832 """ 

833 for node in graph: 

834 print(f"Quantum {node.nodeId.number}: {node.taskDef.taskName}") 

835 for parent in graph.determineInputsToQuantumNode(node): 

836 print(f"Parent Quantum {parent.nodeId.number} - Child Quantum {node.nodeId.number}") 

837 

838 def _showUri(self, graph, args): 

839 """Print input and predicted output URIs to stdout 

840 

841 Parameters 

842 ---------- 

843 graph : `QuantumGraph` 

844 Execution graph 

845 args : `argparse.Namespace` 

846 Parsed command line 

847 """ 

848 def dumpURIs(thisRef): 

849 primary, components = butler.getURIs(thisRef, predict=True, run="TBD") 

850 if primary: 

851 print(f" {primary}") 

852 else: 

853 print(" (disassembled artifact)") 

854 for compName, compUri in components.items(): 

855 print(f" {compName}: {compUri}") 

856 

857 butler = _ButlerFactory.makeReadButler(args) 

858 for node in graph: 

859 print(f"Quantum {node.nodeId.number}: {node.taskDef.taskName}") 

860 print(" inputs:") 

861 for key, refs in node.quantum.inputs.items(): 

862 for ref in refs: 

863 dumpURIs(ref) 

864 print(" outputs:") 

865 for key, refs in node.quantum.outputs.items(): 

866 for ref in refs: 

867 dumpURIs(ref) 

868 

869 def _importGraphFixup(self, args): 

870 """Import/instantiate graph fixup object. 

871 

872 Parameters 

873 ---------- 

874 args : `argparse.Namespace` 

875 Parsed command line. 

876 

877 Returns 

878 ------- 

879 fixup : `ExecutionGraphFixup` or `None` 

880 

881 Raises 

882 ------ 

883 ValueError 

884 Raised if import fails, method call raises exception, or returned 

885 instance has unexpected type. 

886 """ 

887 if args.graph_fixup: 

888 try: 

889 factory = doImport(args.graph_fixup) 

890 except Exception as exc: 

891 raise ValueError("Failed to import graph fixup class/method") from exc 

892 try: 

893 fixup = factory() 

894 except Exception as exc: 

895 raise ValueError("Failed to make instance of graph fixup") from exc 

896 if not isinstance(fixup, ExecutionGraphFixup): 

897 raise ValueError("Graph fixup is not an instance of ExecutionGraphFixup class") 

898 return fixup