51 """One symbol on the drawing."""
60 """What pid-designer colours this by -- ``fuel``, ``lox``, ``pressurant``.
61 A *role*, not a species: it says which leg a symbol belongs to and nothing
62 about what is in it. Carried so a drawing that set the colour and forgot the
63 fluid can be told so specifically, rather than defaulted in silence."""
67 attached_at: float = 0.5
68 """Where on a *line* a clipped probe sits: the fraction of the line's drawn
69 length from its source end (pid-designer's ``attachedAt``). A probe saved
70 before it existed was drawn at the middle. Meaningless on a symbol."""
71 params: Mapping[str, Param] = field(default_factory=dict)
72 options: Mapping[str, str] = field(default_factory=dict)
73 ports: Mapping[str, Mapping[str, str]] = field(default_factory=dict)
75 """The symbol the drawing used, when it is read as another -- see
76 :func:`_gas_tank_as_bottle`. Empty when the symbol is read as drawn."""
78 """Degrees the symbol is turned on the drawing. A tank's ``t`` ports are on
79 its top only while it is upright."""
83 return self.type
in INLINE_TYPES
87 return self.type
in SOURCE_TYPES
91 return self.type
in INSTRUMENT_TYPES
95 return self.type
in ANNOTATION_TYPES
98@dataclass(frozen=True, slots=True)
151def _params(raw: Any, where: str, symbol: str =
"") -> dict[str, Param]:
152 """Read a ``{name: {value, unit, source, reference}}`` block.
154 A parameter with no ``source`` is refused rather than defaulted. The drawing
155 goes out of its way to always ask, so a missing one means the document was
156 written by something else -- and silently calling it a default would turn a
157 format mismatch into a number nobody checked.
159 if not isinstance(raw, Mapping):
161 out: dict[str, Param] = {}
162 for name, entry
in raw.items():
163 if not isinstance(entry, Mapping)
or entry.get(
"value")
is None:
165 source = str(entry.get(
"source",
""))
166 if source
not in _PROVENANCE:
168 f
"{where}: parameter {name!r} has source {source!r}; expected "
169 f
"one of {', '.join(sorted(_PROVENANCE))}"
172 out[str(name)] =
Param(
173 value=float(entry[
"value"]),
176 unit=drawn_unit(str(name), str(entry.get(
"unit",
"-")), symbol),
177 source=_PROVENANCE[source],
178 reference=str(entry.get(
"reference",
"")),
180 except PressureReferenceError
as exc:
182 except (KeyError, TypeError, ValueError)
as exc:
183 raise DiagramError(f
"{where}: parameter {name!r} is malformed ({exc})")
193def _ports(raw: Any) -> dict[str, dict[str, str]]:
194 if not isinstance(raw, Mapping):
196 out: dict[str, dict[str, str]] = {}
197 for port_id, entry
in raw.items():
198 if isinstance(entry, Mapping):
199 out[str(port_id)] = {str(k): str(v)
for k, v
in entry.items()}
221def read_diagram(payload: Mapping[str, Any], *, name: str =
"diagram") -> Diagram:
222 """Parse a saved drawing. Lenient by design -- see the module docstring."""
223 raw_nodes = payload.get(
"nodes")
224 raw_edges = payload.get(
"edges")
225 if not isinstance(raw_nodes, Iterable)
or not isinstance(raw_edges, Iterable):
227 f
"{name}: expected an object with 'nodes' and 'edges' lists; got keys "
228 f
"{', '.join(sorted(str(k) for k in payload))}"
231 nodes: list[PidNode] = []
232 for raw
in raw_nodes:
233 if not isinstance(raw, Mapping):
235 node_raw = raw.get(
"data")
236 data: Mapping[str, Any] = node_raw
if isinstance(node_raw, Mapping)
else {}
237 pos_raw = raw.get(
"position")
238 position: Mapping[str, Any] = pos_raw
if isinstance(pos_raw, Mapping)
else {}
239 node_id = str(raw.get(
"id",
""))
245 type=str(data.get(
"componentType", raw.get(
"type",
""))),
246 label=str(data.get(
"label", node_id)),
247 x=float(position.get(
"x", 0.0)
or 0.0),
248 y=float(position.get(
"y", 0.0)
or 0.0),
249 fluid=str(data.get(
"fluid",
"")
or ""),
250 role=str(data.get(
"fluidType",
"")
or "").strip().lower(),
251 page=str(data.get(
"page",
"")
or ""),
252 attached_to=str(data.get(
"attachedTo",
"")
or ""),
253 attached_at=
_fraction(data.get(
"attachedAt"), 0.5),
257 str(data.get(
"componentType", raw.get(
"type",
""))),
259 options=
_options(data.get(
"options")),
260 ports=
_ports(data.get(
"ports")),
261 rotation=
_rotation(data.get(
"rotation", raw.get(
"rotation"))),
265 edges: list[PidEdge] = []
266 for raw
in raw_edges:
267 if not isinstance(raw, Mapping):
269 edge_raw = raw.get(
"data")
270 edge_data: Mapping[str, Any] = edge_raw
if isinstance(edge_raw, Mapping)
else {}
271 edge_id = str(raw.get(
"id",
""))
272 source, target = str(raw.get(
"source",
"")), str(raw.get(
"target",
""))
273 if not edge_id
or not source
or not target:
274 raise DiagramError(f
"{name}: an edge is missing id, source or target")
280 source_handle=str(raw.get(
"sourceHandle",
"")
or ""),
281 target_handle=str(raw.get(
"targetHandle",
"")
or ""),
282 line_type=str(edge_data.get(
"lineType",
"pipe")
or "pipe"),
283 page=str(edge_data.get(
"page",
"")
or ""),
284 params=
_params(edge_data.get(
"params"), f
"{name}:{edge_id}"),
285 options=
_options(edge_data.get(
"options")),
286 segments=read_segments(edge_data.get(
"segments"), f
"{name}:{edge_id}"),
292 known = {n.id
for n
in nodes}
293 dangling = [e.id
for e
in edges
if e.source
not in known
or e.target
not in known]
296 f
"{name}: {len(dangling)} line(s) connect to symbols that are not on "
297 f
"the drawing: {', '.join(dangling[:5])}"
300 return Diagram(nodes=tuple(nodes), edges=tuple(edges), name=name)