STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
regulator.py
Go to the documentation of this file.
1"""The dome regulator's operating point and the press solenoids' drops, per twin step.
2
3``build_regulator(result, prep, config=None, ...)`` -> ``diagnostics.regulator``;
4``build_solenoids(result, prep=None, config=None, ...)`` -> ``diagnostics.solenoids``
5(DATA-CONTRACT 3). Both read ``result["network"]`` (node pressures and temperatures, branch flows)
6and, for the regulator, the drawing's own regulator parameters from the prepared model.
7
8Two models, each named in its block:
9
10* **The regulator's outlet law is feedtwin's** (``feedtwin.comps.regulator.Regulator``,
11 ``outlet_setpoint``): ``p_set + S (p_ref - p_in) - D |m| / m_rated`` with ``p_set = dome + bias``
12 and the dome from the control regulator's zero-flow outlet. It is evaluated here on the
13 recorded inlet and flow, so the supply-pressure effect (``spe_psi``) and the droop
14 (``droop_psi``) are split out of the outlet the twin produced, and ``residual_psi`` (recorded
15 outlet minus that law) shows when the regulator was not on it. In feedtwin a flowing regulator
16 leaves its law only when its seat's drop exceeds inlet minus target (``Regulator.is_saturated``,
17 no band), which puts the outlet *under* the law, so under the law while flowing is **wide open**.
18 A tolerance of 0.5 % of the setpoint is allowed before calling it (the size of the band feedtwin's
19 ``Regulator.is_choked`` uses between shut and regulating), so solver noise is not read as
20 saturation; the recorded residual is there for anything finer.
21* **Capacity is IEC 60534-2-1 gas sizing** at the inlet node's state: the mass flow the seat
22 passes wide open at this instant's inlet and outlet pressures,
23 ``W = N6 Fp Kv Y sqrt(x p1 rho1)``, ``Y = 1 - x / (3 F_gamma x_T)``, ``F_gamma = gamma / 1.40``,
24 ``x`` capped at ``F_gamma x_T`` where the flow chokes (IEC 60534-2-1:2011, the compressible-flow
25 sizing equations; constants and the choke test from ``fluids.control_valve``). ``rho1`` and
26 ``gamma`` (real gas, cp/cv) are CoolProp's at the regulator inlet node. ``use_frac`` is the
27 recorded flow over that capacity. Feedtwin's own seat law is incompressible (no Y, no choke;
28 AUDIT 9.6 C1), so this is the physics check the twin does not make.
29
30The solenoids' drops are the twin's (the branch's recorded ``dp_psi``), set against the
31regulator-outlet-to-tank drop on the same side (``share_of_reg_to_tank``), with the IEC 60534
32drop at the recorded flow alongside, and the drop the same flow would cost through a valve of
33twice the Cv (the "upgrade the 1.7 Cv solenoids?" question, AUDIT 9.6 D).
34"""
35
36from __future__ import annotations
37
38import math
39from typing import Any, Dict, List, Mapping, Optional, Tuple
40
41import numpy as np
42from feedtwin.comps.regulator import SUPPLY_ZERO
43
44from engine.layerx.diag.ladder import (
45 PSI, SIDES, arr, inp, model_block, network_of, node_p, node_T, out, scalar, series_t, unavailable,
46)
47
48IEC_SOURCE = ("IEC 60534-2-1:2011, Industrial-process control valves - Part 2-1: Flow capacity - Sizing "
49 "equations for fluid flow under installed conditions; compressible flow, turbulent, no "
50 "attached fittings (Fp = 1): W = N6 Fp Kv Y sqrt(x p1 rho1), Y = 1 - x/(3 F_gamma x_T), "
51 "F_gamma = gamma/1.40, choked at x >= F_gamma x_T. Constants (N6, Cv->Kv) and the choke test "
52 "from fluids.control_valve.")
53
54DEFAULT_XT = 0.7
55"""x_T for a valve with no declared value. The 1092's x_T is unpublished; 0.7 is feedtwin's
56``Valve`` default and the value AUDIT 9.6 C3 used (it swept 0.5 as well)."""
57
58MODE_BAND = 0.005
59"""Fraction of the setpoint by which the outlet must sit under the law, while flowing, to read as
60wide open. A tolerance, assumed: it is the size of the band feedtwin's ``Regulator.is_choked`` puts
61between shut and regulating; feedtwin's own saturation test (``is_saturated``) has no band."""
62
63COOLPROP_NAMES = {"helium": "Helium", "nitrogen": "Nitrogen", "oxygen": "Oxygen", "ethanol": "Ethanol",
64 "water": "Water", "argon": "Argon"}
65
66
67def coolprop_name(fluid: str) -> str:
68 return COOLPROP_NAMES.get(str(fluid).strip().lower(), str(fluid))
69
70
71def gas_state(fluid: str, p_pa: float, T_K: float) -> Tuple[float, float]:
72 """(rho [kg/m^3], gamma = cp/cv) from CoolProp; NaNs where CoolProp cannot answer."""
73 import CoolProp.CoolProp as CP
74
75 try:
76 name = coolprop_name(fluid)
77 rho = CP.PropsSI("D", "P", p_pa, "T", T_K, name)
78 gamma = CP.PropsSI("CPMASS", "P", p_pa, "T", T_K, name) / CP.PropsSI("CVMASS", "P", p_pa, "T", T_K, name)
79 return float(rho), float(gamma)
80 except Exception: # noqa: BLE001 - out of CoolProp's range: no answer, not a crash
81 return math.nan, math.nan
82
83
84# ---- IEC 60534-2-1 ------------------------------------------------------------------------------
85
86def iec_gas_flow(p1_pa: float, p2_pa: float, rho1: float, gamma: float, cv: float,
87 xT: float = DEFAULT_XT) -> Dict[str, Any]:
88 """Mass flow [kg/s] a valve of flow coefficient ``cv`` (US Cv) passes wide open from ``p1`` to
89 ``p2``, by IEC 60534-2-1 (turbulent, Fp = 1). Returns {mdot, choked, Y, x, x_choke}."""
90 from fluids.control_valve import N6, Cv_to_Kv, is_choked_turbulent_g
91
92 if not all(math.isfinite(v) for v in (p1_pa, p2_pa, rho1, gamma, cv, xT)) or p1_pa <= 0 or rho1 <= 0 \
93 or cv <= 0 or gamma <= 0:
94 return {"mdot": math.nan, "choked": None, "Y": math.nan, "x": math.nan, "x_choke": math.nan}
95 x = max(0.0, (p1_pa - p2_pa) / p1_pa)
96 f_gamma = gamma / 1.40
97 x_choke = f_gamma * xT
98 choked = bool(is_choked_turbulent_g(x, f_gamma, xT)) if x > 0 else False
99 x_eff = min(x, x_choke)
100 Y = 1.0 - x_eff / (3.0 * f_gamma * xT)
101 # N6 is for W in kg/h with p1 in kPa.
102 w_kg_h = N6 * Cv_to_Kv(cv) * Y * math.sqrt(x_eff * (p1_pa / 1e3) * rho1)
103 return {"mdot": w_kg_h / 3600.0, "choked": choked, "Y": Y, "x": x, "x_choke": x_choke}
104
105
106def iec_gas_drop(mdot: float, p1_pa: float, rho1: float, gamma: float, cv: float,
107 xT: float = DEFAULT_XT) -> float:
108 """Pressure drop [Pa] that passes ``mdot`` [kg/s] through a valve of ``cv`` by IEC 60534-2-1,
109 the inverse of :func:`iec_gas_flow`. NaN when the flow exceeds the choked capacity."""
110 if not (math.isfinite(mdot) and mdot >= 0.0):
111 return math.nan
112 if mdot == 0.0:
113 return 0.0
114 full = iec_gas_flow(p1_pa, 0.0, rho1, gamma, cv, xT)
115 if not math.isfinite(full["mdot"]) or mdot > full["mdot"]:
116 return math.nan
117 from scipy.optimize import brentq
118
119 x_hi = full["x_choke"]
120 p2_hi = p1_pa * (1.0 - x_hi)
121 f = lambda p2: iec_gas_flow(p1_pa, p2, rho1, gamma, cv, xT)["mdot"] - mdot # noqa: E731
122 if f(p2_hi) < 0.0:
123 return math.nan
124 p2 = brentq(f, p2_hi, p1_pa, xtol=1e-10 * p1_pa, rtol=4 * np.finfo(float).eps)
125 return float(p1_pa - p2)
126
127
128# ---- drawing parameters -------------------------------------------------------------------------
129
130def _provenance(component: Any, name: str) -> str:
131 """'<source>: <reference>' for one of a component's declared parameters."""
132 try:
133 prm = component.instance.params.get(name)
134 except Exception: # noqa: BLE001
135 prm = None
136 if prm is None:
137 return "not on the drawing (library default)"
138 src = getattr(prm, "source", None)
139 src = getattr(src, "value", src)
140 ref = getattr(prm, "reference", "") or ""
141 return f"{src}: {ref}" if ref else str(src)
142
143
144def _built(prep: Any) -> Any:
145 return getattr(getattr(prep, "model", None), "built", None)
146
147
148def regulator_id(net: Mapping[str, Any]) -> Optional[str]:
149 """The regulator both feed paths pass through (the dome regulator), else the first one."""
150 regs = [b for b, br in net["branches"].items() if br.get("kind") == "regulator"]
151 paths = net.get("paths") or {}
152 shared = [b for b in regs if all(b in (paths.get(s) or []) for s in SIDES if paths.get(s))]
153 return (shared or regs or [None])[0]
154
155
156def regulator_params(prep: Any, reg_id: str) -> Dict[str, Tuple[float, str]]:
157 """{name: (SI value, provenance)} of the regulator branch's component on the prepared model,
158 plus ``dome`` (the control regulator's loading at the dial) and the loader's own law."""
159 built = _built(prep)
160 if built is None:
161 raise ValueError("the prepared model is needed for the regulator's drawing parameters")
162 comp = built.network.branches[reg_id].component
163 params: Dict[str, Tuple[float, str]] = {}
164 for name in ("Cv", "bore", "supply_coefficient", "flow_droop", "rated_flow",
165 "dome_bias", "dome_pressure", "setpoint", "min_inlet_differential", "xT"):
166 if name in comp.p:
167 params[name] = (float(comp.p[name]), _provenance(comp, name))
168 from feedtwin.session.gauge import from_psig
169
170 setup = getattr(prep, "setup", None)
171 dome_psig = getattr(setup, "dome_psi", None)
172 if dome_psig is None:
173 dome_psig = (getattr(prep, "derived", {}) or {}).get("dome_psig")
174 loader = next((ld for ld in getattr(built, "dome_loaders", {}).values()
175 if str(getattr(ld, "signal", "")).startswith(f"{comp.id}.")), None)
176 if dome_psig is not None:
177 params["dial"] = (float(from_psig(float(dome_psig))),
178 "Layer X: the control-regulator dial solved for the target lockup (prepare.dome_for_lockup)")
179 if loader is not None:
180 lc = loader.component
181 params["loader_bias"] = (float(lc.p.get("dome_bias", 0.0)), _provenance(lc, "dome_bias"))
182 params["loader_supply_coefficient"] = (float(lc.p.get("supply_coefficient", 0.0)),
183 _provenance(lc, "supply_coefficient"))
184 params["_loader_supply_node"] = (math.nan, str(loader.supply_node))
185 params["_loader_id"] = (math.nan, str(loader.id))
186 return params
187
188
189def _pressurant(prep: Any, net: Mapping[str, Any], node: str) -> str:
190 built = _built(prep)
191 if built is not None:
192 try:
193 return str(built.network.nodes[node].fluid)
194 except Exception: # noqa: BLE001
195 pass
196 gas = (getattr(prep, "derived", {}) or {}).get("pressurant_gas") if prep is not None else None
197 if gas:
198 return str(gas)
199 raise ValueError("the pressurant species is unknown (no prepared model)")
200
201
202# ---- the regulator ------------------------------------------------------------------------------
203
204def build_regulator(result: Mapping[str, Any], prep: Any = None, config: Any = None, *,
205 reg_id: Optional[str] = None, xT: Optional[float] = None,
206 params: Optional[Mapping[str, Tuple[float, str]]] = None,
207 gas: Optional[str] = None) -> Dict[str, Any]:
208 """``diagnostics.regulator``. Never raises.
209
210 ``params`` ({name: (SI value, provenance)}, the names of :func:`regulator_params`) and ``gas``
211 stand in for ``prep`` (tests, or a saved run re-read without its model). ``xT`` overrides the
212 drawing's / :data:`DEFAULT_XT`.
213 """
214 try:
215 net = network_of(result)
216 t = series_t(result)
217 n = t.size
218 rid = reg_id or regulator_id(net)
219 if rid is None:
220 return unavailable("no regulator on the recorded network")
221 br = net["branches"][rid]
222 prm = dict(params) if params is not None else regulator_params(prep, rid)
223 fluid = gas or _pressurant(prep, net, br["from"])
224 p_in = node_p(net, br["from"], n) * PSI
225 p_out = node_p(net, br["to"], n) * PSI
226 T_in = node_T(net, br["from"], n)
227 mdot = arr(br.get("mdot"), n)
228
229 def val(name: str, default: float = 0.0) -> float:
230 return float(prm[name][0]) if name in prm else default
231
232 cv = val("Cv", float(br.get("cv") or math.nan))
233 x_t = float(xT) if xT is not None else val("xT", DEFAULT_XT)
234 x_t_prov = ("restated for this diagnostic" if xT is not None else
235 prm["xT"][1] if "xT" in prm else
236 "assumed: the regulator's x_T is not published or drawn; 0.7 is feedtwin's Valve default "
237 "and AUDIT 9.6 C3's value")
238
239 # Dome: the control regulator's zero-flow outlet at its supply, as the session loads it.
240 if "dial" in prm:
241 dome = np.full(n, val("dial")) + val("loader_bias")
242 s_l = val("loader_supply_coefficient")
243 supply_node = prm.get("_loader_supply_node", (0, ""))[1]
244 if s_l and supply_node:
245 dome = dome + s_l * (SUPPLY_ZERO - node_p(net, supply_node, n) * PSI)
246 dome_basis = prm["dial"][1]
247 elif "dome_pressure" in prm and val("dome_pressure") > 0:
248 dome = np.full(n, val("dome_pressure"))
249 dome_basis = prm["dome_pressure"][1]
250 else:
251 dome = np.full(n, math.nan)
252 dome_basis = "no dome: a hand-loaded setpoint"
253 bias = val("dome_bias")
254 p_set = np.where(np.isfinite(dome), dome + bias, val("setpoint", math.nan))
255 S = val("supply_coefficient")
256 spe = S * (SUPPLY_ZERO - p_in)
257 D, rated = val("flow_droop"), val("rated_flow")
258 droop = D * np.abs(mdot) / rated if rated > 0 else np.zeros(n)
259 target = p_set + spe - droop
260 residual = p_out - target
261 flowing = np.nan_to_num(mdot) > 1e-9
262 wide_open = flowing & (residual < -MODE_BAND * p_set)
263
264 cap = np.full(n, math.nan)
265 choked: List[Optional[bool]] = []
266 x_series = np.full(n, math.nan)
267 rho_in = np.full(n, math.nan)
268 for k in range(n):
269 rho, gamma = gas_state(fluid, p_in[k], T_in[k]) if math.isfinite(p_in[k] + T_in[k]) else (math.nan, math.nan)
270 rho_in[k] = rho
271 r = iec_gas_flow(p_in[k], p_out[k], rho, gamma, cv, x_t)
272 cap[k] = r["mdot"]
273 x_series[k] = r["x"]
274 choked.append(r["choked"])
275 with np.errstate(divide="ignore", invalid="ignore"):
276 use = np.where(cap > 0, np.abs(mdot) / cap, np.nan)
277
278 # Rise of the outlet over the burn split into its two drawn causes.
279 firing = np.asarray([bool(x) for x in (result.get("series") or {}).get("firing") or [False] * n])
280 idx = np.flatnonzero(firing & flowing)
281 split = None
282 if idx.size >= 2:
283 a, b = idx[0], idx[-1]
284 split = {"t0": scalar(t[a]), "t1": scalar(t[b]),
285 "outlet_rise_psi": scalar((p_out[b] - p_out[a]) / PSI),
286 "spe_psi": scalar((spe[b] - spe[a]) / PSI),
287 "droop_psi": scalar(-(droop[b] - droop[a]) / PSI),
288 "dome_psi": scalar((dome[b] - dome[a]) / PSI) if np.isfinite(dome[[a, b]]).all() else None,
289 "residual_psi": scalar(((p_out[b] - p_out[a]) - (target[b] - target[a])) / PSI)}
290
291 def pv(name: str, unit: str, scale: float = 1.0) -> Dict[str, Any]:
292 v, prov = prm.get(name, (math.nan, "not on the drawing (library default 0)"))
293 return inp(v / scale if math.isfinite(v) else None, unit, prov)
294
295 model = model_block(
296 "dome regulator: drawn outlet law + IEC 60534-2-1 capacity",
297 "feedtwin.comps.regulator.Regulator.outlet_setpoint (the drawing's coefficients); " + IEC_SOURCE,
298 [
299 "outlet law evaluated on the twin's recorded regulator-inlet node pressure and branch flow",
300 "dome = control-regulator dial + its bias + its own supply effect at its supply node (zero flow), "
301 "as feedtwin.session loads it",
302 f"wide open = flowing and the outlet more than {MODE_BAND:.1%} of the setpoint under the law "
303 "(an assumed tolerance, the size of feedtwin Regulator.is_choked's shut/regulating band; "
304 "feedtwin's is_saturated itself has none)",
305 "capacity at this instant's inlet and outlet node pressures, wide open; turbulent; Fp = 1 "
306 "(no reducer data on the drawing)",
307 "inlet density and gamma = cp/cv from CoolProp (real gas) at the inlet node's pressure and "
308 "temperature",
309 "supply effect measured from zero inlet (gauge): outlet = dome + bias - S x inlet, as "
310 "feedtwin.comps.regulator.Regulator.supply_effect has it (the team, 2026-10-07)",
311 ],
312 {
313 "Cv": pv("Cv", "US gpm/psi^0.5"),
314 "xT": inp(x_t, "-", x_t_prov),
315 "supply_coefficient": pv("supply_coefficient", "psi/psi"),
316 "flow_droop": pv("flow_droop", "psi", PSI),
317 "rated_flow": pv("rated_flow", "kg/s"),
318 "dome_bias": pv("dome_bias", "psi", PSI),
319 "dome": inp(scalar(np.nanmean(dome) / PSI) if np.isfinite(dome).any() else None, "psia", dome_basis),
320 "gas": inp(fluid, "", "drawing (the regulator inlet node's fluid)"),
321 "wide_open_band": inp(MODE_BAND, "fraction of setpoint",
322 "assumed: tolerance before 'wide open'; the size of feedtwin "
323 "Regulator.is_choked's band, no source for this use"),
324 },
325 )
326 return {
327 "id": rid,
328 "label": br.get("label") or rid,
329 "t": out(t),
330 "inlet_psia": out(p_in / PSI),
331 "outlet_psia": out(p_out / PSI),
332 "inlet_T_K": out(T_in),
333 "inlet_rho_kg_m3": out(rho_in),
334 "mdot": out(mdot),
335 "setpoint_psia": out(p_set / PSI),
336 "dome_psia": out(dome / PSI),
337 "target_psia": out(target / PSI),
338 "residual_psi": out(residual / PSI),
339 "capacity_mdot": out(cap),
340 "use_frac": out(use),
341 "x": out(x_series),
342 "droop_psi": out(droop / PSI),
343 "spe_psi": out(spe / PSI),
344 "choked": choked,
345 "wide_open": [bool(w) for w in wide_open],
346 "any_wide_open": bool(wide_open.any()),
347 "use_frac_max": scalar(np.nanmax(use)) if np.isfinite(use).any() else None,
348 "rise": split,
349 "cv": scalar(cv),
350 "model": model,
351 }
352 except Exception as exc: # noqa: BLE001
353 return unavailable(f"{type(exc).__name__}: {exc}")
354
355
356# ---- the press solenoids ------------------------------------------------------------------------
357
358def _gas_reach(net: Mapping[str, Any], side: str, reg: str) -> Tuple[List[str], Optional[str]]:
359 """The branches from the regulator's outlet to the tank on one side, and the tank's node."""
360 path = list((net.get("paths") or {}).get(side) or [])
361 if reg not in path:
362 return [], None
363 seg: List[str] = []
364 prev = None
365 for bid in path[path.index(reg) + 1:]:
366 br = net["branches"][bid]
367 # the tank: the recorder's own head element (<tank>.head, kind tank_head) starts at the
368 # ullage node; a recorder without one leaves a gap between the ullage and the outlet
369 if br.get("kind") == "tank_head" or (prev is not None and br["from"] != prev):
370 return seg, (br["from"] if br.get("kind") == "tank_head" else prev)
371 seg.append(bid)
372 prev = br["to"]
373 return [], None
374
375
376def build_solenoids(result: Mapping[str, Any], prep: Any = None, config: Any = None, *,
377 gas: Optional[str] = None, xT: Optional[float] = None) -> Any:
378 """``diagnostics.solenoids``: one row per solenoid between the regulator and a tank. Never
379 raises (a failure is ``{available: false, error}`` in place of the list)."""
380 try:
381 net = network_of(result)
382 t = series_t(result)
383 n = t.size
384 reg = regulator_id(net)
385 if reg is None:
386 return unavailable("no regulator on the recorded network")
387 built = _built(prep)
388 rows: List[Dict[str, Any]] = []
389 for side in SIDES:
390 seg, tank = _gas_reach(net, side, reg)
391 if not seg or tank is None:
392 continue
393 reg_out = net["branches"][reg]["to"]
394 reg_to_tank = node_p(net, reg_out, n) - node_p(net, tank, n)
395 for bid in seg:
396 br = net["branches"][bid]
397 if br.get("kind") not in ("solenoid", "valve"):
398 continue
399 dp = arr(br.get("dp_psi"), n) if br.get("dp_psi") is not None else \
400 node_p(net, br["from"], n) - node_p(net, br["to"], n)
401 cv = float(br.get("cv") or math.nan)
402 x_t = xT
403 cv_prov = "drawing (recorded network)"
404 xt_prov = "restated for this diagnostic" if xT is not None else \
405 "assumed: feedtwin Valve default 0.7 (no x_T on the drawing)"
406 if built is not None:
407 try:
408 comp = built.network.branches[bid].component
409 cv = float(comp.p.get("Cv", cv))
410 cv_prov = _provenance(comp, "Cv")
411 if x_t is None and "xT" in comp.p:
412 x_t = float(comp.p["xT"])
413 xt_prov = _provenance(comp, "xT")
414 except Exception: # noqa: BLE001
415 pass
416 x_t = DEFAULT_XT if x_t is None else x_t
417 fluid = gas or _pressurant(prep, net, br["from"])
418 mdot = arr(br.get("mdot"), n)
419 p1 = node_p(net, br["from"], n) * PSI
420 T1 = node_T(net, br["from"], n)
421 iec = np.full(n, math.nan)
422 iec2 = np.full(n, math.nan)
423 for k in range(n):
424 if not (math.isfinite(mdot[k]) and mdot[k] > 1e-9 and math.isfinite(p1[k] + T1[k])):
425 if math.isfinite(mdot[k]) and abs(mdot[k]) <= 1e-9:
426 iec[k] = iec2[k] = 0.0
427 continue
428 rho, gamma = gas_state(fluid, p1[k], T1[k])
429 iec[k] = iec_gas_drop(mdot[k], p1[k], rho, gamma, cv, x_t) / PSI
430 iec2[k] = iec_gas_drop(mdot[k], p1[k], rho, gamma, 2.0 * cv, x_t) / PSI
431 with np.errstate(divide="ignore", invalid="ignore"):
432 share = np.where(np.abs(reg_to_tank) > 1e-9, dp / reg_to_tank, np.nan)
433 firing = np.asarray([bool(x) for x in (result.get("series") or {}).get("firing") or [False] * n])
434 live = firing & np.isfinite(dp)
435 rows.append({
436 "id": bid, "label": br.get("label") or bid, "side": side, "cv": scalar(cv),
437 "cv_provenance": cv_prov,
438 "dp_psi": out(dp), "share_of_reg_to_tank": out(share),
439 "reg_to_tank_psi": out(reg_to_tank),
440 "dp_iec_psi": out(iec), "dp_iec_2cv_psi": out(iec2),
441 "dp_max_psi": scalar(np.nanmax(dp[live])) if live.any() else None,
442 "share_max": scalar(np.nanmax(share[live])) if live.any() and np.isfinite(share[live]).any() else None,
443 "model": model_block(
444 "press solenoid drop: twin vs IEC 60534-2-1",
445 IEC_SOURCE,
446 ["dp_psi is the twin's own branch drop (feedtwin Valve, the Cv law at inlet density)",
447 "dp_iec_psi: IEC 60534 drop at the recorded flow and the inlet node's CoolProp state",
448 "dp_iec_2cv_psi: the same flow through twice the Cv (flows held: a first-order "
449 "what-if, not a re-burn)"],
450 {"Cv": inp(cv, "US gpm/psi^0.5", cv_prov),
451 "xT": inp(x_t, "-", xt_prov)}),
452 })
453 return rows
454 except Exception as exc: # noqa: BLE001
455 return unavailable(f"{type(exc).__name__}: {exc}")
#define min(a, b)
Definition Arduino.h:42
#define max(a, b)
Definition Arduino.h:43
Dict[str, Any] build_regulator(Mapping[str, Any] result, Any prep=None, Any config=None, *, Optional[str] reg_id=None, Optional[float] xT=None, Optional[Mapping[str, Tuple[float, str]]] params=None, Optional[str] gas=None)
Definition regulator.py:207
str _pressurant(Any prep, Mapping[str, Any] net, str node)
Definition regulator.py:189
Dict[str, Tuple[float, str]] regulator_params(Any prep, str reg_id)
Definition regulator.py:156
Tuple[float, float] gas_state(str fluid, float p_pa, float T_K)
Definition regulator.py:71
Tuple[List[str], Optional[str]] _gas_reach(Mapping[str, Any] net, str side, str reg)
Definition regulator.py:358
Any build_solenoids(Mapping[str, Any] result, Any prep=None, Any config=None, *, Optional[str] gas=None, Optional[float] xT=None)
Definition regulator.py:377
str _provenance(Any component, str name)
Definition regulator.py:130
str coolprop_name(str fluid)
Definition regulator.py:67
float iec_gas_drop(float mdot, float p1_pa, float rho1, float gamma, float cv, float xT=DEFAULT_XT)
Definition regulator.py:107
Optional[str] regulator_id(Mapping[str, Any] net)
Definition regulator.py:148
Dict[str, Any] iec_gas_flow(float p1_pa, float p2_pa, float rho1, float gamma, float cv, float xT=DEFAULT_XT)
Definition regulator.py:87