STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
link.py
Go to the documentation of this file.
1"""The engine on the end of the feed system: EngineDesign's, as the twin runs it.
2
3The twin's own engine is simple:
4* an orifice leg per side, with the config's Reynolds-law Cd;
5* ``p_c = mdot c* eta / A_t`` on the CEA table, with ``eta_c* = 1``. Its importer says
6 that chamber pressure is an upper bound.
7
8EngineDesign's engine has much more in it:
9* a plate-passage Cd and a ring-network manifold;
10* the Borda dump of the feed line into that manifold;
11* a spray/mixing/heat-loss c* efficiency;
12* a stagnation loss and the real nozzle exit.
13
14On the 6.8 kN engine at 578 psia the two differ by ~14 % in flow. Three ways to put
15an engine on the end of the twin, as ``mode``:
16
17``card`` (default, phase 2)
18 EngineDesign sampled across the burn envelope and tabulated as an engine card
19 (:mod:`engine.layerx.card`, :mod:`feedtwin.engine.card`). The twin evaluates
20 EngineDesign's injector and chamber at every coupling step, everywhere the
21 burn goes. The card carries its own measured error.
22
23``calibrated`` (phase 1)
24 The twin's engine, with four constants set so that it reproduces EngineDesign
25 at one point, T-0:
26 * a Cd per side;
27 * ``eta_c*``;
28 * ``eta_n``.
29
30 Away from T-0 they are held constant. Kept for comparison.
31
32``native``
33 The twin's engine as imported. For seeing what the other two change.
34
35Both EngineDesign modes put the boundary at the **line exit**. EngineDesign is
36solved with its feed-line losses zeroed and its exit dump kept
37(:func:`engine.layerx.card.line_exit_config`), because the drawing's lines carry
38every loss upstream of the injector face and none downstream of it.
39
40Phase 1 calibrated against EngineDesign's still-manifold injector drop, which
41left that ~26 psi LOX dump on neither side.
42"""
43
44from __future__ import annotations
45
46import math
47from dataclasses import dataclass, field, replace
48from pathlib import Path
49from typing import Any, Dict, Optional, Tuple
50
51from engine.layerx.card import EngineSampler, card_for
52
53PSI = 6894.757293168361
54
55MODES = ("card", "calibrated", "native")
56
57
58@dataclass
60 """The twin's engine, and how it was made to agree with EngineDesign."""
61
62 design: Any
63 """feedtwin.engine.EngineDesign, with an engine card attached, calibrated, or native."""
64 chamber: Any
65 """feedtwin.engine.Chamber (a CardChamber in card mode)."""
66 mode: str
67 reference: Dict[str, float] = field(default_factory=dict)
68 """EngineDesign at the line exit, at the T-0 pressures."""
69 calibration: Dict[str, Any] = field(default_factory=dict)
70 card: Any = None
71 """The feedtwin.engine.EngineCard, in card mode."""
72 sampler: Any = None
73 """EngineDesign at the line exit, for checking a burn against it afterwards."""
74 notes: list = field(default_factory=list)
75
76
77def _reference(sampler: EngineSampler, p_O: float, p_F: float) -> Dict[str, float]:
78 point = sampler(p_O, p_F)
79 if point is None:
80 raise ValueError(f"EngineDesign has no solution at {p_O / PSI:.1f} / {p_F / PSI:.1f} psia "
81 "at the line exit")
82 return {**point, "p_O": p_O, "p_F": p_F, "P_ambient": sampler.ambient_pa,
83 "dp_line_O": p_O - point["Pc"], "dp_line_F": p_F - point["Pc"]}
84
85
86def _constant_cd(value: float) -> Any:
87 """A discharge model that is ``value`` at every Reynolds number."""
88 from feedtwin.engine.design import DischargeModel
89
90 return DischargeModel(cd_inf=value, a_re=0.0, cd_min=min(value, 0.15), cd_inf_max=max(value, 0.62))
91
92
93def _chamber(design: Any, table: Any, ambient_pa: float, **losses: float) -> Any:
94 from feedtwin.engine import Chamber
95
96 return Chamber(design.throat_area, table, volume=design.chamber_volume, ambient_pressure=ambient_pa,
97 **losses)
98
99
101 config: Any,
102 *,
103 tank_pa_O: float,
104 tank_pa_F: float,
105 cea_path: Optional[Path],
106 ambient_pa: float,
107 fluid_at: Any,
108 mode: str = "card",
109 card_center_pa: Optional[float] = None,
110) -> EngineLink:
111 """The live config as the twin's engine, in ``mode`` (see the module docstring).
112
113 ``fluid_at(side, pressure_pa) -> (rho, mu)`` is the twin's own propellant state at the
114 injector inlet, used where a Cd has to be recovered at the density the twin's leg sees.
115 ``card_center_pa`` centres the engine card somewhere other than this burn's T-0, so that
116 burns at several tank pressures share one card (the optimiser); the card's own envelope
117 check and the run's ``card_outside_steps`` say whether the burn stayed inside it.
118 """
119 from feedtwin.engine import CEATable, ConstantCStar
120 from feedtwin.engine.importer import engine_from_config
121
122 if mode not in MODES:
123 raise ValueError(f"engine mode {mode!r}; expected one of {', '.join(MODES)}")
124 raw = config.model_dump(mode="json")
125 design = engine_from_config(raw, name=str(getattr(config, "name", "") or "engine"))
126 notes: list = []
127
128 if cea_path is not None:
129 table: Any = CEATable(str(cea_path), expansion_ratio=design.expansion_ratio)
130 else:
131 table = ConstantCStar(cstar=1700.0)
132 notes.append("No CEA table found for this engine; the twin's chamber runs a constant c* of "
133 "1700 m/s and reports no thrust coefficient.")
134
135 sampler = EngineSampler(config, ambient_pa)
136 reference = _reference(sampler, tank_pa_O, tank_pa_F)
137 native_cd = {
138 side: getattr(design, side).cd_at(reference[m], *fluid_at(side, reference[p]), pressure=reference[p])
139 for side, m, p in (("oxidiser", "mdot_O", "p_O"), ("fuel", "mdot_F", "p_F"))
140 }
141
142 if mode == "native":
143 chamber = _chamber(design, table, ambient_pa, efficiency=design.cstar_efficiency,
144 nozzle_efficiency=design.nozzle_efficiency)
145 return EngineLink(design=design, chamber=chamber, mode="native", reference=reference,
146 calibration={"mode": "native", "native_cd_O": native_cd["oxidiser"],
147 "native_cd_F": native_cd["fuel"]},
148 sampler=sampler, notes=notes)
149
150 if mode == "card":
151 center = card_center_pa if card_center_pa is not None else 0.5 * (tank_pa_O + tank_pa_F)
152 card = card_for(config, center_pa=center, ambient_pa=ambient_pa)
153 design, chamber = card.install(design, ambient_pressure=ambient_pa)
154 fit = dict(card.fit)
155 calibration = {
156 "mode": "card",
157 "native_cd_O": native_cd["oxidiser"],
158 "native_cd_F": native_cd["fuel"],
159 "fit": fit,
160 "card": {k: v for k, v in card.provenance.items()},
161 "chamber_grid": [card.chamber.cstar.nx, card.chamber.cstar.ny],
162 "injector_grid": [card.oxidiser.capacity.nx, card.oxidiser.capacity.ny],
163 "of_range": [card.chamber.cstar.x0, card.chamber.cstar.x_max],
164 "mdot_range": [card.chamber.cstar.y0, card.chamber.cstar.y_max],
165 }
166 if not card.provenance.get("within_tolerance", False):
167 notes.append(f"The engine card's error against held-out EngineDesign solves is "
168 f"{fit.get('envelope_worst', float('nan')) * 100:.3f} %, over its "
169 f"{fit.get('tolerance', 0) * 100:.1f} % tolerance.")
170 return EngineLink(design=design, chamber=chamber, mode="card", reference=reference,
171 calibration=calibration, card=card, sampler=sampler, notes=notes)
172
173 # ---- calibrated: one constant Cd per side at the twin's own density, line exit to chamber ----
174 cds: Dict[str, float] = {}
175 rhos: Dict[str, float] = {}
176 for side, m_key, p_key, dp_key in (("oxidiser", "mdot_O", "p_O", "dp_line_O"),
177 ("fuel", "mdot_F", "p_F", "dp_line_F")):
178 injector = getattr(design, side)
179 mdot, p_line, dp = reference[m_key], reference[p_key], reference[dp_key]
180 if not (math.isfinite(dp) and dp > 0.0 and injector.area > 0.0):
181 raise ValueError(f"EngineDesign reported no usable {side} pressure drop at T-0 (dp = {dp})")
182 rho = float(fluid_at(side, p_line)[0])
183 rhos[side] = rho
184 cds[side] = mdot / (injector.area * math.sqrt(2.0 * rho * dp))
185 design = replace(design,
186 oxidiser=replace(design.oxidiser, discharge=_constant_cd(cds["oxidiser"])),
187 fuel=replace(design.fuel, discharge=_constant_cd(cds["fuel"])))
188
189 pc, mo, mf, thrust = reference["Pc"], reference["mdot_O"], reference["mdot_F"], reference["F"]
190 eta_c, eta_n = 1.0, 1.0
191 for _ in range(8):
192 got = _chamber(design, table, ambient_pa, efficiency=eta_c, nozzle_efficiency=eta_n).evaluate(mo, mf)
193 if got.pressure <= 0.0:
194 break
195 eta_c *= pc / got.pressure
196 if got.thrust > 0.0:
197 eta_n *= thrust / got.thrust
198 chamber = _chamber(design, table, ambient_pa, efficiency=eta_c, nozzle_efficiency=eta_n)
199 check = chamber.evaluate(mo, mf)
200 residual_pc = (check.pressure - pc) / pc
201 residual_f = (check.thrust - thrust) / thrust if thrust else float("nan")
202 calibration = {
203 "mode": "calibrated",
204 "at": "T-0 line-exit pressures",
205 "tank_psia_O": tank_pa_O / PSI,
206 "tank_psia_F": tank_pa_F / PSI,
207 "cd_O": cds["oxidiser"],
208 "cd_F": cds["fuel"],
209 "rho_O": rhos["oxidiser"],
210 "rho_F": rhos["fuel"],
211 "eta_cstar": eta_c,
212 "eta_n": eta_n,
213 "ed_eta_cstar": reference.get("eta_cstar", float("nan")),
214 "residual_pc": residual_pc,
215 "residual_thrust": residual_f,
216 "native_cd_O": native_cd["oxidiser"],
217 "native_cd_F": native_cd["fuel"],
218 }
219 if abs(residual_pc) > 1e-4 or (math.isfinite(residual_f) and abs(residual_f) > 1e-4):
220 notes.append(f"Chamber calibration left a residual of {residual_pc:+.2e} in Pc and "
221 f"{residual_f:+.2e} in thrust at T-0.")
222 return EngineLink(design=design, chamber=chamber, mode="calibrated", reference=reference,
223 calibration=calibration, sampler=sampler, notes=notes)
224
225
226def twin_injector_check(link: EngineLink, fluid_at: Any) -> Dict[str, Tuple[float, float]]:
227 """At EngineDesign's T-0 flows, the twin's line-exit-to-chamber pressure drop per side
228 against EngineDesign's: ``side -> (twin_dp, ed_dp)`` [Pa]. Equal by construction when
229 calibrated, equal to the card's error with a card, the gap they close when native."""
230 out: Dict[str, Tuple[float, float]] = {}
231 ref = link.reference
232 for side, m_key, p_key, dp_key in (("oxidiser", "mdot_O", "p_O", "dp_line_O"),
233 ("fuel", "mdot_F", "p_F", "dp_line_F")):
234 injector = getattr(link.design, side)
235 if injector.card is not None:
236 twin_dp = injector.card.pressure_drop(ref[m_key], ref[p_key])
237 else:
238 rho, mu = fluid_at(side, ref[p_key])
239 cd = injector.cd_at(ref[m_key], rho, mu, pressure=ref[p_key])
240 twin_dp = ref[m_key] ** 2 / (2.0 * rho * (cd * injector.area) ** 2)
241 out[side] = (twin_dp, ref[dp_key])
242 return out
#define min(a, b)
Definition Arduino.h:42
#define max(a, b)
Definition Arduino.h:43