105 cea_path: Optional[Path],
109 card_center_pa: Optional[float] =
None,
111 """The live config as the twin's engine, in ``mode`` (see the module docstring).
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.
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"))
128 if cea_path
is not None:
129 table: Any = CEATable(str(cea_path), expansion_ratio=design.expansion_ratio)
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.")
136 reference =
_reference(sampler, tank_pa_O, tank_pa_F)
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"))
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)
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)
157 "native_cd_O": native_cd[
"oxidiser"],
158 "native_cd_F": native_cd[
"fuel"],
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],
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)
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])
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"])))
189 pc, mo, mf, thrust = reference[
"Pc"], reference[
"mdot_O"], reference[
"mdot_F"], reference[
"F"]
190 eta_c, eta_n = 1.0, 1.0
192 got =
_chamber(design, table, ambient_pa, efficiency=eta_c, nozzle_efficiency=eta_n).evaluate(mo, mf)
193 if got.pressure <= 0.0:
195 eta_c *= pc / got.pressure
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")
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"],
209 "rho_O": rhos[
"oxidiser"],
210 "rho_F": rhos[
"fuel"],
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"],
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)
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]] = {}
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])
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])