146 def _update(self, t: float, seen: list[_Seen]) ->
None:
147 now = {(s.check, s.subject): s
for s
in seen}
148 for key
in list(self.
_since):
151 for key, flag
in list(self.
_open.items()):
152 if key
not in now
and t - flag.last >= CLOSE_AFTER_S:
155 for key, s
in now.items():
156 since = self.
_since.setdefault(key, t)
157 current = self.
_open.get(key)
158 if current
is not None:
159 current.last, current.detail = t, s.detail
161 if t - since + 1e-9 < s.hold:
163 flag =
Flag(s.check, s.subject, since, t, s.detail)
164 self.
_open[key] = flag
165 self.
flags.append(flag)
169 def _checks(self, session:
"Session", record: SolverRecord) -> Iterator[_Seen]:
170 net = session.model.built.network
171 labels = {n.id: n.label
or n.id
for n
in session.model.diagram.nodes}
173 def name(item: str) -> str:
174 return labels.get(item)
or labels.get(item.split(
".")[0])
or item
176 flows = session._last_flows
177 isolated = session._last_isolated
180 for sim
in session.tanks.values():
181 before = self.
_floored.get(sim.id, sim.floored_kg)
182 self.
_floored[sim.id] = sim.floored_kg
183 made = sim.floored_kg - before
184 if sim.sealed
and made > SEALED_GAIN_KG:
188 f
"the ullage floor put in {made * 1e3:.3g} g of gas this tick "
189 "with no open path to the air",
194 for branch_id, flow
in flows.items():
195 if branch_id
in isolated:
197 if abs(flow - INITIAL_FLOW) < 1e-15:
201 "converged with this branch at the solve's 1 g/s "
202 "starting flow: Newton took no step on it",
206 vessels = {s.ullage_node
for s
in session.tanks.values()}
207 vessels |= {s.outlet_node
for s
in session.tanks.values()}
208 vessels |= {b.node
for b
in session.bottles.values()}
209 dry = session._dry_branches()
210 shut_by_valve = frozenset(isolated - dry)
211 for sim
in session.tanks.values():
212 for node, side
in ((sim.ullage_node,
"gas"), (sim.outlet_node,
"liquid")):
213 pressure = net.nodes[node].pressure
214 if pressure
is None or abs(pressure - ATMOSPHERE) <= HELD_DP:
216 if side ==
"liquid" and sim.state.liquid_mass <= 1e-3:
221 gas_in, gas_out = session._split_at(node, flows)
222 if gas_in + gas_out > STILL_FLOW:
225 cut = [b
for b
in drawn
if b
in isolated]
227 f
"{say(pressure, 1)} ({(pressure - ATMOSPHERE) / PSI:+.1f} psi "
228 f
"off atmosphere) at the {side} side, "
229 f
"open to the air through {', '.join(name(b) for b in drawn[:6])}"
231 if solved
is None and cut:
232 detail += f
"; the solve shut {', '.join(name(b) for b in cut)}"
234 detail +=
"; nothing flows"
235 yield _Seen(
"held_open", f
"{sim.label} ({side})", detail, HELD_FOR_S)
239 for sim
in session.tanks.values():
240 before = self.
_refused.get(sim.id, sim.refused_kg)
241 self.
_refused[sim.id] = sim.refused_kg
242 dt = record.t - self.
_t if self.
_t is not None else 0.0
243 rate = (sim.refused_kg - before) / dt
if dt > 0.0
else 0.0
244 if rate > LIQUID_IN_MIN:
246 "liquid_from_nowhere",
248 f
"the network sent {rate * 1e3:.3g} g/s into its outlet that no "
249 "tank gave; refused, and booked as a clamp",
259 f
"{self._unconverged} ticks in a row; residual {record.residual:.2e}",
261 tol = float(getattr(session.setup,
"network_tolerance", 0.0)
or 0.0)
262 if tol > 0.0
and record.residual > EDGE_SHARE * tol:
271 f
"residual above {EDGE_SHARE:.0%} of the tolerance ({tol:g}) since "
272 f
"{self._edge_since:.1f} s; now {record.residual:.2e}",
276 unexplained = record.mass_error_kg - record.guard_kg
277 scale =
max(record.throughput_kg, record.inventory_kg)
278 if abs(unexplained) > UNEXPLAINED_KG + UNEXPLAINED_SHARE * scale:
282 f
"{unexplained * 1e3:+.3g} g that no vessel booked and no boundary "