1"""Layer-1 injector pressure-drop ratio penalty: per-stream quadratic hinge on ΔP_inj / Pc."""
3from __future__
import annotations
5from typing
import Any, Dict, Optional, Tuple
12_DEFAULT_INJECTOR_DP_GATE_RTOL = 2.0e-4
15_DEFAULT_DP_O_BAND = (0.20, 0.40)
16_DEFAULT_DP_F_BAND = (0.20, 0.40)
17_LEGACY_DP_BAND = (0.15, 0.35)
21 requirements: Dict[str, Any],
23 default_o: Tuple[float, float] = _DEFAULT_DP_O_BAND,
24 default_f: Tuple[float, float] = _DEFAULT_DP_F_BAND,
25) -> Tuple[Tuple[float, float], Tuple[float, float]]:
26 """Read oxidizer/fuel ΔP_inj/Pc bands; migrate legacy [0.15, 0.35] yaml to [0.20, 0.40]."""
28 def _one(min_key: str, max_key: str, default: Tuple[float, float]) -> Tuple[float, float]:
29 lo = float(requirements.get(min_key, default[0]))
30 hi = float(requirements.get(max_key, default[1]))
31 if abs(lo - _LEGACY_DP_BAND[0]) < 1e-9
and abs(hi - _LEGACY_DP_BAND[1]) < 1e-9:
32 return _DEFAULT_DP_O_BAND
36 _one(
"injector_dp_ratio_O_min",
"injector_dp_ratio_O_max", default_o),
37 _one(
"injector_dp_ratio_F_min",
"injector_dp_ratio_F_max", default_f),
46 rtol: float = _DEFAULT_INJECTOR_DP_GATE_RTOL,
47 atol_floor: float = 5.0e-7,
49 """True if finite ``r`` lies in ``[lo, hi]`` with a small symmetric margin at the edges."""
54 except (TypeError, ValueError):
56 if not np.isfinite(rr):
58 lo_f, hi_f = float(lo), float(hi)
61 scale_ref =
max(abs(lo_f), abs(hi_f), abs(rr), 0.05)
62 margin =
max(float(rtol) * scale_ref, float(atol_floor))
63 return (lo_f - margin) <= rr <= (hi_f + margin)
67 """Squared shortfall when ratio < floor (extra cost for LOX ΔP_inj/Pc too low).
69 Returns 0 when ``floor`` is None or ``r`` is missing/non-finite.
73 if r
is None or not np.isfinite(r):
77 short =
max(0.0, fl - rr)
78 return float(short * short)
82 """Normalized squared hinge penalty outside [lo, hi]. Zero when lo <= r <= hi.
84 Normalization by band width keeps penalty strength consistent across different
85 configured bands and avoids under-penalizing small absolute misses.
91 except (TypeError, ValueError):
93 if not np.isfinite(rr):
95 lo_f, hi_f = float(lo), float(hi)
98 span =
max(hi_f - lo_f, 1e-9)
99 below =
max(0.0, lo_f - rr)
100 above =
max(0.0, rr - hi_f)
101 return float((below / span) ** 2 + (above / span) ** 2)
105 """Normalized squared deviation from the band CENTRE: ``((r - mid)/span)**2``.
107 Unlike the hinge (which is zero everywhere inside the band), this is nonzero inside the
108 band too, so a small weight on it gives the optimizer a gentle preference for the middle
109 of the pass band instead of parking on an edge (e.g. 0.40). Zero for missing/non-finite
110 ``r`` or an invalid band.
116 except (TypeError, ValueError):
118 if not np.isfinite(rr):
120 lo_f, hi_f = float(lo), float(hi)
123 span =
max(hi_f - lo_f, 1e-9)
124 mid = 0.5 * (lo_f + hi_f)
125 return float(((rr - mid) / span) ** 2)
129 ratio_o: Optional[float],
130 ratio_f: Optional[float],
132 w_dp_high: float = 0.0,
134 o_band: Tuple[float, float] = (0.20, 0.40),
135 f_band: Tuple[float, float] = (0.50, 1.20),
136 w_dp_o: Optional[float] =
None,
137 w_dp_f: Optional[float] =
None,
138 o_soft_floor: Optional[float] =
None,
139 w_dp_o_floor: float = 0.0,
140 w_dp_center: float = 0.0,
142 """Soft weighted penalty for oxidizer and fuel ΔP_inj/Pc ratios (independent bands).
144 Per-stream weights: if ``w_dp_o`` / ``w_dp_f`` are None, both streams use ``w_dp``.
145 Otherwise ``injector_penalty = w_dp_o * hinge_O + w_dp_f * hinge_F``.
147 Optionally adds ``w_dp_o_floor * (max(0, o_soft_floor - ratio_o))**2`` when
148 ``o_soft_floor`` is set (stronger discourage very low oxidizer ΔP/Pc).
150 ``w_dp_center`` (>0) adds a gentle ``w_dp_center * ((r - mid)/span)**2`` per stream that
151 pulls each ratio toward the CENTRE of its pass band — the "soft target" so the optimizer
152 settles near the middle (~0.30 for a [0.20,0.40] band) rather than the 0.40 edge. It is
153 deliberately small relative to the band hinge so it only breaks ties inside the band and
154 never overrides feasibility/thrust/O-F.
156 ``w_dp_high`` is retained for call-site compatibility but does not contribute (legacy tier removed).
158 lo_o, hi_o = float(o_band[0]), float(o_band[1])
159 lo_f, hi_f = float(f_band[0]), float(f_band[1])
162 wo = float(w_dp_o)
if w_dp_o
is not None else float(w_dp)
163 wf = float(w_dp_f)
if w_dp_f
is not None else float(w_dp)
166 wflo = float(w_dp_o_floor)
if np.isfinite(w_dp_o_floor)
else 0.0
167 wc = float(w_dp_center)
if np.isfinite(w_dp_center)
else 0.0
172 return wo * float(s_o) + wf * float(s_f) + wflo * float(floor_pen) + wc * float(center_pen)
176 """Deprecated: symmetric LOX-style band (0.20, 0.35). Returns (hinge, 0)."""
182 """ΔP_inj / Pc using **injector-face** ΔP only (not tank−Pc).
184 Impinging feed coupling produces ``diagnostics['delta_p_injector_{O,F}']`` =
185 ``P_inj − Pc``, where ``P_inj`` is stagnation **after** ``delta_p_feed`` from the tank.
186 Ratios used for Layer‑1 gates and penalties are ``delta_p_injector_* / Pc``.
188 if not np.isfinite(pc)
or pc <= 0:
190 diag = result.get(
"diagnostics")
if isinstance(result.get(
"diagnostics"), dict)
else {}
191 ip = result.get(
"injector_pressure")
if isinstance(result.get(
"injector_pressure"), dict)
else {}
193 def ratio_one(side: str) -> Optional[float]:
194 dp = diag.get(f
"delta_p_injector_{side}")
195 if dp
is None or not np.isfinite(dp):
196 p_inj = diag.get(f
"P_injector_{side}")
198 p_inj = ip.get(f
"P_injector_{side}")
199 if p_inj
is not None and np.isfinite(p_inj):
200 dp = float(p_inj) - float(pc)
201 if dp
is None or not np.isfinite(dp):
203 return float(dp) / float(pc)
205 return ratio_one(
"O"), ratio_one(
"F")
float stream_injector_dp_band_hinge_squared(Optional[float] r, float lo, float hi)
Tuple[Optional[float], Optional[float]] injector_dp_ratios_from_eval_result(float pc, Dict[str, Any] result)
Optional[bool] injector_dp_ratio_within_gate(Optional[float] r, float lo, float hi, *, float rtol=_DEFAULT_INJECTOR_DP_GATE_RTOL, float atol_floor=5.0e-7)
float injector_dp_ratio_penalty_weighted(Optional[float] ratio_o, Optional[float] ratio_f, float w_dp, float w_dp_high=0.0, *, Tuple[float, float] o_band=(0.20, 0.40), Tuple[float, float] f_band=(0.50, 1.20), Optional[float] w_dp_o=None, Optional[float] w_dp_f=None, Optional[float] o_soft_floor=None, float w_dp_o_floor=0.0, float w_dp_center=0.0)
float stream_injector_dp_soft_floor_squared(Optional[float] r, Optional[float] floor)
Tuple[Tuple[float, float], Tuple[float, float]] injector_dp_bands_from_requirements(Dict[str, Any] requirements, *, Tuple[float, float] default_o=_DEFAULT_DP_O_BAND, Tuple[float, float] default_f=_DEFAULT_DP_F_BAND)
float stream_injector_dp_center_squared(Optional[float] r, float lo, float hi)
Tuple[float, float] stream_injector_dp_raw_terms(float r)