52from typing
import Any, Dict, List, Mapping, Optional, Sequence, Tuple
76def critical_height(Q: float, accel: float, rho_liquid: float, rho_gas: float = 0.0,
77 constant: float = LS_CONSTANT) -> float:
78 """Lubin & Springer's dip height [m] for a volumetric outflow ``Q`` [m^3/s] at specific force
80 g_eff = accel * (1.0 - rho_gas / rho_liquid)
81 if Q <= 0.0
or g_eff <= 0.0:
82 return 0.0
if Q <= 0.0
else math.inf
83 return constant * (Q * Q / g_eff) ** 0.2
86def volume_below(h: float, radius: float, head_depth: float) -> float:
87 """Liquid volume [m^3] up to height ``h`` over the outlet in a vertical cylinder of ``radius``
88 with a semi-ellipsoidal bottom head of depth ``head_depth`` (0 = flat, radius = hemisphere).
89 In the head, the cross-section at height y is ``pi R^2 (2 b y - y^2)/b^2``."""
92 R, b = radius, head_depth
94 return math.pi * R * R * h
96 return math.pi * R * R / (b * b) * (b * h * h - h ** 3 / 3.0)
97 return (2.0 / 3.0) * math.pi * R * R * b + math.pi * R * R * (h - b)
101 """Inverse of :func:`volume_below` (bisection: monotone)."""
104 lo, hi = 0.0,
max(head_depth, 0.0) + V / (math.pi * radius * radius) + 1e-9
108 mid = 0.5 * (lo + hi)
115 return 0.5 * (lo + hi)
118def onset(t: Sequence[float], liquid_kg: Sequence[float], mdot: Sequence[float], accel: Sequence[float], *,
119 rho_liquid: Sequence[float], rho_gas: Sequence[float], radius: float, head_depth: float,
120 constant: float = LS_CONSTANT) -> Dict[str, Any]:
121 """First instant the liquid height falls to the dip height, interpolated between steps.
122 Inputs are per step over the firing steps (mdot > 0)."""
124 rows: List[Tuple[float, float, float, float]] = []
125 for k
in range(len(t)):
129 rows.append((t[k], h, hc, liquid_kg[k]))
132 if margin_prev
is None:
133 return {
"t": t[k],
"residual_kg": liquid_kg[k],
"h_c": hc,
"h": h,
"index": k,
"rows": rows}
134 f = margin_prev / (margin_prev - margin)
135 t_on = t[k - 1] + f * (t[k] - t[k - 1])
136 m_on = liquid_kg[k - 1] + f * (liquid_kg[k] - liquid_kg[k - 1])
137 hc_on = rows[k - 1][2] + f * (hc - rows[k - 1][2])
138 return {
"t": t_on,
"residual_kg": m_on,
"h_c": hc_on,
"h": hc_on,
"index": k,
"rows": rows}
140 return {
"t":
None,
"residual_kg":
None,
"h_c": rows[-1][2]
if rows
else None,
141 "h": rows[-1][1]
if rows
else None,
"index":
None,
"rows": rows}
147@dataclass(frozen=True)
156def _schedule_accel(result: Mapping[str, Any], accel: Optional[Mapping[str, Any]]) -> Tuple[Optional[Mapping[str, Any]], str]:
157 if accel
is not None:
158 return accel,
"given acceleration schedule"
159 flight = result.get(
"flight")
or {}
160 sch = flight.get(
"schedule")
if isinstance(flight, Mapping)
else None
161 if isinstance(sch, Mapping)
and sch.get(
"t")
and sch.get(
"accel_m_s2"):
162 return sch,
"the flight's acceleration (result.flight.schedule)"
163 return None,
"standard gravity (on the pad)"
167 accel: Optional[Mapping[str, Any]] =
None) -> List[Dict[str, Any]]:
168 """``diagnostics.outflow`` rows. Never raises."""
169 rows: List[Dict[str, Any]] = []
170 for (side, key), d_mm
in zip(SIDES, settings.outlet_d_mm):
172 rows.append(
_row(prep, result, side, key, d_mm, settings, accel))
173 except Exception
as exc:
174 rows.append({
"tank": (getattr(prep,
"roles", {})
or {}).get(side),
"side": key,
175 **unavailable(f
"outflow: {exc}")})
179def _row(prep: Any, result: Mapping[str, Any], side: str, key: str, d_mm: Optional[float],
180 settings: OutflowSettings, accel: Optional[Mapping[str, Any]]) -> Dict[str, Any]:
185 if settings.head
not in HEADS:
186 raise ValueError(f
"unknown head shape {settings.head!r}; one of {', '.join(HEADS)}")
187 series = result[
"series"]
189 tank = prep.roles[side]
190 node = prep.model.diagram.node(tank)
191 dparam = node.params.get(
"diameter")
193 raise ValueError(f
"{tank} has no drawn diameter")
194 D_tank = float(dparam.si)
197 net = prep.model.built.network
198 path = feed_path(net, prep.inlet_nodes[side], tank)
199 first = net.branches[path[0]].component
if path
else None
200 bore_line = float((getattr(first,
"p", {})
or {}).get(
"bore", 0.0)
or 0.0)
if first
is not None else 0.0
203 d_prov = f
"drawing: the bore of the line leaving the tank ({path[0] if path else '?'})"
205 d_out = float(d_mm) * 1e-3
207 species = (prep.derived.get(
"species")
or {}).get(side)
208 gas = prep.derived.get(
"pressurant_gas")
209 gas_prov = f
"feedtwin Fluid('{gas}') at the tank pressure and ullage temperature"
212 gas_prov =
"assumed nitrogen: the run names no pressurant gas (it enters only the buoyancy factor)"
213 fl, fg = Fluid(species), Fluid(gas)
214 idx = [i
for i, f
in enumerate(series.get(
"firing")
or [])
if f]
216 raise ValueError(
"the burn never fired")
217 t = [float(series[
"t"][i])
for i
in idx]
218 liq = [float(sub[
"liquid_kg"][i])
for i
in idx]
219 md = [float(sub[
"mdot"][i])
for i
in idx]
220 p = [float(sub[
"tank_psia"][i]) * PSI
for i
in idx]
221 TL = [float(sub[
"liquid_K"][i])
for i
in idx]
222 TU = [float(sub[
"ullage_K"][i])
for i
in idx]
223 rho_l = [fl.get(
"rho", p=pi, T=ti)
for pi, ti
in zip(p, TL)]
224 rho_g = [fg.get(
"rho", p=pi, T=ti)
for pi, ti
in zip(p, TU)]
227 a = [float(np.interp(ti, sch[
"t"], sch[
"accel_m_s2"]))
for ti
in t]
230 by_head: Dict[str, Dict[str, Any]] = {}
231 for name, frac
in HEADS.items():
232 on =
onset(t, liq, md, a, rho_liquid=rho_l, rho_gas=rho_g, radius=R, head_depth=frac * R,
233 constant=settings.constant)
235 main = by_head[settings.head]
236 load = finite((result.get(
"summary")
or {}).get(key, {}).get(
"loaded_kg"))
or liq[0]
238 deliv = result.get(
"delivered")
or {}
240 if main[
"t"]
is not None and deliv.get(
"t")
and deliv.get(
"thrust_N"):
241 dt_ = [float(x)
for x
in deliv[
"t"]]
242 F = [float(x)
for x
in deliv[
"thrust_N"]]
244 for k
in range(1, len(dt_)):
245 lo, hi =
max(dt_[k - 1], main[
"t"]), dt_[k]
247 f0 = float(np.interp(lo, dt_, F))
248 at_risk += 0.5 * (f0 + F[k]) * (hi - lo)
250 i_ref = k_on
if k_on
is not None else len(t) - 1
251 Q_ref = md[i_ref] / rho_l[i_ref]
252 v_out = Q_ref / (math.pi * d_out ** 2 / 4.0)
if d_out > 0
else None
253 fr = v_out / math.sqrt(a[i_ref] * d_out)
if (v_out
and d_out > 0)
else None
255 warnings: List[str] = []
256 if hc
is not None and d_out > 0
and hc < 2.0 * d_out:
257 warnings.append(f
"h_c {1e3 * hc:.1f} mm is under two outlet diameters: the point-sink picture behind the "
258 "correlation is stretched")
260 "tank_diameter": inp(D_tank,
"m", param_provenance(dparam,
"flagged: estimated, not measured")),
261 "outlet_d": inp(d_out,
"m", d_prov),
262 "head": inp(settings.head,
"-",
"assumed flat: the head shape is not drawn; flat puts the most liquid under "
263 "h_c (upper bound on the residual)"),
264 "constant": inp(settings.constant,
"-", LS_SOURCE),
265 "rho_liquid": inp(rho_l[i_ref],
"kg/m^3", f
"feedtwin Fluid('{species}') at the tank state"),
266 "rho_gas": inp(rho_g[i_ref],
"kg/m^3", gas_prov),
267 "accel": inp(a[i_ref],
"m/s^2", accel_basis),
270 MODEL_NAME, LS_SOURCE,
272 "no swirl: a dip, not a vortex (a vortex forms higher)",
273 "bottom-centre outlet; h measured from the outlet plane",
274 "the tank is a vertical cylinder of the drawn diameter with the stated bottom head",
275 "the residual at onset is an upper bound on what the outlet cannot deliver gas-free",
276 "the specific force is standard gravity on the pad, or the flight's acceleration when flown",
277 "g is reduced by the buoyancy factor (1 - rho_gas/rho_liquid): this module's, not the restatement's",
278 "0.574 (docs/layer-x.md) could not be verified and is not used",
282 "tank": tank,
"side": key,
"available":
True,
283 "ingestion_onset_s": main[
"t"],
284 "residual_kg": main[
"residual_kg"],
285 "residual_frac": (main[
"residual_kg"] / load)
if (main[
"residual_kg"]
is not None and load)
else None,
286 "residual_band_kg": {name: on[
"residual_kg"]
for name, on
in by_head.items()},
287 "onset_band_s": {name: on[
"t"]
for name, on
in by_head.items()},
288 "h_c_mm": 1e3 * hc
if hc
is not None else None,
289 "level_end_mm": 1e3 *
liquid_height(liq[-1] / rho_l[-1], R, HEADS[settings.head] * R),
290 "outlet_d_mm": 1e3 * d_out,
292 "impulse_at_risk_Ns": at_risk,
293 "head": settings.head,
294 "warnings": warnings,
295 "unmeasured": [
"outlet bore",
"tank bottom head shape",
"tank diameter (estimated)",
"swirl at the outlet",
296 "a weighed residual from a cold flow to depletion would replace all of these"],
List[Dict[str, Any]] outflow_from_run(Any prep, Mapping[str, Any] result, OutflowSettings settings=OutflowSettings(), Optional[Mapping[str, Any]] accel=None)
Dict[str, Any] onset(Sequence[float] t, Sequence[float] liquid_kg, Sequence[float] mdot, Sequence[float] accel, *, Sequence[float] rho_liquid, Sequence[float] rho_gas, float radius, float head_depth, float constant=LS_CONSTANT)
Dict[str, Any] _row(Any prep, Mapping[str, Any] result, str side, str key, Optional[float] d_mm, OutflowSettings settings, Optional[Mapping[str, Any]] accel)