Coverage for python/lsst/pipe/base/graph/quantumNode.py: 68%
57 statements
« prev ^ index » next coverage.py v7.4.1, created at 2024-02-01 11:41 +0000
« prev ^ index » next coverage.py v7.4.1, created at 2024-02-01 11:41 +0000
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
29__all__ = ("QuantumNode", "NodeId", "BuildId")
31import uuid
32from dataclasses import dataclass
33from typing import Any, NewType
35import pydantic
36from lsst.daf.butler import (
37 DatasetRef,
38 DimensionRecordsAccumulator,
39 DimensionUniverse,
40 Quantum,
41 SerializedQuantum,
42)
44from ..pipeline import TaskDef
46BuildId = NewType("BuildId", str)
49def _hashDsRef(ref: DatasetRef) -> int:
50 return hash((ref.datasetType, ref.dataId))
53@dataclass(frozen=True, eq=True)
54class NodeId:
55 """Deprecated, this class is used with QuantumGraph save formats of
56 1 and 2 when unpicking objects and must be retained until those formats
57 are considered unloadable.
59 This represents an unique identifier of a node within an individual
60 construction of a `QuantumGraph`. This identifier will stay constant
61 through a pickle, and any `QuantumGraph` methods that return a new
62 `QuantumGraph`.
64 A `NodeId` will not be the same if a new graph is built containing the same
65 information in a `QuantumNode`, or even built from exactly the same inputs.
67 `NodeId`s do not play any role in deciding the equality or identity (hash)
68 of a `QuantumNode`, and are mainly useful in debugging or working with
69 various subsets of the same graph.
71 This interface is a convenance only, and no guarantees on long term
72 stability are made. New implementations might change the `NodeId`, or
73 provide more or less guarantees.
74 """
76 number: int
77 """The unique position of the node within the graph assigned at graph
78 creation.
79 """
80 buildId: BuildId
81 """Unique identifier created at the time the originating graph was created
82 """
85@dataclass(frozen=True)
86class QuantumNode:
87 """Class representing a node in the quantum graph.
89 The ``quantum`` attribute represents the data that is to be processed at
90 this node.
91 """
93 quantum: Quantum
94 """The unit of data that is to be processed by this graph node"""
95 taskDef: TaskDef
96 """Definition of the task that will process the `Quantum` associated with
97 this node.
98 """
99 nodeId: uuid.UUID
100 """The unique position of the node within the graph assigned at graph
101 creation.
102 """
104 __slots__ = ("quantum", "taskDef", "nodeId", "_precomputedHash")
106 def __post_init__(self) -> None:
107 # use setattr here to preserve the frozenness of the QuantumNode
108 self._precomputedHash: int
109 object.__setattr__(self, "_precomputedHash", hash((self.taskDef.label, self.quantum)))
111 def __eq__(self, other: object) -> bool:
112 if not isinstance(other, QuantumNode):
113 return False
114 if self.quantum != other.quantum:
115 return False
116 return self.taskDef == other.taskDef
118 def __hash__(self) -> int:
119 """For graphs it is useful to have a more robust hash than provided
120 by the default quantum id based hashing
121 """
122 return self._precomputedHash
124 def __repr__(self) -> str:
125 """Make more human readable string representation."""
126 return (
127 f"{self.__class__.__name__}(quantum={self.quantum}, taskDef={self.taskDef}, nodeId={self.nodeId})"
128 )
130 def to_simple(self, accumulator: DimensionRecordsAccumulator | None = None) -> SerializedQuantumNode:
131 return SerializedQuantumNode(
132 quantum=self.quantum.to_simple(accumulator=accumulator),
133 taskLabel=self.taskDef.label,
134 nodeId=self.nodeId,
135 )
137 @classmethod
138 def from_simple(
139 cls,
140 simple: SerializedQuantumNode,
141 taskDefMap: dict[str, TaskDef],
142 universe: DimensionUniverse,
143 ) -> QuantumNode:
144 return QuantumNode(
145 quantum=Quantum.from_simple(simple.quantum, universe),
146 taskDef=taskDefMap[simple.taskLabel],
147 nodeId=simple.nodeId,
148 )
150 def _replace_quantum(self, quantum: Quantum) -> None:
151 """Replace Quantum instance in this node.
153 Parameters
154 ----------
155 quantum : `Quantum`
156 New Quantum instance for this node.
158 Raises
159 ------
160 ValueError
161 Raised if the hash of the new quantum is different from the hash of
162 the existing quantum.
164 Notes
165 -----
166 This class is immutable and hashable, so this method checks that new
167 quantum does not invalidate its current hash. This method is supposed
168 to used only by `QuantumGraph` class as its implementation detail,
169 so it is made "underscore-protected".
170 """
171 if hash(quantum) != hash(self.quantum):
172 raise ValueError(
173 f"Hash of the new quantum {quantum} does not match hash of existing quantum {self.quantum}"
174 )
175 object.__setattr__(self, "quantum", quantum)
178_fields_set = {"quantum", "taskLabel", "nodeId"}
181class SerializedQuantumNode(pydantic.BaseModel):
182 """Model representing a `QuantumNode` in serializable form."""
184 quantum: SerializedQuantum
185 taskLabel: str
186 nodeId: uuid.UUID
188 @classmethod
189 def direct(cls, *, quantum: dict[str, Any], taskLabel: str, nodeId: str) -> SerializedQuantumNode:
190 node = cls.model_construct(
191 __fields_set=_fields_set,
192 quantum=SerializedQuantum.direct(**quantum),
193 taskLabel=taskLabel,
194 nodeId=uuid.UUID(nodeId),
195 )
197 return node