Coverage for python / lsst / pipe / base / pipeline_graph / io.py: 45%

213 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-20 01:37 -0800

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# (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 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 <http://www.gnu.org/licenses/>. 

27from __future__ import annotations 

28 

29__all__ = ( 

30 "SerializedDatasetTypeNode", 

31 "SerializedEdge", 

32 "SerializedPipelineGraph", 

33 "SerializedTaskInitNode", 

34 "SerializedTaskNode", 

35 "SerializedTaskSubset", 

36 "expect_not_none", 

37) 

38 

39from collections.abc import Mapping 

40from typing import Any, TypeVar 

41 

42import networkx 

43import pydantic 

44 

45from lsst.daf.butler import DatasetType, DimensionConfig, DimensionGroup, DimensionUniverse 

46 

47from .. import automatic_connection_constants as acc 

48from ._dataset_types import DatasetTypeNode 

49from ._edges import Edge, ReadEdge, WriteEdge 

50from ._exceptions import PipelineGraphReadError 

51from ._nodes import NodeKey, NodeType 

52from ._pipeline_graph import PipelineGraph 

53from ._task_subsets import StepDefinitions, TaskSubset 

54from ._tasks import TaskImportMode, TaskInitNode, TaskNode 

55 

56_U = TypeVar("_U") 

57 

58_IO_VERSION_INFO = (0, 0, 1) 

59"""Version tuple embedded in saved PipelineGraphs. 

60""" 

61 

62 

63def expect_not_none(value: _U | None, msg: str) -> _U: 

64 """Check that a value is not `None` and return it. 

65 

66 Parameters 

67 ---------- 

68 value : `~typing.Any` 

69 Value to check. 

70 msg : `str` 

71 Error message for the case where ``value is None``. 

72 

73 Returns 

74 ------- 

75 value : `typing.Any` 

76 Value, guaranteed not to be `None`. 

77 

78 Raises 

79 ------ 

80 PipelineGraphReadError 

81 Raised with ``msg`` if ``value is None``. 

82 """ 

83 if value is None: 

84 raise PipelineGraphReadError(msg) 

85 return value 

86 

87 

88class SerializedEdge(pydantic.BaseModel): 

89 """Struct used to represent a serialized `Edge` in a `PipelineGraph`. 

90 

91 All `ReadEdge` and `WriteEdge` state not included here is instead 

92 effectively serialized by the context in which a `SerializedEdge` appears 

93 (e.g. the keys of the nested dictionaries in which it serves as the value 

94 type). 

95 """ 

96 

97 dataset_type_name: str 

98 """Full dataset type name (including component).""" 

99 

100 storage_class: str 

101 """Name of the storage class.""" 

102 

103 raw_dimensions: list[str] 

104 """Raw dimensions of the dataset type from the task connections.""" 

105 

106 is_calibration: bool = False 

107 """Whether this dataset type can be included in 

108 `~lsst.daf.butler.CollectionType.CALIBRATION` collections.""" 

109 

110 defer_query_constraint: bool = False 

111 """If `True`, by default do not include this dataset type's existence as a 

112 constraint on the initial data ID query in QuantumGraph generation.""" 

113 

114 @classmethod 

115 def serialize(cls, target: Edge) -> SerializedEdge: 

116 """Transform an `Edge` to a `SerializedEdge`. 

117 

118 Parameters 

119 ---------- 

120 target : `Edge` 

121 The object to serialize. 

122 

123 Returns 

124 ------- 

125 `SerializedEdge` 

126 Model transformed into something that can be serialized. 

127 """ 

128 return SerializedEdge.model_construct( 

129 storage_class=target.storage_class_name, 

130 dataset_type_name=target.dataset_type_name, 

131 raw_dimensions=sorted(target.raw_dimensions), 

132 is_calibration=target.is_calibration, 

133 defer_query_constraint=getattr(target, "defer_query_constraint", False), 

134 ) 

135 

136 def deserialize_read_edge( 

137 self, 

138 task_key: NodeKey, 

139 connection_name: str, 

140 dataset_type_keys: Mapping[str, NodeKey], 

141 is_prerequisite: bool = False, 

142 ) -> ReadEdge: 

143 """Transform a `SerializedEdge` to a `ReadEdge`. 

144 

145 Parameters 

146 ---------- 

147 task_key : `NodeKey` 

148 Key for the task node this edge is connected to. 

149 connection_name : `str` 

150 Internal name for the connection as seen by the task. 

151 dataset_type_keys : `~collections.abc.Mapping` [`str`, `NodeKey`] 

152 Mapping of dataset type name to node key. 

153 is_prerequisite : `bool`, optional 

154 Whether this dataset must be present in the data repository prior 

155 to `QuantumGraph` generation. 

156 

157 Returns 

158 ------- 

159 `ReadEdge` 

160 Deserialized object. 

161 """ 

162 parent_dataset_type_name, component = DatasetType.splitDatasetTypeName(self.dataset_type_name) 

163 return ReadEdge( 

164 dataset_type_key=dataset_type_keys[parent_dataset_type_name], 

165 task_key=task_key, 

166 storage_class_name=self.storage_class, 

167 is_prerequisite=is_prerequisite, 

168 component=component, 

169 connection_name=connection_name, 

170 is_calibration=self.is_calibration, 

171 defer_query_constraint=self.defer_query_constraint, 

172 raw_dimensions=frozenset(self.raw_dimensions), 

173 ) 

174 

175 def deserialize_write_edge( 

176 self, 

177 task_key: NodeKey, 

178 connection_name: str, 

179 dataset_type_keys: Mapping[str, NodeKey], 

180 ) -> WriteEdge: 

181 """Transform a `SerializedEdge` to a `WriteEdge`. 

182 

183 Parameters 

184 ---------- 

185 task_key : `NodeKey` 

186 Key for the task node this edge is connected to. 

187 connection_name : `str` 

188 Internal name for the connection as seen by the task. 

189 dataset_type_keys : `~collections.abc.Mapping` [`str`, `NodeKey`] 

190 Mapping of dataset type name to node key. 

191 

192 Returns 

193 ------- 

194 `WriteEdge` 

195 Deserialized object. 

196 """ 

197 return WriteEdge( 

198 task_key=task_key, 

199 dataset_type_key=dataset_type_keys[self.dataset_type_name], 

200 storage_class_name=self.storage_class, 

201 connection_name=connection_name, 

202 is_calibration=self.is_calibration, 

203 raw_dimensions=frozenset(self.raw_dimensions), 

204 ) 

205 

206 

207class SerializedTaskInitNode(pydantic.BaseModel): 

208 """Struct used to represent a serialized `TaskInitNode` in a 

209 `PipelineGraph`. 

210 

211 The task label is serialized by the context in which a 

212 `SerializedTaskInitNode` appears (e.g. the keys of the nested dictionary 

213 in which it serves as the value type), and the task class name and config 

214 string are save with the corresponding `SerializedTaskNode`. 

215 """ 

216 

217 inputs: dict[str, SerializedEdge] 

218 """Mapping of serialized init-input edges, keyed by connection name.""" 

219 

220 outputs: dict[str, SerializedEdge] 

221 """Mapping of serialized init-output edges, keyed by connection name.""" 

222 

223 config_output: SerializedEdge 

224 """The serialized config init-output edge.""" 

225 

226 index: int | None = None 

227 """The index of this node in the sorted sequence of `PipelineGraph`. 

228 

229 This is `None` if the `PipelineGraph` was not sorted when it was 

230 serialized. 

231 """ 

232 

233 @classmethod 

234 def serialize(cls, target: TaskInitNode) -> SerializedTaskInitNode: 

235 """Transform a `TaskInitNode` to a `SerializedTaskInitNode`. 

236 

237 Parameters 

238 ---------- 

239 target : `TaskInitNode` 

240 Object to be serialized. 

241 

242 Returns 

243 ------- 

244 `SerializedTaskInitNode` 

245 Model that can be serialized. 

246 """ 

247 return cls.model_construct( 

248 inputs={ 

249 connection_name: SerializedEdge.serialize(edge) 

250 for connection_name, edge in sorted(target.inputs.items()) 

251 }, 

252 outputs={ 

253 connection_name: SerializedEdge.serialize(edge) 

254 for connection_name, edge in sorted(target.outputs.items()) 

255 }, 

256 config_output=SerializedEdge.serialize(target.config_output), 

257 ) 

258 

259 def deserialize( 

260 self, 

261 key: NodeKey, 

262 task_class_name: str, 

263 config_str: str, 

264 dataset_type_keys: Mapping[str, NodeKey], 

265 ) -> TaskInitNode: 

266 """Transform a `SerializedTaskInitNode` to a `TaskInitNode`. 

267 

268 Parameters 

269 ---------- 

270 key : `NodeKey` 

271 Key that identifies this node in internal and exported networkx 

272 graphs. 

273 task_class_name : `str`, optional 

274 Fully-qualified name of the task class. Must be provided if 

275 ``imported_data`` is not. 

276 config_str : `str`, optional 

277 Configuration for the task as a string of override statements. 

278 dataset_type_keys : `~collections.abc.Mapping` [`str`, `NodeKey`] 

279 Mapping of dataset type name to node key. 

280 

281 Returns 

282 ------- 

283 `TaskInitNode` 

284 Deserialized object. 

285 """ 

286 return TaskInitNode( 

287 key, 

288 inputs={ 

289 connection_name: serialized_edge.deserialize_read_edge( 

290 key, connection_name, dataset_type_keys 

291 ) 

292 for connection_name, serialized_edge in self.inputs.items() 

293 }, 

294 outputs={ 

295 connection_name: serialized_edge.deserialize_write_edge( 

296 key, connection_name, dataset_type_keys 

297 ) 

298 for connection_name, serialized_edge in self.outputs.items() 

299 }, 

300 config_output=self.config_output.deserialize_write_edge( 

301 key, acc.CONFIG_INIT_OUTPUT_CONNECTION_NAME, dataset_type_keys 

302 ), 

303 task_class_name=task_class_name, 

304 config_str=config_str, 

305 ) 

306 

307 

308class SerializedTaskNode(pydantic.BaseModel): 

309 """Struct used to represent a serialized `TaskNode` in a `PipelineGraph`. 

310 

311 The task label is serialized by the context in which a 

312 `SerializedTaskNode` appears (e.g. the keys of the nested dictionary in 

313 which it serves as the value type). 

314 """ 

315 

316 task_class: str 

317 """Fully-qualified name of the task class.""" 

318 

319 init: SerializedTaskInitNode 

320 """Serialized task initialization node.""" 

321 

322 config_str: str 

323 """Configuration for the task as a string of override statements.""" 

324 

325 prerequisite_inputs: dict[str, SerializedEdge] 

326 """Mapping of serialized prerequisiste input edges, keyed by connection 

327 name. 

328 """ 

329 

330 inputs: dict[str, SerializedEdge] 

331 """Mapping of serialized input edges, keyed by connection name.""" 

332 

333 outputs: dict[str, SerializedEdge] 

334 """Mapping of serialized output edges, keyed by connection name.""" 

335 

336 metadata_output: SerializedEdge 

337 """The serialized metadata output edge.""" 

338 

339 dimensions: list[str] 

340 """The task's dimensions, if they were resolved.""" 

341 

342 log_output: SerializedEdge | None = None 

343 """The serialized log output edge.""" 

344 

345 index: int | None = None 

346 """The index of this node in the sorted sequence of `PipelineGraph`. 

347 

348 This is `None` if the `PipelineGraph` was not sorted when it was 

349 serialized. 

350 """ 

351 

352 @classmethod 

353 def serialize(cls, target: TaskNode) -> SerializedTaskNode: 

354 """Transform a `TaskNode` to a `SerializedTaskNode`. 

355 

356 Parameters 

357 ---------- 

358 target : `TaskNode` 

359 Object to be serialized. 

360 

361 Returns 

362 ------- 

363 `SerializedTaskNode` 

364 Object that can be serialized. 

365 """ 

366 dimensions = list(target.raw_dimensions) 

367 dimensions.sort() 

368 return cls.model_construct( 

369 task_class=target.task_class_name, 

370 init=SerializedTaskInitNode.serialize(target.init), 

371 config_str=target.get_config_str(), 

372 dimensions=dimensions, 

373 prerequisite_inputs={ 

374 connection_name: SerializedEdge.serialize(edge) 

375 for connection_name, edge in sorted(target.prerequisite_inputs.items()) 

376 }, 

377 inputs={ 

378 connection_name: SerializedEdge.serialize(edge) 

379 for connection_name, edge in sorted(target.inputs.items()) 

380 }, 

381 outputs={ 

382 connection_name: SerializedEdge.serialize(edge) 

383 for connection_name, edge in sorted(target.outputs.items()) 

384 }, 

385 metadata_output=SerializedEdge.serialize(target.metadata_output), 

386 log_output=( 

387 SerializedEdge.serialize(target.log_output) if target.log_output is not None else None 

388 ), 

389 ) 

390 

391 def deserialize( 

392 self, 

393 key: NodeKey, 

394 init_key: NodeKey, 

395 dataset_type_keys: Mapping[str, NodeKey], 

396 universe: DimensionUniverse | None, 

397 ) -> TaskNode: 

398 """Transform a `SerializedTaskNode` to a `TaskNode`. 

399 

400 Parameters 

401 ---------- 

402 key : `NodeKey` 

403 Key that identifies this node in internal and exported networkx 

404 graphs. 

405 init_key : `NodeKey` 

406 Key that identifies the init node in internal and exported networkx 

407 graphs. 

408 dataset_type_keys : `~collections.abc.Mapping` [`str`, `NodeKey`] 

409 Mapping of dataset type name to node key. 

410 universe : `~lsst.daf.butler.DimensionUniverse` or `None` 

411 The dimension universe. 

412 

413 Returns 

414 ------- 

415 `TaskNode` 

416 Deserialized object. 

417 """ 

418 init = self.init.deserialize( 

419 init_key, 

420 task_class_name=self.task_class, 

421 config_str=expect_not_none( 

422 self.config_str, f"No serialized config file for task with label {key.name!r}." 

423 ), 

424 dataset_type_keys=dataset_type_keys, 

425 ) 

426 inputs = { 

427 connection_name: serialized_edge.deserialize_read_edge(key, connection_name, dataset_type_keys) 

428 for connection_name, serialized_edge in self.inputs.items() 

429 } 

430 prerequisite_inputs = { 

431 connection_name: serialized_edge.deserialize_read_edge( 

432 key, connection_name, dataset_type_keys, is_prerequisite=True 

433 ) 

434 for connection_name, serialized_edge in self.prerequisite_inputs.items() 

435 } 

436 outputs = { 

437 connection_name: serialized_edge.deserialize_write_edge(key, connection_name, dataset_type_keys) 

438 for connection_name, serialized_edge in self.outputs.items() 

439 } 

440 if (serialized_log_output := self.log_output) is not None: 

441 log_output = serialized_log_output.deserialize_write_edge( 

442 key, acc.LOG_OUTPUT_CONNECTION_NAME, dataset_type_keys 

443 ) 

444 else: 

445 log_output = None 

446 metadata_output = self.metadata_output.deserialize_write_edge( 

447 key, acc.METADATA_OUTPUT_CONNECTION_NAME, dataset_type_keys 

448 ) 

449 dimensions: frozenset[str] | DimensionGroup 

450 if universe is not None: 

451 dimensions = universe.conform(self.dimensions) 

452 else: 

453 dimensions = frozenset(self.dimensions) 

454 return TaskNode( 

455 key=key, 

456 init=init, 

457 inputs=inputs, 

458 prerequisite_inputs=prerequisite_inputs, 

459 outputs=outputs, 

460 log_output=log_output, 

461 metadata_output=metadata_output, 

462 dimensions=dimensions, 

463 ) 

464 

465 

466class SerializedDatasetTypeNode(pydantic.BaseModel): 

467 """Struct used to represent a serialized `DatasetTypeNode` in a 

468 `PipelineGraph`. 

469 

470 Unresolved dataset types are serialized as instances with at most the 

471 `index` attribute set, and are typically converted to JSON with pydantic's 

472 ``exclude_defaults=True`` option to keep this compact. 

473 

474 The dataset typename is serialized by the context in which a 

475 `SerializedDatasetTypeNode` appears (e.g. the keys of the nested dictionary 

476 in which it serves as the value type). 

477 """ 

478 

479 dimensions: list[str] | None = None 

480 """Dimensions of the dataset type.""" 

481 

482 storage_class: str | None = None 

483 """Name of the storage class.""" 

484 

485 is_calibration: bool = False 

486 """Whether this dataset type is a calibration.""" 

487 

488 is_initial_query_constraint: bool = False 

489 """Whether this dataset type should be a query constraint during 

490 `QuantumGraph` generation.""" 

491 

492 is_prerequisite: bool = False 

493 """Whether datasets of this dataset type must exist in the input collection 

494 before `QuantumGraph` generation.""" 

495 

496 index: int | None = None 

497 """The index of this node in the sorted sequence of `PipelineGraph`. 

498 

499 This is `None` if the `PipelineGraph` was not sorted when it was 

500 serialized. 

501 """ 

502 

503 @classmethod 

504 def serialize(cls, target: DatasetTypeNode | None) -> SerializedDatasetTypeNode: 

505 """Transform a `DatasetTypeNode` to a `SerializedDatasetTypeNode`. 

506 

507 Parameters 

508 ---------- 

509 target : `DatasetTypeNode` or `None` 

510 Object to serialize. 

511 

512 Returns 

513 ------- 

514 `SerializedDatasetTypeNode` 

515 Object in serializable form. 

516 """ 

517 if target is None: 

518 return cls.model_construct() 

519 return cls.model_construct( 

520 dimensions=list(target.dataset_type.dimensions.names), 

521 storage_class=target.dataset_type.storageClass_name, 

522 is_calibration=target.dataset_type.isCalibration(), 

523 is_initial_query_constraint=target.is_initial_query_constraint, 

524 is_prerequisite=target.is_prerequisite, 

525 ) 

526 

527 def deserialize( 

528 self, key: NodeKey, xgraph: networkx.MultiDiGraph, universe: DimensionUniverse | None 

529 ) -> DatasetTypeNode | None: 

530 """Transform a `SerializedDatasetTypeNode` to a `DatasetTypeNode`. 

531 

532 Parameters 

533 ---------- 

534 key : `NodeKey` 

535 Key that identifies this node in internal and exported networkx 

536 graphs. 

537 xgraph : `networkx.MultiDiGraph` 

538 <unknown>. 

539 universe : `~lsst.daf.butler.DimensionUniverse` or `None` 

540 The dimension universe. 

541 

542 Returns 

543 ------- 

544 `DatasetTypeNode` 

545 Deserialized object. 

546 """ 

547 if self.dimensions is not None: 

548 dataset_type = DatasetType( 

549 key.name, 

550 expect_not_none( 

551 self.dimensions, 

552 f"Serialized dataset type {key.name!r} has no dimensions.", 

553 ), 

554 storageClass=expect_not_none( 

555 self.storage_class, 

556 f"Serialized dataset type {key.name!r} has no storage class.", 

557 ), 

558 isCalibration=self.is_calibration, 

559 universe=expect_not_none( 

560 universe, 

561 f"Serialized dataset type {key.name!r} has dimensions, " 

562 "but no dimension universe was stored.", 

563 ), 

564 ) 

565 producer: str | None = None 

566 producing_edge: WriteEdge | None = None 

567 for _, _, producing_edge in xgraph.in_edges(key, data="instance"): 

568 assert producing_edge is not None, "Should only be None if we never loop." 

569 if producer is not None: 

570 raise PipelineGraphReadError( 

571 f"Serialized dataset type {key.name!r} is produced by both " 

572 f"{producing_edge.task_label!r} and {producer!r} in resolved graph." 

573 ) 

574 producer = producing_edge.task_label 

575 consuming_edges = tuple( 

576 consuming_edge for _, _, consuming_edge in xgraph.in_edges(key, data="instance") 

577 ) 

578 return DatasetTypeNode( 

579 dataset_type=dataset_type, 

580 is_prerequisite=self.is_prerequisite, 

581 is_initial_query_constraint=self.is_initial_query_constraint, 

582 producing_edge=producing_edge, 

583 consuming_edges=consuming_edges, 

584 ) 

585 return None 

586 

587 

588class SerializedTaskSubset(pydantic.BaseModel): 

589 """Struct used to represent a serialized `TaskSubset` in a `PipelineGraph`. 

590 

591 The subset label is serialized by the context in which a 

592 `SerializedDatasetTypeNode` appears (e.g. the keys of the nested dictionary 

593 in which it serves as the value type). 

594 """ 

595 

596 description: str 

597 """Description of the subset.""" 

598 

599 tasks: list[str] 

600 """Labels of tasks in the subset, sorted lexicographically for 

601 determinism. 

602 """ 

603 

604 dimensions: list[str] 

605 """Dimensions that can be used to divide this step's quanta into 

606 independent groups. 

607 """ 

608 

609 @classmethod 

610 def serialize(cls, target: TaskSubset) -> SerializedTaskSubset: 

611 """Transform a `TaskSubset` into a `SerializedTaskSubset`. 

612 

613 Parameters 

614 ---------- 

615 target : `TaskSubset` 

616 Object to serialize. 

617 

618 Returns 

619 ------- 

620 `SerializedTaskSubset` 

621 Object in serializable form. 

622 """ 

623 dimensions = sorted(target._step_definitions._dimensions_by_label.get(target.label, ())) 

624 return cls.model_construct( 

625 description=target._description, 

626 tasks=sorted(target), 

627 dimensions=dimensions, 

628 ) 

629 

630 def deserialize_task_subset( 

631 self, label: str, xgraph: networkx.MultiDiGraph, steps: StepDefinitions 

632 ) -> TaskSubset: 

633 """Transform a `SerializedTaskSubset` into a `TaskSubset`. 

634 

635 Parameters 

636 ---------- 

637 label : `str` 

638 Subset label. 

639 xgraph : `networkx.MultiDiGraph` 

640 The under-construction networkx graph that backs the pipeline 

641 graph. 

642 steps : `StepDefinitions` 

643 Step definitions for the pipeline graph. Modified in-place to 

644 set sharding dimension if this is a step. 

645 

646 Returns 

647 ------- 

648 `TaskSubset` 

649 Deserialized object. 

650 """ 

651 members = set(self.tasks) 

652 if label in steps: 

653 steps._dimensions_by_label[label] = frozenset(self.dimensions) 

654 return TaskSubset(xgraph, label, members, self.description, steps) 

655 

656 

657class SerializedPipelineGraph(pydantic.BaseModel): 

658 """Struct used to represent a serialized `PipelineGraph`.""" 

659 

660 version: str = ".".join(str(v) for v in _IO_VERSION_INFO) 

661 """Serialization version.""" 

662 

663 description: str 

664 """Human-readable description of the pipeline.""" 

665 

666 tasks: dict[str, SerializedTaskNode] = pydantic.Field(default_factory=dict) 

667 """Mapping of serialized tasks, keyed by label.""" 

668 

669 dataset_types: dict[str, SerializedDatasetTypeNode] = pydantic.Field(default_factory=dict) 

670 """Mapping of serialized dataset types, keyed by parent dataset type name. 

671 """ 

672 

673 task_subsets: dict[str, SerializedTaskSubset] = pydantic.Field(default_factory=dict) 

674 """Mapping of task subsets, keyed by subset label.""" 

675 

676 dimensions: dict[str, Any] | None = None 

677 """Dimension universe configuration.""" 

678 

679 data_id: dict[str, Any] = pydantic.Field(default_factory=dict) 

680 """Data ID that constrains all quanta generated from this pipeline.""" 

681 

682 step_labels: list[str] = pydantic.Field(default_factory=list) 

683 """List of task subset labels that are steps.""" 

684 

685 steps_verified: bool 

686 """Whether the step definitions in this pipeline were checked for 

687 consistency with the task and subset definitions. 

688 """ 

689 

690 @classmethod 

691 def serialize(cls, target: PipelineGraph) -> SerializedPipelineGraph: 

692 """Transform a `PipelineGraph` into a `SerializedPipelineGraph`. 

693 

694 Parameters 

695 ---------- 

696 target : `PipelineGraph` 

697 Object to serialize. 

698 

699 Returns 

700 ------- 

701 `SerializedPipelineGraph` 

702 Object in serializable form. 

703 """ 

704 result = SerializedPipelineGraph.model_construct( 

705 description=target.description, 

706 tasks={label: SerializedTaskNode.serialize(node) for label, node in target.tasks.items()}, 

707 dataset_types={ 

708 name: SerializedDatasetTypeNode().serialize(target.dataset_types.get_if_resolved(name)) 

709 for name in target.dataset_types 

710 }, 

711 task_subsets={ 

712 label: SerializedTaskSubset.serialize(subset) for label, subset in target.task_subsets.items() 

713 }, 

714 step_labels=list(target.steps), 

715 steps_verified=target.steps.verified, 

716 dimensions=target._universe.dimensionConfig.toDict() if target._universe is not None else None, 

717 data_id=target._raw_data_id, 

718 ) 

719 if target._sorted_keys: 

720 for index, node_key in enumerate(target._sorted_keys): 

721 match node_key.node_type: 

722 case NodeType.TASK: 

723 result.tasks[node_key.name].index = index 

724 case NodeType.DATASET_TYPE: 

725 result.dataset_types[node_key.name].index = index 

726 case NodeType.TASK_INIT: 

727 result.tasks[node_key.name].init.index = index 

728 return result 

729 

730 def deserialize( 

731 self, 

732 import_mode: TaskImportMode, 

733 ) -> PipelineGraph: 

734 """Transform a `SerializedPipelineGraph` into a `PipelineGraph`. 

735 

736 Parameters 

737 ---------- 

738 import_mode : `TaskImportMode` 

739 Import mode. 

740 

741 Returns 

742 ------- 

743 `PipelineGraph` 

744 Deserialized object. 

745 """ 

746 universe: DimensionUniverse | None = None 

747 if self.dimensions is not None: 

748 universe = DimensionUniverse( 

749 config=DimensionConfig( 

750 expect_not_none( 

751 self.dimensions, 

752 "Serialized pipeline graph has not been resolved; " 

753 "load it is a MutablePipelineGraph instead.", 

754 ) 

755 ) 

756 ) 

757 xgraph = networkx.MultiDiGraph() 

758 sort_index_map: dict[int, NodeKey] = {} 

759 # Save the dataset type keys after the first time we make them - these 

760 # may be tiny objects, but it's still to have only one copy of each 

761 # value floating around the graph. 

762 dataset_type_keys: dict[str, NodeKey] = {} 

763 for dataset_type_name, serialized_dataset_type in self.dataset_types.items(): 

764 dataset_type_key = NodeKey(NodeType.DATASET_TYPE, dataset_type_name) 

765 # We intentionally don't attach a DatasetTypeNode instance here 

766 # yet, since we need edges to do that and those are saved with 

767 # the tasks. 

768 xgraph.add_node(dataset_type_key, bipartite=NodeType.DATASET_TYPE.value) 

769 if serialized_dataset_type.index is not None: 

770 sort_index_map[serialized_dataset_type.index] = dataset_type_key 

771 dataset_type_keys[dataset_type_name] = dataset_type_key 

772 for task_label, serialized_task in self.tasks.items(): 

773 task_key = NodeKey(NodeType.TASK, task_label) 

774 task_init_key = NodeKey(NodeType.TASK_INIT, task_label) 

775 task_node = serialized_task.deserialize(task_key, task_init_key, dataset_type_keys, universe) 

776 if serialized_task.index is not None: 

777 sort_index_map[serialized_task.index] = task_key 

778 if serialized_task.init.index is not None: 

779 sort_index_map[serialized_task.init.index] = task_init_key 

780 xgraph.add_node(task_key, instance=task_node, bipartite=NodeType.TASK.bipartite) 

781 xgraph.add_node(task_init_key, instance=task_node.init, bipartite=NodeType.TASK_INIT.bipartite) 

782 xgraph.add_edge(task_init_key, task_key, Edge.INIT_TO_TASK_NAME, instance=None) 

783 for read_edge in task_node.init.iter_all_inputs(): 

784 xgraph.add_edge( 

785 read_edge.dataset_type_key, 

786 read_edge.task_key, 

787 read_edge.connection_name, 

788 instance=read_edge, 

789 ) 

790 for write_edge in task_node.init.iter_all_outputs(): 

791 xgraph.add_edge( 

792 write_edge.task_key, 

793 write_edge.dataset_type_key, 

794 write_edge.connection_name, 

795 instance=write_edge, 

796 ) 

797 for read_edge in task_node.iter_all_inputs(): 

798 xgraph.add_edge( 

799 read_edge.dataset_type_key, 

800 read_edge.task_key, 

801 read_edge.connection_name, 

802 instance=read_edge, 

803 ) 

804 for write_edge in task_node.iter_all_outputs(): 

805 xgraph.add_edge( 

806 write_edge.task_key, 

807 write_edge.dataset_type_key, 

808 write_edge.connection_name, 

809 instance=write_edge, 

810 ) 

811 # Iterate over dataset types again to add instances. 

812 for dataset_type_name, serialized_dataset_type in self.dataset_types.items(): 

813 dataset_type_key = dataset_type_keys[dataset_type_name] 

814 xgraph.nodes[dataset_type_key]["instance"] = serialized_dataset_type.deserialize( 

815 dataset_type_key, xgraph, universe 

816 ) 

817 steps = StepDefinitions( 

818 universe, 

819 dimensions_by_label=dict.fromkeys(self.step_labels, frozenset()), 

820 verified=self.steps_verified, 

821 ) 

822 result = PipelineGraph.__new__(PipelineGraph) 

823 result._init_from_args( 

824 xgraph, 

825 sorted_keys=[sort_index_map[i] for i in range(len(xgraph))] if sort_index_map else None, 

826 task_subsets={ 

827 subset_label: serialized_subset.deserialize_task_subset(subset_label, xgraph, steps) 

828 for subset_label, serialized_subset in self.task_subsets.items() 

829 }, 

830 description=self.description, 

831 step_definitions=steps, 

832 universe=universe, 

833 data_id=self.data_id, 

834 ) 

835 result._import_and_configure(import_mode) 

836 return result