112 """A listing-level look at a drawing without building anything."""
113 raw_nodes = payload.get(
"nodes")
114 raw_edges = payload.get(
"edges")
115 nodes: list[Any] = list(raw_nodes)
if isinstance(raw_nodes, list)
else []
116 edges: list[Any] = list(raw_edges)
if isinstance(raw_edges, list)
else []
118 kinds: dict[str, int] = {}
121 if not isinstance(node, dict):
123 data = node.get(
"data")
124 if not isinstance(data, dict):
130 kind = str(data.get(
"componentType")
or node.get(
"type")
or "?")
131 kinds[kind] = kinds.get(kind, 0) + 1
132 options = data.get(
"options")
133 if isinstance(options, dict)
and options.get(
"engineConfig"):
134 engine_ref = str(options[
"engineConfig"])
136 "symbols": len(nodes),
139 "engineConfig": engine_ref,
145 "injector": design.injector_type,
146 "oxidiser": design.oxidiser.propellant,
147 "fuel": design.fuel.propellant,
148 "throat_mm2": round(design.throat_area * 1e6, 1),
149 "thrust_N": design.design_thrust,
150 "mixture_ratio": design.design_mixture_ratio,
151 "warnings": list(design.warnings),
173 engine: EngineDesign |
None =
None,
174 engine_reference: str =
"",
175 chamber: Chamber |
None =
None,
177 fluid_swap: Mapping[str, tuple[str, float]] |
None =
None,
178 gas_swap: Mapping[str, str] |
None =
None,
179 multiphase: bool =
False,
180 meta: Mapping[str, object] |
None =
None,
181 vehicle_only: bool =
False,
183 """Build and audit a model from a drawing already read and an engine already
184 imported. The half of the feed-twin app's ``assemble`` that needs no
185 artifact store, so a caller with its own (EngineDesign's Layer X) gets the
186 same model, the same assumptions and the same report.
189 diagram_id: What the report calls the drawing -- an artifact id, a
190 document id, a path. Recorded, never resolved.
191 engine_reference: What the report calls the engine. Defaults to the
192 engine's own name when an engine is given.
193 chamber: A chamber to attach. When omitted and an engine is given, one
194 is built from ``cea_cache`` (see :func:`chamber_for`).
195 gas_swap: The pressurant replaced, ``{"nitrogen": "helium"}``
196 (:func:`swap_gases`): every symbol declaring the one gas declares
197 the other. Tanks keep their contents.
198 vehicle_only: Cut the ground support away before building
199 (:func:`feedtwin.pid.roles.vehicle_only`, ``Setup.ignore_gse``):
200 the cart is not simulated and the session's built-in fills stand in
201 for it. ``meta`` then carries ``vehicle_only``, ``ground_cut``
202 (the labels of the vessels cut) and ``capped`` (the ids of the
203 vehicle's disconnects whose mate was cut).
205 loose_vents = unpaired_vents(diagram)
208 n.id: str(n.options.get(
"pairedWith",
"")
or "").strip()
209 for n
in diagram.nodes
211 diagram, cut = cut_to_vehicle(diagram)
214 for n
in diagram.nodes
215 if mates.get(n.id)
not in (
"",
"none",
None)
216 and not str(n.options.get(
"pairedWith",
"")
or "").strip()
220 "vehicle_only":
True,
221 "ground_cut": list(cut),
229 reference = engine_reference
or (engine.name
if engine
is not None else "")
230 warnings: list[str] = []
231 if engine
is not None:
232 warnings.extend(engine.warnings)
237 built = build_network(diagram, engine=engine, multiphase=multiphase)
238 except (DiagramError, SpecError)
as exc:
243 warnings.extend(built.warnings)
244 warnings.extend(loose_vents)
248 component=branch.component.instance.id,
250 value=branch.component.instance.params[name].value,
251 unit=branch.component.instance.params[name].unit,
252 source=branch.component.instance.params[name].source.value,
253 reference=branch.component.instance.params[name].reference,
255 for branch
in built.network.branches.values()
256 for name
in assumed_params(branch.component.instance.params)
266 if engine
is not None:
269 "mixture_ratio": (
"design_mixture_ratio",
"-"),
270 "design_chamber_pressure": (
"design_chamber_pressure",
"Pa"),
272 engine_notes: list[Assumption] = []
273 for key, note
in sorted(engine.provenance.items()):
274 field = engine_fields.get(key)
277 attribute, unit = field
282 value=float(getattr(engine, attribute, 0.0)
or 0.0),
286 source=
"resolved" if "intent wins" in note
else "measured",
290 assumptions = assumptions + tuple(engine_notes)
295 symbols=len(diagram.nodes),
296 lines=len(diagram.edges),
297 nodes=len(built.network.nodes),
298 branches=len(built.network.branches),
299 instruments=len(built.instruments),
300 actuators=len(built.actuators),
301 assumptions=assumptions,
302 warnings=tuple(warnings),
311 meta=dict(meta
or {}),
316 """A chamber for this engine, from a CEA table when one is reachable.
318 A constant ``c*`` is the fallback and it is a real loss: with it, mixture
319 ratio still moves during a run but nothing downstream of it responds, which
320 reads as "O/F drift does not matter here" when it means "this model cannot
321 see it". So the fallback is named in the warnings rather than taken quietly.
323 from pathlib
import Path
329 "efficiency": engine.cstar_efficiency,
330 "nozzle_efficiency": engine.nozzle_efficiency,
332 if cea_cache
and Path(cea_cache).is_file():
335 CEATable(cea_cache, expansion_ratio=engine.expansion_ratio),
341def swap_gases(diagram: Diagram, swaps: Mapping[str, str]) -> Diagram:
342 """The pressurant replaced: every symbol that declares a gas in ``swaps``
343 -- the bottles, and any valve or line labelled with it -- declares the
344 other instead, keeping its own temperature. Tanks and dewars hold liquids
345 and are :func:`swap_fluids`'s. The network spreads gases from what is
346 declared (a K-bottle first), so this is the whole of the change: helium
347 in the COPV, its press lines and the ullages it fills."""
348 from dataclasses
import replace
350 def swapped(node: PidNode) -> PidNode:
351 if node.type
in (
"TANK",
"DEWAR")
or node.fluid
not in swaps:
353 return replace(node, fluid=swaps[node.fluid])
355 return replace(diagram, nodes=tuple(swapped(n)
for n
in diagram.nodes))
358def swap_fluids(diagram: Diagram, swaps: Mapping[str, tuple[str, float]]) -> Diagram:
359 """Tank contents replaced, for a cold flow. Temperature travels with the
360 species -- water at the LOX tank's 90 K is ice."""
361 from dataclasses
import replace
365 def swapped(node: PidNode) -> PidNode:
366 if node.type !=
"TANK" or node.fluid
not in swaps:
368 species, temperature = swaps[node.fluid]
369 params = dict(node.params)
370 params[
"temperature"] = Param(
371 temperature,
"K", Provenance.MEASURED, f
"{species} for this test"
373 return replace(node, fluid=species, params=params)
375 return replace(diagram, nodes=tuple(swapped(n)
for n
in diagram.nodes))
Model assemble_model(Diagram diagram, *, str diagram_id, EngineDesign|None engine=None, str engine_reference="", Chamber|None chamber=None, str cea_cache="", Mapping[str, tuple[str, float]]|None fluid_swap=None, Mapping[str, str]|None gas_swap=None, bool multiphase=False, Mapping[str, object]|None meta=None, bool vehicle_only=False)