48 PSI, arr, firing_mask, inp, model_block, node_p, node_T, out, scalar, series_t, unavailable,
51 DEFAULT_XT, _built, _provenance, coolprop_name, gas_state, iec_gas_flow, regulator_id,
116def bottle_floor(fluid: str, mdot: float, p_out_pa: float, cv: float, xT: float, min_differential_pa: float,
117 p_b_end_pa: float, T_b_end_K: float, p_in_end_pa: float, T_in_end_K: float,
118 line_drop_end_pa: float) -> Dict[str, float]:
119 """The bottle state at which the regulator, wide open, just passes ``mdot`` to ``p_out``.
121 Solves together (fixed point on the regulator inlet pressure):
123 * regulator inlet ``p_in`` = :func:`regulator_inlet_floor` at the inlet gas temperature ``T_in``;
124 * bottle ``p_b = p_in + dp_line``, ``dp_line = line_drop_end * rho_in_end / rho_in`` (a
125 loss-coefficient drop at the same mass flow scales as 1/rho);
126 * bottle ``T_b`` on the burnout isentrope (:func:`isentropic_temperature`);
127 * ``T_in = T(p_in, h(p_b, T_b))``: the bottle gas throttled through the adiabatic line.
129 Returns {p_b, T_b, p_in, T_in, line_drop, iterations, converged} in Pa and K.
131 import CoolProp.CoolProp
as CP
133 name = coolprop_name(fluid)
134 rho_in_end =
density(fluid, p_in_end_pa, T_in_end_K)
135 drop_end =
max(line_drop_end_pa, 0.0)
138 p_b = T_b = drop = math.nan
141 for it
in range(1, FLOOR_ITERS + 1):
143 if not math.isfinite(p_new):
145 rho_in =
density(fluid, p_new, T_in)
146 drop = drop_end * rho_in_end / rho_in
if (drop_end > 0
and rho_in > 0)
else drop_end
150 h_b = CP.PropsSI(
"HMASS",
"P", p_b,
"T", T_b, name)
151 T_in = float(CP.PropsSI(
"T",
"P", p_new,
"HMASS", h_b, name))
154 done = math.isfinite(p_in)
and abs(p_new - p_in) < FLOOR_TOL_PA
159 return {
"p_b": p_b,
"T_b": T_b,
"p_in": p_in,
"T_in": T_in,
"line_drop": drop,
160 "iterations": float(it),
"converged": float(converged)}
164 xT: float = DEFAULT_XT, min_differential_pa: float = 0.0) -> float:
165 """Lowest regulator inlet pressure [Pa] at which the wide-open seat (IEC 60534-2-1) still
166 passes ``mdot`` to ``p_out``, and at least ``p_out + min_differential``."""
167 from scipy.optimize
import brentq
169 base = p_out_pa +
max(0.0, min_differential_pa)
170 if not (math.isfinite(mdot)
and mdot > 0
and math.isfinite(cv)
and cv > 0):
173 def excess(p_in: float) -> float:
174 rho, gamma = gas_state(fluid, p_in, T_in_K)
175 return iec_gas_flow(p_in, p_out_pa, rho, gamma, cv, xT)[
"mdot"] - mdot
177 lo = p_out_pa * (1.0 + 1e-9)
178 if excess(lo) >= 0.0:
181 while excess(hi) < 0.0:
185 return max(base, float(brentq(excess, lo, hi, xtol=1.0, rtol=1e-12)))
196 gas: Optional[str] =
None, volume_m3: Optional[float] =
None,
197 regulator: Optional[Mapping[str, Any]] =
None,
198 xT: Optional[float] =
None) -> Dict[str, Any]:
199 """``diagnostics.pressurant``. Never raises.
201 ``gas`` and ``volume_m3`` default to the drawing's (``derived.pressurant_gas``,
202 ``derived.copv_volume_L``, from ``prep`` or the result's provenance). ``regulator`` is
203 ``diagnostics.regulator`` when already built (its Cv and x_T are reused). Needs
204 ``result["network"]`` only for the regulator's inlet state (JT, floor); without it, those
205 keys are null and the mass budget stands.
208 series = result.get(
"series")
or {}
212 fluid = gas
or derived.get(
"pressurant_gas")
214 return unavailable(
"the pressurant species is unknown")
215 vol = volume_m3
if volume_m3
is not None else (
216 float(derived[
"copv_volume_L"]) / 1e3
if derived.get(
"copv_volume_L")
else None)
217 mass = arr(series.get(
"copv_mass_kg"), n)
218 p_b = arr(series.get(
"copv_psia"), n) * PSI
219 if not np.isfinite(mass).any():
220 return unavailable(
"series.copv_mass_kg is missing")
221 firing = firing_mask(result, n)
222 end = int(np.flatnonzero(firing)[-1])
if firing.any()
else n - 1
224 loaded = float(mass[start])
225 residual = float(mass[end])
226 used = loaded - residual
228 T_b = np.full(n, math.nan)
231 if math.isfinite(mass[k] + p_b[k])
and mass[k] > 0:
237 if math.isfinite(T_b[start] + p_b[start] + p_b[end])
else math.nan)
239 net = result.get(
"network")
240 jt = np.full(n, math.nan)
241 twin_dT = np.full(n, math.nan)
246 fl: Dict[str, float] = {}
247 reg_basis =
"no network recorded: no regulator inlet state"
251 min_diff_prov =
"assumed 0: no min_inlet_differential on the drawing"
252 if isinstance(net, Mapping)
and net.get(
"branches"):
253 rid = (regulator
or {}).get(
"id")
or regulator_id(net)
255 br = net[
"branches"][rid]
256 p_in = node_p(net, br[
"from"], n) * PSI
257 p_out = node_p(net, br[
"to"], n) * PSI
258 T_in = node_T(net, br[
"from"], n)
259 T_out = node_T(net, br[
"to"], n)
261 if math.isfinite(p_in[k] + T_in[k] + p_out[k]):
263 twin_dT = T_out - T_in
264 reg_basis = f
"regulator {rid}: inlet node {br['from']} state, outlet node {br['to']} pressure"
265 cv = (regulator
or {}).get(
"cv")
or br.get(
"cv")
266 x_t = xT
if xT
is not None else DEFAULT_XT
268 if built
is not None:
270 comp = built.network.branches[rid].component
271 cv = cv
or comp.p.get(
"Cv")
272 if "min_inlet_differential" in comp.p:
273 min_diff = float(comp.p[
"min_inlet_differential"])
274 min_diff_prov = _provenance(comp,
"min_inlet_differential")
277 if regulator
and regulator.get(
"model"):
278 x_t = xT
if xT
is not None else float(
279 ((regulator[
"model"].get(
"inputs")
or {}).get(
"xT")
or {}).get(
"value")
or DEFAULT_XT)
280 mdot_end = float(arr(br.get(
"mdot"), n)[end])
282 path = (net.get(
"paths")
or {}).get(
"ox")
or (net.get(
"paths")
or {}).get(
"fuel")
or []
283 bottle_node = net[
"branches"][path[0]][
"from"]
if path
else br[
"from"]
284 line_drop = float((node_p(net, bottle_node, n) * PSI - p_in)[end])
285 if cv
and math.isfinite(T_in[end])
and math.isfinite(p_out[end]):
288 if vol
and math.isfinite(T_b[end])
and math.isfinite(held_in):
289 unusable_held =
density(fluid, held_in +
max(line_drop, 0.0), T_b[end]) * vol
290 if math.isfinite(T_b[end] + p_b[end]):
291 fl =
bottle_floor(fluid, mdot_end, p_out[end], float(cv), x_t, min_diff, p_b[end], T_b[end],
292 p_in[end], T_in[end], line_drop)
293 if math.isfinite(fl[
"p_b"] + fl[
"T_b"]):
294 floor_reg_in = fl[
"p_in"]
297 unusable =
density(fluid, floor, fl[
"T_b"]) * vol
298 required = used + unusable
if unusable
is not None else None
299 margin = loaded - required
if required
is not None else None
300 live = firing & np.isfinite(jt)
302 "pressurant budget + isenthalpic regulator throttle",
303 COOLPROP_SOURCE + f
", version {_cp_version()}; steady-flow energy equation (adiabatic, no work, "
304 "kinetic energy neglected: h_out = h_in); first law for a rigid vessel discharging with no heat "
305 "(d(mu) = h dm -> ds = 0 for the gas left inside); IEC 60534-2-1:2011 for the seat capacity floor",
307 "mass from the bottle's vessel state; loaded = first recorded sample (bottle assumed full "
308 "after the tanks were pressed, AUDIT D13), residual = last firing step",
309 "bottle gas temperature = CoolProp T(rho = m/V, p) of the vessel state",
310 "required = used + the mass left when the bottle reaches the floor, the bottle taken on from "
311 "its burnout state along its isentrope (no wall heat after burnout: the conservative bound; "
312 "unusable_held_T_kg holds the burnout temperature and line drop instead)",
313 "floor at the regulator inlet = max(regulator outlet at burnout + min_inlet_differential, inlet "
314 "at which the wide-open seat's IEC capacity equals the burnout flow), at the inlet temperature "
315 "of the bottle gas throttled isenthalpically through the bottle-to-regulator line",
316 "bottle-to-regulator line drop carried from burnout to the floor at the same flow as a "
317 "loss-coefficient drop (scales as 1/rho at the regulator inlet; friction factor held)",
318 "JT: regulator inlet node (p, T) to outlet node p at constant enthalpy; no heat exchange "
319 "in the regulator body",
322 "gas": inp(fluid,
"",
"drawing (KBOTTLE fluid)"),
323 "bottle_volume": inp(vol * 1e3
if vol
else None,
"L",
"drawing (KBOTTLE volume)"),
324 "regulator_Cv": inp(scalar(cv)
if cv
else None,
"US gpm/psi^0.5",
"drawing (regulator Cv)"),
325 "regulator_xT": inp(x_t,
"-",
"restated for this diagnostic" if xT
is not None else
326 "assumed: unpublished; feedtwin Valve default (AUDIT 9.6 C3)"),
327 "min_inlet_differential": inp(min_diff / PSI,
"psi", min_diff_prov),
331 "species": str(fluid),
333 "loaded_kg": scalar(loaded),
334 "used_kg": scalar(used),
335 "residual_kg": scalar(residual),
336 "required_kg": scalar(required)
if required
is not None else None,
337 "margin_kg": scalar(margin)
if margin
is not None else None,
338 "unusable_kg": scalar(unusable)
if unusable
is not None else None,
339 "unusable_held_T_kg": scalar(unusable_held)
if unusable_held
is not None else None,
340 "floor_psia": scalar(floor / PSI)
if floor
is not None else None,
341 "floor_regulator_inlet_psia": scalar(floor_reg_in / PSI)
if floor_reg_in
is not None else None,
342 "floor_bottle_T_K": scalar(fl.get(
"T_b"))
if fl
else None,
343 "floor_regulator_inlet_T_K": scalar(fl.get(
"T_in"))
if fl
else None,
344 "floor_line_drop_psi": scalar(fl[
"line_drop"] / PSI)
if fl
and math.isfinite(fl[
"line_drop"])
else None,
345 "floor_converged": bool(fl.get(
"converged"))
if fl
else None,
346 "bottle_end_psia": scalar(p_b[end] / PSI),
347 "bottle_T_end_isentropic_K": scalar(T_end_isentropic),
348 "margin_psi": scalar((p_b[end] - floor) / PSI)
if floor
is not None else None,
349 "line_drop_end_psi": scalar(line_drop / PSI)
if line_drop
is not None else None,
350 "t_end": scalar(t[end]),
351 "bottle_T_K": out(T_b),
353 "twin_dT_K": out(twin_dT),
354 "jt_dT_range_K": ([scalar(np.nanmin(jt[live])), scalar(np.nanmax(jt[live]))]
if live.any()
else None),
355 "jt_basis": reg_basis,
358 except Exception
as exc:
359 return unavailable(f
"{type(exc).__name__}: {exc}")
Dict[str, Any] build_pressurant(Mapping[str, Any] result, Any prep=None, Any config=None, *, Optional[str] gas=None, Optional[float] volume_m3=None, Optional[Mapping[str, Any]] regulator=None, Optional[float] xT=None)
Dict[str, float] bottle_floor(str fluid, float mdot, float p_out_pa, float cv, float xT, float min_differential_pa, float p_b_end_pa, float T_b_end_K, float p_in_end_pa, float T_in_end_K, float line_drop_end_pa)