STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
injector.py
Go to the documentation of this file.
1"""The injector's state per twin step: jet velocities, momentum ratio, the spray's resultant tilt,
2and eta_c* from the replay.
3
4``build_injector(result, prep=None, config, *, forward=None, runner=None)`` ->
5``diagnostics.injector`` (DATA-CONTRACT 3).
6
7Every quantity is EngineDesign's own definition, called rather than restated:
8
9* jet bulk velocity ``v = mdot / (rho n A_jet)``, ``A_jet = pi d_jet^2 / 4``, at the config density
10 (the same ``v_O_bulk``/``v_F_bulk`` ``engine.core.injectors.impinging`` computes);
11* momentum ratio ``R = sqrt(rho_O v_O^2 / (rho_F v_F^2))``
12 (``impinging.momentum_ratio_R_from_bulk_velocities``; R is the Cd-dp split, not Rupe's mixing
13 criterion) and Rupe's ``M = rho_O v_O^2 d_O / (rho_F v_F^2 d_F)`` (``impinging.rupe_mixing_ratio``:
14 Rupe, JPL PR 20-195, restated by Elverum & Morey, JPL Memo 30-5, 1959, eq. 1);
15* the resultant's tilt from the chamber axis, + toward the wall, from the vector sum of the two
16 streams' momenta (``layer1_static_optimization._impinging_resultant_tilt_deg``, the rule
17 ``reconcile._angles_for_tilt`` uses; which ring is inboard from the pitch circles).
18
19The flows are the twin's (``series.*.mdot``), so these follow the burn; the densities are the
20config's, because that is what the card's injector was tabulated at (AUDIT ledger #27: the twin's
21saturated-line densities differ by 0.2 % / 0.04 %). ``design_momentum_ratio`` is EngineDesign's
22Forward solve at the burn's lockup (``forward``, or evaluated with ``runner``); without either it
23is null. ``eta_cstar`` is the replay's, linearly interpolated onto the twin's steps inside the
24replay's span and null outside it (the replay has ~28 points: never read 50 ms resolution into it).
25"""
26
27from __future__ import annotations
28
29from typing import Any, Dict, Mapping, Optional
30
31import numpy as np
32
33from engine.layerx.diag.ladder import (
34 PSI, arr, firing_mask, inp, model_block, out, scalar, series_t, unavailable,
35)
36from engine.layerx.diag.saturation import cd_effective, orifice_geometry
37
38SOURCE = ("EngineDesign: engine.core.injectors.impinging.momentum_ratio_R_from_bulk_velocities and "
39 "rupe_mixing_ratio (Rupe, JPL PR 20-195; Elverum & Morey, JPL Memo 30-5, 1959, eq. 1); "
40 "engine.optimizer.layers.layer1_static_optimization._impinging_resultant_tilt_deg (momentum "
41 "vector sum, + toward the wall)")
42
43
44def forward_design_point(prep: Any, config: Any, runner: Any = None,
45 forward: Optional[Mapping[str, Any]] = None) -> Optional[Dict[str, Any]]:
46 """EngineDesign's Forward solve at the burn's lockup, both tanks, at the site's ambient: what
47 the design column of a "design assumed X -> feed delivers Y(t)" row must come from (AUDIT 4).
48 ``forward`` (a ``runner.evaluate`` result) is returned as is; else ``runner`` is evaluated."""
49 if forward is not None:
50 return dict(forward)
51 if runner is None or prep is None:
52 return None
53 derived = getattr(prep, "derived", {}) or {}
54 lockup = derived.get("target_lockup_psia")
55 if not lockup:
56 return None
57 ambient = float(getattr(prep, "ambient_pa", None) or derived.get("ambient_pa") or 101325.0)
58 res = runner.evaluate(float(lockup) * PSI, float(lockup) * PSI, P_ambient=ambient, silent=True)
59 res = dict(res)
60 res["_basis"] = {"tank_psia": float(lockup), "ambient_pa": ambient}
61 return res
62
63
64def _replay_onto(result: Mapping[str, Any], key: str, t: np.ndarray) -> np.ndarray:
65 rep = result.get("replay") or {}
66 rt = arr(rep.get("t"))
67 rv = arr(rep.get(key)) if rep.get(key) is not None else np.full(rt.size, np.nan)
68 ok = np.isfinite(rt) & np.isfinite(rv)
69 if ok.sum() < 1:
70 return np.full(t.size, np.nan)
71 rt, rv = rt[ok], rv[ok]
72 vals = np.interp(t, rt, rv)
73 return np.where((t >= rt[0] - 1e-9) & (t <= rt[-1] + 1e-9), vals, np.nan)
74
75
76def build_injector(result: Mapping[str, Any], prep: Any = None, config: Any = None, *,
77 forward: Optional[Mapping[str, Any]] = None, runner: Any = None) -> Dict[str, Any]:
78 """``diagnostics.injector``. Never raises. Needs ``config`` (the orifices) and the series."""
79 try:
80 if config is None:
81 return unavailable("the engine config is needed for the orifices")
82 from engine.core.injectors.impinging import momentum_ratio_R_from_bulk_velocities, rupe_mixing_ratio
83 from engine.optimizer.layers.layer1_static_optimization import _impinging_resultant_tilt_deg
84
85 series = result.get("series") or {}
86 t = series_t(result)
87 n = t.size
88 firing = firing_mask(result, n)
89 go, gf = orifice_geometry(config, "ox"), orifice_geometry(config, "fuel")
90 m_o = arr((series.get("ox") or {}).get("mdot"), n)
91 m_f = arr((series.get("fuel") or {}).get("mdot"), n)
92 flowing = firing & (np.nan_to_num(m_o) > 0) & (np.nan_to_num(m_f) > 0)
93 v_o = np.where(flowing, m_o / (go["rho"] * go["area"]), np.nan)
94 v_f = np.where(flowing, m_f / (gf["rho"] * gf["area"]), np.nan)
95 R = np.full(n, np.nan)
96 M = np.full(n, np.nan)
97 tilt = np.full(n, np.nan)
98 for k in np.flatnonzero(flowing):
99 R[k] = momentum_ratio_R_from_bulk_velocities(go["rho"], gf["rho"], float(v_o[k]), float(v_f[k]))
100 M[k] = rupe_mixing_ratio(go["rho"], float(v_o[k]), go["d_jet"], gf["rho"], float(v_f[k]), gf["d_jet"])
101 tilt[k] = _impinging_resultant_tilt_deg(
102 float(m_o[k]), float(m_f[k]), go["rho"], gf["rho"], float(go["n"]), go["d_jet"], gf["d_jet"],
103 go["angle_deg"], gf["angle_deg"], spacing_O_m=go["spacing"], spacing_F_m=gf["spacing"])
104 cd_o = cd_effective(m_o, arr((series.get("ox") or {}).get("dp_injector_psi"), n), go["area"], go["rho"])
105 cd_f = cd_effective(m_f, arr((series.get("fuel") or {}).get("dp_injector_psi"), n), gf["area"], gf["rho"])
106 eta = _replay_onto(result, "eta_cstar", t)
107
108 fwd = None
109 fwd_note = "no Forward solve supplied (pass forward= or runner=)"
110 try:
111 fwd = forward_design_point(prep, config, runner, forward)
112 except Exception as exc: # noqa: BLE001
113 fwd_note = f"Forward solve failed: {type(exc).__name__}: {exc}"
114 d = (fwd or {}).get("diagnostics") or {}
115 design_R = scalar(d.get("momentum_ratio_R")) if fwd else None
116 design_M = scalar(d.get("rupe_M")) if fwd else None
117 design_tilt = None
118 if fwd and fwd.get("mdot_O") and fwd.get("mdot_F"):
119 design_tilt = scalar(_impinging_resultant_tilt_deg(
120 float(fwd["mdot_O"]), float(fwd["mdot_F"]), go["rho"], gf["rho"], float(go["n"]), go["d_jet"],
121 gf["d_jet"], go["angle_deg"], gf["angle_deg"], spacing_O_m=go["spacing"], spacing_F_m=gf["spacing"]))
122 if fwd:
123 fwd_note = f"EngineDesign Forward at {((fwd.get('_basis') or {}).get('tank_psia'))} psia both tanks"
124
125 def rng(a: np.ndarray) -> Optional[list]:
126 ok = flowing & np.isfinite(a)
127 return [scalar(np.nanmin(a[ok])), scalar(np.nanmax(a[ok]))] if ok.any() else None
128
129 req = getattr(config, "design_requirements", None)
130 band = [getattr(req, "impinging_momentum_R_min", None), getattr(req, "impinging_momentum_R_max", None)]
131 return {
132 "t": out(t),
133 "v_ox": out(v_o), "v_fuel": out(v_f),
134 "momentum_ratio": out(R), "rupe_M": out(M),
135 "design_momentum_ratio": design_R, "design_rupe_M": design_M,
136 "momentum_ratio_band": band if any(b is not None for b in band) else None,
137 "resultant_angle_deg": out(tilt), "design_resultant_angle_deg": design_tilt,
138 "Cd_eff_ox": out(cd_o), "Cd_eff_fuel": out(cd_f),
139 "design_Cd": ({"ox": scalar(d.get("Cd_O")), "fuel": scalar(d.get("Cd_F"))} if fwd else None),
140 "eta_cstar": out(eta),
141 "design_eta_cstar": scalar(fwd.get("eta_cstar") or d.get("eta_cstar")) if fwd else None,
142 "eta_cstar_replay": {"t": (result.get("replay") or {}).get("t"),
143 "values": (result.get("replay") or {}).get("eta_cstar")},
144 "range": {"momentum_ratio": rng(R), "resultant_angle_deg": rng(tilt), "v_ox": rng(v_o),
145 "v_fuel": rng(v_f)},
146 "design_basis": fwd_note,
147 "model": model_block(
148 "injector state on the twin's flows (EngineDesign definitions)",
149 SOURCE,
150 ["v = mdot / (rho n A_jet) at the config density (the card's basis), the twin's mdot per step",
151 "R = sqrt(rho_O v_O^2 / (rho_F v_F^2)); M = R^2 d_O/d_F (Rupe)",
152 "tilt: + toward the wall; ring order from the pitch circles (D = n s / pi)",
153 "Cd_eff = mdot / (A sqrt(2 rho dp_inj)) on the twin's orifice drop",
154 "eta_c* interpolated between replay points, null outside the replay's span",
155 "design values: one EngineDesign Forward solve at the burn's lockup on both tanks"],
156 {"rho_ox": inp(go["rho"], "kg/m^3", "config fluids.oxidizer.density"),
157 "rho_fuel": inp(gf["rho"], "kg/m^3", "config fluids.fuel.density"),
158 "d_jet_ox": inp(go["d_jet"] * 1e3, "mm", "config injector.geometry.oxidizer.d_jet"),
159 "d_jet_fuel": inp(gf["d_jet"] * 1e3, "mm", "config injector.geometry.fuel.d_jet"),
160 "n_elements": inp(go["n"], "-", "config injector.geometry.*.n_elements"),
161 "angle_ox": inp(go["angle_deg"], "deg", "config injector.geometry.oxidizer.impingement_angle"),
162 "angle_fuel": inp(gf["angle_deg"], "deg", "config injector.geometry.fuel.impingement_angle"),
163 "spacing_ox": inp(go["spacing"] * 1e3, "mm", "config injector.geometry.oxidizer.spacing"),
164 "spacing_fuel": inp(gf["spacing"] * 1e3, "mm", "config injector.geometry.fuel.spacing")}),
165 }
166 except Exception as exc: # noqa: BLE001
167 return unavailable(f"{type(exc).__name__}: {exc}")
Dict[str, Any] build_injector(Mapping[str, Any] result, Any prep=None, Any config=None, *, Optional[Mapping[str, Any]] forward=None, Any runner=None)
Definition injector.py:77
Optional[Dict[str, Any]] forward_design_point(Any prep, Any config, Any runner=None, Optional[Mapping[str, Any]] forward=None)
Definition injector.py:45
np.ndarray _replay_onto(Mapping[str, Any] result, str key, np.ndarray t)
Definition injector.py:64