STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
diagnostics.py
Go to the documentation of this file.
1"""What the solver did each tick, and whether mass was kept: the solver tab.
2
3A run that converges, plots smoothly and violates no assertion is not evidence
4(docs/PHYSICS-BENCHMARK.md opens with that). This is the other half: the
5numbers a CFD code shows beside every answer, so a person can see when one is
6not to be trusted.
7
8Per tick, a :class:`SolverRecord`:
9
10* **Newton.** How many coupling steps the tick took, the network iterations
11 they cost, the worst scaled residual and whether every solve converged.
12* **Continuity.** The worst net mass imbalance at any free node of the network
13 [kg/s] -- the solve's own conservation, which a converged solve can still
14 violate if its tolerance is loose.
15* **Chamber closure.** How far apart the network's flow and the engine's
16 chamber pressure were left [psi].
17* **Global mass balance.** Everything held in the vessels, against everything
18 that crossed the stand's boundary: the engine, vents, a drawn supply, and the
19 built-in loads and charges. Cumulative, so a leak of micrograms a step shows
20 as a line that drifts. Zero is the only right answer.
21* **Integrator guards.** What the vessels' own floors and clamps changed beyond
22 what their rates said -- mass [kg] and ullage energy [J], cumulative. Each
23 guard is physics (a vent cannot pull a tank below atmosphere), but each one
24 changes the inventory, and seeing how much is the point.
25
26Read-only: nothing here changes a step.
27"""
28
29from __future__ import annotations
30
31from dataclasses import dataclass
32from typing import Iterable, Mapping
33
34from feedtwin.solve.network import Network
35
36
37@dataclass(frozen=True, slots=True)
39 """One tick of the solver, as the solver tab plots it."""
40
41 t: float
42 couplings: int
43 """Coupling steps (network solves) the tick took."""
44 iterations: int
45 """Newton iterations, summed over the tick's solves."""
46 iterations_max: int
47 """The most any one solve took."""
48 residual: float
49 """Worst scaled residual norm of the tick's solves [-]."""
50 continuity: float
51 """Worst net mass imbalance at a free node [kg/s]."""
52 converged: bool
53 """Every solve in the tick converged."""
54 chamber_residual_psi: float
55 """``|g(p) - p|`` the chamber closure left [psi]; 0 without an engine."""
56 inventory_kg: float
57 """Fluid held in every vessel: liquid, ullage gas, vapour, bottles [kg]."""
58 crossed_in_kg: float
59 """Cumulative mass in across the boundary [kg]."""
60 crossed_out_kg: float
61 """Cumulative mass out across the boundary [kg]."""
62 mass_error_kg: float
63 """Cumulative: inventory gained less what crossed in net [kg]. Zero when
64 mass is kept."""
65 guard_kg: float
66 """Cumulative mass the vessels' guards added (+) or removed (-) [kg]."""
67 guard_J: float
68 """Cumulative ullage energy the vessels' guards added or removed [J]."""
69
70 @property
71 def throughput_kg(self) -> float:
72 return self.crossed_in_kg + self.crossed_out_kg
73
74 def to_dict(self) -> dict[str, float | int | bool]:
75 return {
76 "t": self.t,
77 "couplings": self.couplings,
78 "iterations": self.iterations,
79 "iterations_max": self.iterations_max,
80 "residual": self.residual,
81 "continuity": self.continuity,
82 "converged": self.converged,
83 "chamber_residual_psi": self.chamber_residual_psi,
84 "inventory_kg": self.inventory_kg,
85 "crossed_in_kg": self.crossed_in_kg,
86 "crossed_out_kg": self.crossed_out_kg,
87 "mass_error_kg": self.mass_error_kg,
88 "guard_kg": self.guard_kg,
89 "guard_J": self.guard_J,
90 }
91
92
93def boundary_nodes(net: Network, vessel_nodes: Iterable[str]) -> frozenset[str]:
94 """Fixed-pressure nodes that are not a vessel's: atmosphere at a vent, the
95 chamber, a drawn source the session does not integrate."""
96 vessels = set(vessel_nodes)
97 return frozenset(
98 node_id
99 for node_id, node in net.nodes.items()
100 if node.pressure is not None and node_id not in vessels
101 )
102
103
105 net: Network, flows: Mapping[str, float], boundary: frozenset[str]
106) -> tuple[float, float]:
107 """``(in, out)`` [kg/s] across the boundary, from one solve's flows.
108
109 A branch between two boundary nodes carries nothing the stand holds, and a
110 branch with neither end on the boundary is internal; only a branch with
111 exactly one end there crosses it.
112 """
113 into = out = 0.0
114 for branch_id, branch in net.branches.items():
115 flow = flows.get(branch_id, 0.0)
116 if flow == 0.0:
117 continue
118 up, down = branch.upstream in boundary, branch.downstream in boundary
119 if up == down:
120 continue
121 # Positive flow runs upstream -> downstream.
122 signed = flow if down else -flow # positive: leaving the stand
123 if signed > 0.0:
124 out += signed
125 else:
126 into -= signed
127 return into, out
dict[str, float|int|bool] to_dict(self)
frozenset[str] boundary_nodes(Network net, Iterable[str] vessel_nodes)
tuple[float, float] crossing(Network net, Mapping[str, float] flows, frozenset[str] boundary)