lsst-pipe-base 29.2025.4100__py3-none-any.whl → 29.2025.4300__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. lsst/pipe/base/_status.py +1 -1
  2. lsst/pipe/base/cli/cmd/__init__.py +2 -2
  3. lsst/pipe/base/cli/cmd/commands.py +116 -1
  4. lsst/pipe/base/graph_walker.py +8 -4
  5. lsst/pipe/base/pipeline_graph/_pipeline_graph.py +30 -5
  6. lsst/pipe/base/quantum_graph/__init__.py +1 -0
  7. lsst/pipe/base/quantum_graph/_common.py +2 -1
  8. lsst/pipe/base/quantum_graph/_multiblock.py +41 -7
  9. lsst/pipe/base/quantum_graph/_predicted.py +62 -5
  10. lsst/pipe/base/quantum_graph/_provenance.py +1209 -0
  11. lsst/pipe/base/quantum_graph/aggregator/__init__.py +143 -0
  12. lsst/pipe/base/quantum_graph/aggregator/_communicators.py +981 -0
  13. lsst/pipe/base/quantum_graph/aggregator/_config.py +139 -0
  14. lsst/pipe/base/quantum_graph/aggregator/_ingester.py +312 -0
  15. lsst/pipe/base/quantum_graph/aggregator/_progress.py +208 -0
  16. lsst/pipe/base/quantum_graph/aggregator/_scanner.py +371 -0
  17. lsst/pipe/base/quantum_graph/aggregator/_structs.py +167 -0
  18. lsst/pipe/base/quantum_graph/aggregator/_supervisor.py +225 -0
  19. lsst/pipe/base/quantum_graph/aggregator/_writer.py +593 -0
  20. lsst/pipe/base/resource_usage.py +183 -0
  21. lsst/pipe/base/simple_pipeline_executor.py +4 -1
  22. lsst/pipe/base/tests/util.py +31 -0
  23. lsst/pipe/base/version.py +1 -1
  24. {lsst_pipe_base-29.2025.4100.dist-info → lsst_pipe_base-29.2025.4300.dist-info}/METADATA +1 -1
  25. {lsst_pipe_base-29.2025.4100.dist-info → lsst_pipe_base-29.2025.4300.dist-info}/RECORD +33 -22
  26. {lsst_pipe_base-29.2025.4100.dist-info → lsst_pipe_base-29.2025.4300.dist-info}/WHEEL +0 -0
  27. {lsst_pipe_base-29.2025.4100.dist-info → lsst_pipe_base-29.2025.4300.dist-info}/entry_points.txt +0 -0
  28. {lsst_pipe_base-29.2025.4100.dist-info → lsst_pipe_base-29.2025.4300.dist-info}/licenses/COPYRIGHT +0 -0
  29. {lsst_pipe_base-29.2025.4100.dist-info → lsst_pipe_base-29.2025.4300.dist-info}/licenses/LICENSE +0 -0
  30. {lsst_pipe_base-29.2025.4100.dist-info → lsst_pipe_base-29.2025.4300.dist-info}/licenses/bsd_license.txt +0 -0
  31. {lsst_pipe_base-29.2025.4100.dist-info → lsst_pipe_base-29.2025.4300.dist-info}/licenses/gpl-v3.0.txt +0 -0
  32. {lsst_pipe_base-29.2025.4100.dist-info → lsst_pipe_base-29.2025.4300.dist-info}/top_level.txt +0 -0
  33. {lsst_pipe_base-29.2025.4100.dist-info → lsst_pipe_base-29.2025.4300.dist-info}/zip-safe +0 -0
lsst/pipe/base/_status.py CHANGED
@@ -166,7 +166,7 @@ class QuantumSuccessCaveats(enum.Flag):
166
166
  """
167
167
  return {
168
168
  "+": "at least one predicted output was missing, but not all were",
169
- "*": "all predicated outputs were missing (besides logs and metadata)",
169
+ "*": "all predicted outputs were missing (besides logs and metadata)",
170
170
  "A": "adjustQuantum raised NoWorkFound; a regenerated QG would not include this quantum",
171
171
  "D": "algorithm considers data too bad to be processable",
172
172
  "U": "one or more input dataset was incomplete due to an upstream failure",
@@ -25,6 +25,6 @@
25
25
  # You should have received a copy of the GNU General Public License
26
26
  # along with this program. If not, see <https://www.gnu.org/licenses/>.
27
27
 
28
- __all__ = ["register_instrument", "transfer_from_graph", "zip_from_graph", "retrieve_artifacts_for_quanta"]
28
+ __all__ = ["register_instrument", "transfer_from_graph", "zip_from_graph", "retrieve_artifacts_for_quanta", "aggregate_graph"]
29
29
 
30
- from .commands import register_instrument, retrieve_artifacts_for_quanta, transfer_from_graph, zip_from_graph
30
+ from .commands import (register_instrument, retrieve_artifacts_for_quanta, transfer_from_graph, zip_from_graph, aggregate_graph)
@@ -40,6 +40,7 @@ from lsst.daf.butler.cli.opt import (
40
40
  from lsst.daf.butler.cli.utils import ButlerCommand, split_commas, unwrap
41
41
 
42
42
  from ... import script
43
+ from ...quantum_graph import aggregator
43
44
  from ..opt import instrument_argument, update_output_chain_option
44
45
 
45
46
 
@@ -140,7 +141,7 @@ def zip_from_graph(**kwargs: Any) -> None:
140
141
  "--include-outputs/--no-include-outputs",
141
142
  is_flag=True,
142
143
  default=True,
143
- help="Whether to include outut datasets in retrieval.",
144
+ help="Whether to include output datasets in retrieval.",
144
145
  )
145
146
  @options_file_option()
146
147
  def retrieve_artifacts_for_quanta(**kwargs: Any) -> None:
@@ -153,3 +154,117 @@ def retrieve_artifacts_for_quanta(**kwargs: Any) -> None:
153
154
  """
154
155
  artifacts = script.retrieve_artifacts_for_quanta(**kwargs)
155
156
  print(f"Written {len(artifacts)} artifacts to {kwargs['dest']}.")
157
+
158
+
159
+ _AGGREGATOR_DEFAULTS = aggregator.AggregatorConfig()
160
+
161
+
162
+ @click.command(short_help="Scan for the outputs of an active or completed quantum graph.", cls=ButlerCommand)
163
+ @click.argument("predicted_graph", required=True)
164
+ @repo_argument(required=True, help="Path to the central butler repository.")
165
+ @click.option(
166
+ "-o",
167
+ "--output",
168
+ "output_path",
169
+ default=_AGGREGATOR_DEFAULTS.output_path,
170
+ help=(
171
+ "Path to the output provenance quantum graph. THIS OPTION IS FOR "
172
+ "DEVELOPMENT AND DEBUGGING ONLY. IT MAY BE REMOVED IN THE FUTURE."
173
+ ),
174
+ )
175
+ @click.option(
176
+ "--processes",
177
+ "-j",
178
+ "n_processes",
179
+ default=_AGGREGATOR_DEFAULTS.n_processes,
180
+ type=click.IntRange(min=1),
181
+ help="Number of processes to use.",
182
+ )
183
+ @click.option(
184
+ "--complete/--incomplete",
185
+ "assume_complete",
186
+ default=_AGGREGATOR_DEFAULTS.assume_complete,
187
+ help="Whether execution has completed (and failures cannot be retried).",
188
+ )
189
+ @click.option(
190
+ "--dry-run",
191
+ is_flag=True,
192
+ default=_AGGREGATOR_DEFAULTS.dry_run,
193
+ help="Do not actually perform any central database ingests.",
194
+ )
195
+ @click.option(
196
+ "--interactive-status/--no-interactive-status",
197
+ "interactive_status",
198
+ default=_AGGREGATOR_DEFAULTS.interactive_status,
199
+ help="Use progress bars for status reporting instead of periodic logging.",
200
+ )
201
+ @click.option(
202
+ "--log-status-interval",
203
+ type=int,
204
+ default=_AGGREGATOR_DEFAULTS.log_status_interval,
205
+ help="Interval (in seconds) between periodic logger status updates.",
206
+ )
207
+ @click.option(
208
+ "--register-dataset-types/--no-register-dataset-types",
209
+ default=_AGGREGATOR_DEFAULTS.register_dataset_types,
210
+ help="Register output dataset types.",
211
+ )
212
+ @click.option(
213
+ "--update-output-chain/--no-update-output-chain",
214
+ default=_AGGREGATOR_DEFAULTS.update_output_chain,
215
+ help="Prepend the output RUN collection to the output CHAINED collection.",
216
+ )
217
+ @click.option(
218
+ "--worker-log-dir",
219
+ type=str,
220
+ default=_AGGREGATOR_DEFAULTS.worker_log_dir,
221
+ help="Path to a directory (POSIX only) for parallel worker logs.",
222
+ )
223
+ @click.option(
224
+ "--worker-log-level",
225
+ type=str,
226
+ default=_AGGREGATOR_DEFAULTS.worker_log_level,
227
+ help="Log level for worker processes/threads (use DEBUG for per-quantum messages).",
228
+ )
229
+ @click.option(
230
+ "--zstd-level",
231
+ type=int,
232
+ default=_AGGREGATOR_DEFAULTS.zstd_level,
233
+ help="Compression level for the provenance quantum graph file.",
234
+ )
235
+ @click.option(
236
+ "--zstd-dict-size",
237
+ type=int,
238
+ default=_AGGREGATOR_DEFAULTS.zstd_dict_size,
239
+ help="Size (in bytes) of the ZStandard compression dictionary.",
240
+ )
241
+ @click.option(
242
+ "--zstd-dict-n-inputs",
243
+ type=int,
244
+ default=_AGGREGATOR_DEFAULTS.zstd_dict_n_inputs,
245
+ help=("Number of samples of each type to include in ZStandard compression dictionary training."),
246
+ )
247
+ @click.option(
248
+ "--mock-storage-classes/--no-mock-storage-classes",
249
+ default=_AGGREGATOR_DEFAULTS.mock_storage_classes,
250
+ help="Enable support for storage classes created by the lsst.pipe.base.tests.mocks package.",
251
+ )
252
+ def aggregate_graph(predicted_graph: str, repo: str, **kwargs: Any) -> None:
253
+ """Scan for quantum graph's outputs to gather provenance, ingest datasets
254
+ into the central butler repository, and delete datasets that are no
255
+ longer needed.
256
+ """
257
+ # It'd be nice to allow to the user to provide a path to an
258
+ # AggregatorConfig JSON file for options that weren't provided, but Click
259
+ # 8.1 fundamentally cannot handle flag options that default to None rather
260
+ # than True or False (i.e. so they fall back to the config value when not
261
+ # set). It's not clear whether Click 8.2.x has actually fixed this; Click
262
+ # 8.2.0 tried but caused new problems.
263
+
264
+ config = aggregator.AggregatorConfig(**kwargs)
265
+ try:
266
+ aggregator.aggregate_graph(predicted_graph, repo, config)
267
+ except aggregator.FatalWorkerError as err:
268
+ # When this exception is raised, we'll have already logged the relevant
269
+ # traceback from a separate worker.
270
+ raise click.ClickException(str(err)) from None
@@ -81,10 +81,12 @@ class GraphWalker(Generic[_T]):
81
81
  Parameters
82
82
  ----------
83
83
  key : unspecified
84
- NetworkX key of the node to mark finished.
84
+ NetworkX key of the node to mark finished. Does not need to have
85
+ been returned by the iterator yet.
85
86
  """
86
- self._active.remove(key)
87
87
  self._incomplete.remove(key)
88
+ self._active.discard(key)
89
+ self._ready.discard(key)
88
90
  successors = list(self._xgraph.successors(key))
89
91
  for successor in successors:
90
92
  assert successor not in self._active, (
@@ -102,7 +104,8 @@ class GraphWalker(Generic[_T]):
102
104
  Parameters
103
105
  ----------
104
106
  key : unspecified
105
- NetworkX key of the node to mark as a failure.
107
+ NetworkX key of the node to mark as a failure. Does not need to
108
+ have been returned by the iterator yet.
106
109
 
107
110
  Returns
108
111
  -------
@@ -110,8 +113,9 @@ class GraphWalker(Generic[_T]):
110
113
  NetworkX keys of nodes that were recursive descendants of the
111
114
  failed node, and will hence never be yielded by the iterator.
112
115
  """
113
- self._active.remove(key)
114
116
  self._incomplete.remove(key)
117
+ self._active.discard(key)
118
+ self._ready.discard(key)
115
119
  descendants = list(networkx.dag.descendants(self._xgraph, key))
116
120
  self._xgraph.remove_node(key)
117
121
  self._xgraph.remove_nodes_from(descendants)
@@ -1697,7 +1697,15 @@ class PipelineGraph:
1697
1697
  PACKAGES_INIT_OUTPUT_NAME, self._universe.empty, PACKAGES_INIT_OUTPUT_STORAGE_CLASS
1698
1698
  )
1699
1699
 
1700
- def register_dataset_types(self, butler: Butler, include_packages: bool = True) -> None:
1700
+ def register_dataset_types(
1701
+ self,
1702
+ butler: Butler,
1703
+ include_packages: bool = True,
1704
+ *,
1705
+ include_inputs: bool = True,
1706
+ include_configs: bool = True,
1707
+ include_logs: bool = True,
1708
+ ) -> None:
1701
1709
  """Register all dataset types in a data repository.
1702
1710
 
1703
1711
  Parameters
@@ -1709,11 +1717,28 @@ class PipelineGraph:
1709
1717
  software versions (this is not associated with a task and hence is
1710
1718
  not considered part of the pipeline graph in other respects, but it
1711
1719
  does get written with other provenance datasets).
1712
- """
1713
- dataset_types = [node.dataset_type for node in self.dataset_types.values()]
1720
+ include_inputs : `bool`, optional
1721
+ Whether to register overall-input dataset types as well as outputs.
1722
+ include_configs : `bool`, optional
1723
+ Whether to register task config dataset types.
1724
+ include_logs : `bool`, optional
1725
+ Whether to register task log dataset types.
1726
+ """
1727
+ dataset_types = {
1728
+ node.name: node.dataset_type
1729
+ for node in self.dataset_types.values()
1730
+ if include_inputs or self.producer_of(node.name) is not None
1731
+ }
1714
1732
  if include_packages:
1715
- dataset_types.append(self.packages_dataset_type)
1716
- for dataset_type in dataset_types:
1733
+ dataset_types[self.packages_dataset_type.name] = self.packages_dataset_type
1734
+ if not include_configs:
1735
+ for task_node in self.tasks.values():
1736
+ del dataset_types[task_node.init.config_output.dataset_type_name]
1737
+ if not include_logs:
1738
+ for task_node in self.tasks.values():
1739
+ if task_node.log_output is not None:
1740
+ del dataset_types[task_node.log_output.dataset_type_name]
1741
+ for dataset_type in dataset_types.values():
1717
1742
  butler.registry.registerDatasetType(dataset_type)
1718
1743
 
1719
1744
  def check_dataset_type_registrations(self, butler: Butler, include_packages: bool = True) -> None:
@@ -30,3 +30,4 @@ from __future__ import annotations
30
30
  from ._common import *
31
31
  from ._multiblock import *
32
32
  from ._predicted import *
33
+ from ._provenance import *
@@ -82,6 +82,7 @@ if TYPE_CHECKING:
82
82
  TaskLabel: TypeAlias = str
83
83
  DatasetTypeName: TypeAlias = str
84
84
  ConnectionName: TypeAlias = str
85
+ DatasetIndex: TypeAlias = int
85
86
  QuantumIndex: TypeAlias = int
86
87
  DatastoreName: TypeAlias = str
87
88
  DimensionElementName: TypeAlias = str
@@ -326,7 +327,7 @@ class BaseQuantumGraph(ABC):
326
327
  ----------
327
328
  header : `HeaderModel`
328
329
  Structured metadata for the graph.
329
- pipeline_graph : `..pipeline_graph.PipelineGraph`
330
+ pipeline_graph : `.pipeline_graph.PipelineGraph`
330
331
  Graph of tasks and dataset types. May contain a superset of the tasks
331
332
  and dataset types that actually have quanta and datasets in the quantum
332
333
  graph.
@@ -41,6 +41,7 @@ __all__ = (
41
41
 
42
42
  import dataclasses
43
43
  import logging
44
+ import tempfile
44
45
  import uuid
45
46
  from collections.abc import Iterator
46
47
  from contextlib import contextmanager
@@ -501,13 +502,13 @@ class AddressReader:
501
502
  self.pages.clear()
502
503
  return self.rows
503
504
 
504
- def find(self, key: uuid.UUID) -> AddressRow:
505
- """Read the row for the given UUID.
505
+ def find(self, key: uuid.UUID | int) -> AddressRow:
506
+ """Read the row for the given UUID or integer index.
506
507
 
507
508
  Parameters
508
509
  ----------
509
- key : `uuid.UUID`
510
- UUID to find.
510
+ key : `uuid.UUID` or `int`
511
+ UUID or integer index to find.
511
512
 
512
513
  Returns
513
514
  -------
@@ -517,6 +518,8 @@ class AddressReader:
517
518
  match key:
518
519
  case uuid.UUID():
519
520
  return self._find_uuid(key)
521
+ case int():
522
+ return self._find_index(key)
520
523
  case _:
521
524
  raise TypeError(f"Invalid argument: {key}.")
522
525
 
@@ -546,6 +549,22 @@ class AddressReader:
546
549
  # Ran out of pages to search.
547
550
  raise LookupError(f"Address for {target} not found.")
548
551
 
552
+ def _find_index(self, target: int) -> AddressRow:
553
+ # First shortcut if we've already loaded this row.
554
+ if (row := self.rows_by_index.get(target)) is not None:
555
+ return row
556
+ if target < 0 or target >= self.n_rows:
557
+ raise LookupError(f"Address for index {target} not found.")
558
+ # Since all indexes should be present, we can predict the right page
559
+ # exactly.
560
+ page_index = target // self.rows_per_page
561
+ self._read_page(page_index)
562
+ try:
563
+ return self.rows_by_index[target]
564
+ except KeyError:
565
+ _LOG.debug("Index find failed: %s should have been in page %s.", target, page_index)
566
+ raise LookupError(f"Address for {target} not found.") from None
567
+
549
568
  def _read_page(self, page_index: int, page_stream: BytesIO | None = None) -> bool:
550
569
  page = self.pages[page_index]
551
570
  if page.read:
@@ -594,7 +613,9 @@ class MultiblockWriter:
594
613
 
595
614
  @classmethod
596
615
  @contextmanager
597
- def open_in_zip(cls, zf: zipfile.ZipFile, name: str, int_size: int) -> Iterator[MultiblockWriter]:
616
+ def open_in_zip(
617
+ cls, zf: zipfile.ZipFile, name: str, int_size: int, use_tempfile: bool = False
618
+ ) -> Iterator[MultiblockWriter]:
598
619
  """Open a writer for a file in a zip archive.
599
620
 
600
621
  Parameters
@@ -605,14 +626,26 @@ class MultiblockWriter:
605
626
  Base name for the multi-block file; an extension will be added.
606
627
  int_size : `int`
607
628
  Number of bytes to use for all integers.
629
+ use_tempfile : `bool`, optional
630
+ If `True`, send writes to a temporary file and only add the file to
631
+ the zip archive when the context manager closes. This involves
632
+ more overall I/O, but it permits multiple multi-block files to be
633
+ open for writing in the same zip archive at once.
608
634
 
609
635
  Returns
610
636
  -------
611
637
  writer : `contextlib.AbstractContextManager` [ `MultiblockWriter` ]
612
638
  Context manager that returns a writer when entered.
613
639
  """
614
- with zf.open(f"{name}.mb", mode="w", force_zip64=True) as stream:
615
- yield MultiblockWriter(stream, int_size)
640
+ filename = f"{name}.mb"
641
+ if use_tempfile:
642
+ with tempfile.NamedTemporaryFile(suffix=filename) as tmp:
643
+ yield MultiblockWriter(tmp, int_size)
644
+ tmp.flush()
645
+ zf.write(tmp.name, filename)
646
+ else:
647
+ with zf.open(f"{name}.mb", mode="w", force_zip64=True) as stream:
648
+ yield MultiblockWriter(stream, int_size)
616
649
 
617
650
  def write_bytes(self, id: uuid.UUID, data: bytes) -> Address:
618
651
  """Write raw bytes to the multi-block file.
@@ -629,6 +662,7 @@ class MultiblockWriter:
629
662
  address : `Address`
630
663
  Address of the bytes just written.
631
664
  """
665
+ assert id not in self.addresses, "Duplicate write to multi-block file detected."
632
666
  self.stream.write(len(data).to_bytes(self.int_size))
633
667
  self.stream.write(data)
634
668
  block_size = len(data) + self.int_size
@@ -347,8 +347,21 @@ class PredictedQuantumDatasetsModel(pydantic.BaseModel):
347
347
  the data repository.
348
348
  """
349
349
 
350
- def iter_dataset_ids(self) -> Iterator[uuid.UUID]:
351
- """Return an iterator over the UUIDs of all datasets referenced by this
350
+ def iter_input_dataset_ids(self) -> Iterator[uuid.UUID]:
351
+ """Return an iterator over the UUIDs of all datasets consumed by this
352
+ quantum.
353
+
354
+ Returns
355
+ -------
356
+ iter : `~collections.abc.Iterator` [ `uuid.UUID` ]
357
+ Iterator over dataset IDs.
358
+ """
359
+ for datasets in self.inputs.values():
360
+ for dataset in datasets:
361
+ yield dataset.dataset_id
362
+
363
+ def iter_output_dataset_ids(self) -> Iterator[uuid.UUID]:
364
+ """Return an iterator over the UUIDs of all datasets produced by this
352
365
  quantum.
353
366
 
354
367
  Returns
@@ -356,10 +369,22 @@ class PredictedQuantumDatasetsModel(pydantic.BaseModel):
356
369
  iter : `~collections.abc.Iterator` [ `uuid.UUID` ]
357
370
  Iterator over dataset IDs.
358
371
  """
359
- for datasets in itertools.chain(self.inputs.values(), self.outputs.values()):
372
+ for datasets in self.outputs.values():
360
373
  for dataset in datasets:
361
374
  yield dataset.dataset_id
362
375
 
376
+ def iter_dataset_ids(self) -> Iterator[uuid.UUID]:
377
+ """Return an iterator over the UUIDs of all datasets referenced by this
378
+ quantum.
379
+
380
+ Returns
381
+ -------
382
+ iter : `~collections.abc.Iterator` [ `uuid.UUID` ]
383
+ Iterator over dataset IDs.
384
+ """
385
+ yield from self.iter_input_dataset_ids()
386
+ yield from self.iter_output_dataset_ids()
387
+
363
388
  def deserialize_datastore_records(self) -> dict[DatastoreName, DatastoreRecordData]:
364
389
  """Deserialize the mapping of datastore records."""
365
390
  return {
@@ -774,7 +799,7 @@ class PredictedQuantumGraph(BaseQuantumGraph):
774
799
  Approximate number of bytes to read at once from address files.
775
800
  Note that this does not set a page size for *all* reads, but it
776
801
  does affect the smallest, most numerous reads.
777
- import_mode : `..pipeline_graph.TaskImportMode`, optional
802
+ import_mode : `.pipeline_graph.TaskImportMode`, optional
778
803
  How to handle importing the task classes referenced in the pipeline
779
804
  graph.
780
805
 
@@ -1498,6 +1523,38 @@ class PredictedQuantumGraphComponents:
1498
1523
  This does include special "init" quanta.
1499
1524
  """
1500
1525
 
1526
+ def make_dataset_ref(self, predicted: PredictedDatasetModel) -> DatasetRef:
1527
+ """Make a `lsst.daf.butler.DatasetRef` from information in the
1528
+ predicted quantum graph.
1529
+
1530
+ Parameters
1531
+ ----------
1532
+ predicted : `PredictedDatasetModel`
1533
+ Model for the dataset in the predicted graph.
1534
+
1535
+ Returns
1536
+ -------
1537
+ ref : `lsst.daf.butler.DatasetRef`
1538
+ A dataset reference. Data ID will be expanded if and only if
1539
+ the dimension data has been loaded.
1540
+ """
1541
+ try:
1542
+ dataset_type = self.pipeline_graph.dataset_types[predicted.dataset_type_name].dataset_type
1543
+ except KeyError:
1544
+ if predicted.dataset_type_name == acc.PACKAGES_INIT_OUTPUT_NAME:
1545
+ dataset_type = self.pipeline_graph.packages_dataset_type
1546
+ else:
1547
+ raise
1548
+ data_id = DataCoordinate.from_full_values(dataset_type.dimensions, tuple(predicted.data_coordinate))
1549
+ if self.dimension_data is not None:
1550
+ (data_id,) = self.dimension_data.attach(dataset_type.dimensions, [data_id])
1551
+ return DatasetRef(
1552
+ dataset_type,
1553
+ data_id,
1554
+ run=predicted.run,
1555
+ id=predicted.dataset_id,
1556
+ )
1557
+
1501
1558
  def set_quantum_indices(self) -> None:
1502
1559
  """Populate the `quantum_indices` component by sorting the UUIDs in the
1503
1560
  `init_quanta` and `quantum_datasets` components (which must both be
@@ -1813,7 +1870,7 @@ class PredictedQuantumGraphReader(BaseQuantumGraphReader):
1813
1870
  Approximate number of bytes to read at once from address files.
1814
1871
  Note that this does not set a page size for *all* reads, but it
1815
1872
  does affect the smallest, most numerous reads.
1816
- import_mode : `..pipeline_graph.TaskImportMode`, optional
1873
+ import_mode : `.pipeline_graph.TaskImportMode`, optional
1817
1874
  How to handle importing the task classes referenced in the pipeline
1818
1875
  graph.
1819
1876