Skip to content

Commit d45bce2

Browse files
authored
Merge pull request #367 from lsst/tickets/DM-55320
DM-55320: Add a feature to prune unanchored quanta in qgraph building
2 parents e114a2b + f8b3ca8 commit d45bce2

5 files changed

Lines changed: 105 additions & 0 deletions

File tree

doc/changes/DM-55320.feature.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add --prune-unanchored-quanta option to pipetask qgraph

python/lsst/ctrl/mpexec/cli/opt/optionGroups.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ def __init__(
128128
ctrlMpExecOpts.skip_existing_in_option(),
129129
ctrlMpExecOpts.skip_existing_option(),
130130
ctrlMpExecOpts.retained_dataset_types_option(),
131+
ctrlMpExecOpts.prune_unanchored_quanta_option(),
131132
ctrlMpExecOpts.save_qgraph_option(),
132133
ctrlMpExecOpts.qgraph_dot_option(),
133134
ctrlMpExecOpts.qgraph_mermaid_option(),

python/lsst/ctrl/mpexec/cli/opt/options.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,56 @@
375375
)
376376

377377

378+
def parse_prune_unanchored_quanta(
379+
ctx: click.Context, param: click.Option, value: str | None
380+
) -> tuple[str, str] | None:
381+
"""Parse the --prune-unanchored-quanta option value into a tuple.
382+
383+
Parameters
384+
----------
385+
ctx : `click.Context`
386+
Context provided by Click.
387+
param : `click.Option`
388+
Click option.
389+
value : `str` or `None`
390+
Value from option, expected to be ``SOURCE:ANCHOR`` or `None` if the
391+
option was not provided.
392+
393+
Returns
394+
-------
395+
result : `tuple` [`str`, `str`] or `None`
396+
A ``(source_label, anchor_label)`` tuple, or `None` if ``value`` is
397+
`None`.
398+
399+
Raises
400+
------
401+
click.UsageError
402+
Raised if ``value`` is not `None` and does not match the
403+
``SOURCE:ANCHOR`` format.
404+
"""
405+
if value is None:
406+
return None
407+
parts = value.split(":", 1)
408+
if len(parts) != 2 or not parts[0] or not parts[1]:
409+
raise click.UsageError(
410+
f"Invalid value for --prune-unanchored-quanta: {value!r}; expected SOURCE:ANCHOR."
411+
)
412+
return (parts[0], parts[1])
413+
414+
415+
prune_unanchored_quanta_option = MWOptionDecorator(
416+
"--prune-unanchored-quanta",
417+
callback=parse_prune_unanchored_quanta,
418+
default=None,
419+
metavar="SOURCE:ANCHOR",
420+
help=unwrap(
421+
"""Remove source quanta that have no reachable anchor quantum downstream,
422+
along with their entire downstream chain. Specify as a colon-separated
423+
pair of task labels SOURCE:ANCHOR."""
424+
),
425+
)
426+
427+
378428
clobber_outputs_option = MWOptionDecorator(
379429
"--clobber-outputs",
380430
help=(

python/lsst/ctrl/mpexec/cli/script/qgraph.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ def qgraph(
7070
skip_existing_in: Iterable[str] | None,
7171
skip_existing: bool,
7272
retained_dataset_types: str | None,
73+
prune_unanchored_quanta: tuple[str, str] | None = None,
7374
save_qgraph: ResourcePathExpression | None,
7475
qgraph_dot: str | None,
7576
qgraph_mermaid: str | None,
@@ -128,6 +129,10 @@ def qgraph(
128129
propagates the must-run signal backward through non-retained input
129130
datasets, forcing the upstream quanta that need to regenerate those
130131
intermediates to also run. Has no effect without ``skip_existing_in``.
132+
prune_unanchored_quanta : `tuple` [ `str`, `str` ] or `None`, optional
133+
A ``(source_label, anchor_label)`` pair of task labels. If not `None`,
134+
source quanta with no reachable anchor quantum downstream are removed,
135+
along with their entire downstream chain.
131136
save_qgraph : convertible to `lsst.resources.ResourcePath` or `None`
132137
URI location for saving the quantum graph.
133138
qgraph_dot : `str` or `None`
@@ -322,6 +327,7 @@ def qgraph(
322327
where=data_query,
323328
skip_existing_in=skip_existing_in,
324329
retained_dataset_types=retained_dataset_type_patterns,
330+
prune_unanchored_quanta=prune_unanchored_quanta,
325331
clobber=clobber_outputs,
326332
dataset_query_constraint=DatasetQueryConstraintVariant.fromExpression(
327333
dataset_query_constraint

tests/test_run.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -833,6 +833,53 @@ def test_retained_dataset_types_invalid_yaml_raises(self):
833833
with self.assertRaises(ValueError):
834834
script.qgraph(**{**base_kwargs, "retained_dataset_types": retained_path})
835835

836+
def test_simple_qg_prune_unanchored_anchor_absent(self):
837+
"""With --prune-unanchored-quanta SOURCE:ANCHOR where ANCHOR does not
838+
exist in the pipeline, all source quanta are pruned and the graph is
839+
empty.
840+
"""
841+
with DirectButlerRepo.make_temporary() as (helper, root):
842+
helper.add_task("source")
843+
helper.add_task("anchor")
844+
helper.insert_datasets("dataset_auto0")
845+
kwargs = self._make_run_args(
846+
"-b",
847+
root,
848+
"-i",
849+
helper.input_chain,
850+
"-o",
851+
"output",
852+
"--register-dataset-types",
853+
"--prune-unanchored-quanta",
854+
"source:no_such_task",
855+
pipeline_graph_factory=PipelineGraphFactory(pipeline_graph=helper.pipeline_graph),
856+
)
857+
qg = script.qgraph(**kwargs)
858+
self.assertIsNone(qg)
859+
860+
def test_simple_qg_prune_unanchored_anchor_reachable(self):
861+
"""With --prune-unanchored-quanta SOURCE:ANCHOR where every source
862+
quantum has an anchor quantum downstream, nothing is pruned.
863+
"""
864+
with DirectButlerRepo.make_temporary() as (helper, root):
865+
helper.add_task("source")
866+
helper.add_task("anchor")
867+
helper.insert_datasets("dataset_auto0")
868+
kwargs = self._make_run_args(
869+
"-b",
870+
root,
871+
"-i",
872+
helper.input_chain,
873+
"-o",
874+
"output",
875+
"--register-dataset-types",
876+
"--prune-unanchored-quanta",
877+
"source:anchor",
878+
pipeline_graph_factory=PipelineGraphFactory(pipeline_graph=helper.pipeline_graph),
879+
)
880+
qg = script.qgraph(**kwargs)
881+
self.assertEqual(len(qg), 2)
882+
836883

837884
class CoverageTestCase(unittest.TestCase):
838885
"""Test the coverage context manager."""

0 commit comments

Comments
 (0)