92def _streams(eta_ox: float, eta_fu: float, tau_ox: float, tau_fu: float):
93 """L17 rig as two ChugStreams. The rig decouples the chamber from the feed lines (L17 fig. 3):
94 the injector cavities hold constant pressure, so there is no line inertance or resistance to
95 model — which is exactly why it is a clean test of the chamber+injector loop."""
97 o =
chug.ChugStream(
"O", mdot=MDOT_OX, eta_inj=eta_ox, Pc=PC, dP_feed=0.0,
98 feed_length=0.0, feed_area=A_C, tau_conv=tau_ox, regulator=reg)
99 f =
chug.ChugStream(
"F", mdot=MDOT_FU, eta_inj=eta_fu, Pc=PC, dP_feed=0.0,
100 feed_length=0.0, feed_area=A_C, tau_conv=tau_fu, regulator=reg)
114 print(
"A. chug.py vs the L17 / ref.[25] experiment (lags taken FROM the paper)")
117 print(f
" c* implied by Pc*A_t/mdot : {ch.cstar:8.1f} m/s (eta_c* = {ETA_CSTAR} rig)")
118 print(f
" theta_c = L*/(Gamma^2 c*) : {ch.theta_c()*1e3:8.3f} ms")
120 st =
_streams(DP_OX_LIMIT, DP_FU_FIXED, TAU_OX_PAPER, TAU_FU_PAPER)
121 fast = chug.chug_margin_fast(st, ch)
122 f_pred = fast[
"f_chug_hz"]
123 err = abs(f_pred - F_CHUG_EXPERIMENT) / F_CHUG_EXPERIMENT * 100.0
124 print(f
"\n chug frequency at the stability limit")
125 print(f
" measured (ref.[25]) : {F_CHUG_EXPERIMENT:8.1f} Hz")
126 print(f
" L17 constant-DTL model : {60.0:8.1f} Hz (+-5 Hz broadband resolution)")
127 print(f
" L17 variable-DTL model : {65.0:8.1f} Hz")
128 print(f
" STAR chug.py : {f_pred:8.1f} Hz ({err:5.1f} % from measured)")
129 ok_f = np.isfinite(f_pred)
and err < 35.0
132 print(f
"\n inherent stability limit, Delta_p_ox/pc at Delta_p_fu/pc = {DP_FU_FIXED}")
133 etas = np.linspace(0.15, 0.90, 151)
134 gms = [chug.chug_margin_fast(
_streams(e, DP_FU_FIXED, TAU_OX_PAPER, TAU_FU_PAPER), ch)[
"gain_margin"]
137 for i
in range(len(etas) - 1):
138 if (gms[i] - 1.0) * (gms[i + 1] - 1.0) < 0:
139 t = (1.0 - gms[i]) / (gms[i + 1] - gms[i])
140 cross = etas[i] + t * (etas[i + 1] - etas[i])
142 print(f
" measured / L17 : {DP_OX_LIMIT:8.2f}")
144 print(f
" STAR chug.py : no gain-margin crossing in 0.15..0.90")
147 print(f
" STAR chug.py : {cross:8.2f} "
148 f
"({abs(cross-DP_OX_LIMIT)/DP_OX_LIMIT*100:.0f} % from measured)")
149 ok_b = abs(cross - DP_OX_LIMIT) / DP_OX_LIMIT < 0.60
150 print(f
"\n -> {'PASS' if (ok_f and ok_b) else 'FAIL'}"
151 f
" (frequency {'ok' if ok_f else 'off'}, boundary {'ok' if ok_b else 'off'})")
174 print(
"B. tau_vap: each model vs the experiment-derived 4.4 ms (D0 = 83 um, L17 §3.2)")
176 rho_g = PC / (R_G * TC)
177 u_g = MDOT_TOT / (rho_g * A_C)
178 print(f
" chamber: Pc {PC/PA_PER_BAR:.1f} bar, Tc {TC:.0f} K, MR {MR}, "
179 f
"rho_g {rho_g:.2f} kg/m3, u_g {u_g:.1f} m/s")
185 for model, conv
in ((
"d2_law",
"none"),
186 (
"leonardi_dtl",
"none"),
187 (
"leonardi_dtl",
"leonardi_eq8")):
188 lags = timelag.compute_lags(streams, ch, model=model, mix_fraction=0.5,
190 rows.append((f
"{model}/{conv}" if model ==
"leonardi_dtl" else model,
191 k_g, lags[
"O"], lags[
"F"]))
193 print(f
"\n {'model':<28}{'k_g':>6}{'tau_vap':>11}{'err vs 4.4ms':>14}"
194 f
"{'tau_tot(O)':>12}{'err vs 6.6ms':>14}{'tau(F)':>9}")
196 for model, k_g, lo, lf
in rows:
197 e_v = (lo.tau_vap - TAU_VAP_PAPER) / TAU_VAP_PAPER * 100.0
198 e_t = (lo.tau_total - TAU_OX_PAPER) / TAU_OX_PAPER * 100.0
199 tag =
"-" if model.startswith(
"leonardi")
else f
"{k_g:.2f}"
200 print(f
" {model:<28}{tag:>6}{lo.tau_vap*1e3:>10.3f}m{e_v:>13.0f}%"
201 f
"{lo.tau_total*1e3:>11.3f}m{e_t:>13.0f}%{lf.tau_total*1e3:>8.3f}m")
202 best.setdefault(model, []).append(abs(e_v))
204 print(f
"\n L17's own stated tau_vap for this point: {TAU_VAP_PAPER*1e3:.1f} ms")
205 print(f
" gaseous fuel lag, measured (L17 §3.1) : {TAU_FU_PAPER*1e3:.1f} ms")
206 for name
in sorted(best):
207 print(f
" best-case |error| in tau_vap, {name:<28}: {min(best[name]):5.0f} %")
208 print(
" -> L17 eq. 9's constant in its QUIESCENT form is the only variant inside 20 % of")
209 print(
" the experiment-derived anchor, and it is the only one that does not depend on")
210 print(
" the hot-gas conductivity k_g (which alone moves d2_law by a factor of 2).")
213 _, _, _, lf = rows[0]
214 ok_gas = (lf.tau_atom == 0.0
and lf.tau_vap == 0.0
and lf.tau_total > 0.0)
215 print(f
" gas-phase stream carries tau_mix only: {'PASS' if ok_gas else 'FAIL'} "
216 f
"(atom {lf.tau_atom*1e3:.3f} ms, vap {lf.tau_vap*1e3:.3f} ms, "
217 f
"mix {lf.tau_mix*1e3:.3f} ms)")
228 print(
"C. L17 eq. 8 convective speed-up vs Ranz-Marshall")
230 rho_g = PC / (R_G * TC)
232 u_g = MDOT_TOT / (rho_g * A_C)
233 print(f
" {'Pc [bar]':>9}{'L17 1+1.5a':>13}{'Re_d':>10}{'Ranz-Marshall':>16}{'ratio':>9}")
235 for pc_bar
in (10.0, 44.8, 100.0, 200.0):
236 leo = timelag.convection_correction_leonardi(pc_bar)
237 rho = pc_bar * PA_PER_BAR / (R_G * TC)
238 u_slip = MDOT_TOT / (rho * A_C)
239 Re_d = rho * u_slip * D0_PAPER / mu_g
240 rm = timelag.ranz_marshall_correction(Re_d)
241 print(f
" {pc_bar:>9.1f}{leo:>13.3f}{Re_d:>10.1f}{rm:>16.3f}{leo/rm:>9.2f}")
242 if not (0.2 < leo / rm < 5.0):
244 print(
"\n L17 eq. 8 depends on pressure ONLY (no slip velocity, no drop size), so it cannot")
245 print(
" track Ranz-Marshall across conditions; it falls as Pc rises while the physical")
246 print(
" correction rises. Both agree to within a factor of ~2 near the paper's own 44.8 bar,")
247 print(
" which is where it was calibrated.")
248 print(f
" -> {'PASS' if ok else 'FAIL'} (same order of magnitude over 10-200 bar)")
259 print(
"D. Blast radius: what each model does to a STAR-class engine")
263 (
"ethalox LOX/ethanol", dict(
264 Pc=2.4e6, Tc=3094.0, MR=1.71, R_g=389.0, gamma=1.14,
265 ox=dict(name=
"LOX", rho=1140.0, mu=1.8e-4, sigma=0.013, Tb=90.19, Tc_=154.58,
266 hfg=213000.0, D0=80e-6, u=30.0, d=0.9e-3),
267 fu=dict(name=
"Ethanol", rho=789.0, mu=1.2e-3, sigma=0.0223, Tb=351.4, Tc_=514.0,
268 hfg=838000.0, D0=60e-6, u=25.0, d=0.7e-3))),
269 (
"methalox LOX/CH4", dict(
270 Pc=2.4e6, Tc=3500.0, MR=2.8, R_g=360.0, gamma=1.18,
271 ox=dict(name=
"LOX", rho=1140.0, mu=1.8e-4, sigma=0.013, Tb=90.19, Tc_=154.58,
272 hfg=213000.0, D0=80e-6, u=30.0, d=0.9e-3),
273 fu=dict(name=
"Methane", rho=422.6, mu=1.1e-4, sigma=0.013, Tb=111.65, Tc_=190.56,
274 hfg=510000.0, D0=60e-6, u=35.0, d=0.7e-3))),
276 A_c_star = math.pi * (0.10 / 2) ** 2
277 print(f
" {'case':<22}{'stream':<9}{'d2_law':>10}{'leonardi':>11}{'ratio':>8}")
278 for label, c
in cases:
279 rho_g = c[
"Pc"] / (c[
"R_g"] * c[
"Tc"])
280 cp_g = c[
"gamma"] * c[
"R_g"] / (c[
"gamma"] - 1.0)
281 u_g = 20.0 / (rho_g * A_c_star)
283 u_g=u_g, k_g=0.20, cp_g=cp_g)
285 for key, d
in ((
"O", c[
"ox"]), (
"F", c[
"fu"])):
287 name=d[
"name"], phase=
"liquid", rho_l=d[
"rho"], mu_l=d[
"mu"], sigma_l=d[
"sigma"],
288 T_boil=d[
"Tb"], T_crit=d[
"Tc_"], h_fg=d[
"hfg"], D0=d[
"D0"],
289 u_inj=d[
"u"], d_orifice=d[
"d"])
290 a = timelag.compute_lags(streams, ch, model=
"d2_law", mix_fraction=0.0)
291 b = timelag.compute_lags(streams, ch, model=
"leonardi_dtl", mix_fraction=0.5)
292 for key
in (
"O",
"F"):
293 r = b[key].tau_total / a[key].tau_total
if a[key].tau_total > 0
else float(
"nan")
294 print(f
" {label if key=='O' else '':<22}{streams[key].name:<9}"
295 f
"{a[key].tau_total*1e3:>9.3f}m{b[key].tau_total*1e3:>10.3f}m{r:>8.2f}")
296 print(f
" {'':<22}{' (leonardi split O: atom ' + f'{b[chr(79)].tau_atom*1e3:.3f}':<9}"
297 f
" vap {b['O'].tau_vap*1e3:.3f} mix {b['O'].tau_mix*1e3:.3f} ms)")
306 """(Delta_p_ox/pc at the gain-margin crossing, chug frequency there)."""
308 etas = np.linspace(0.10, 0.95, 341)
309 gms = [chug.chug_margin_fast(
_streams(e, DP_FU_FIXED, tau_ox, tau_fu), ch)[
"gain_margin"]
312 for i
in range(len(etas) - 1):
313 if (gms[i] - 1.0) * (gms[i + 1] - 1.0) < 0:
314 t = (1.0 - gms[i]) / (gms[i + 1] - gms[i])
315 cross = etas[i] + t * (etas[i + 1] - etas[i])
317 e = cross
if cross
is not None else DP_OX_LIMIT
318 f = chug.chug_margin_fast(
_streams(e, DP_FU_FIXED, tau_ox, tau_fu), ch)[
"f_chug_hz"]
325 print(
"E. THE DECIDER — each model's own lags, end-to-end, vs what ref.[25] measured")
327 rho_g = PC / (R_G * TC)
328 u_g = MDOT_TOT / (rho_g * A_C)
332 trials = [(
"L17's own stated lags (reference)", TAU_OX_PAPER, TAU_FU_PAPER,
None)]
333 for conv
in (
"none",
"leonardi_eq8",
"ranz_marshall"):
336 lag = timelag.compute_lags(streams, ch, model=
"leonardi_dtl",
337 mix_fraction=MIX, convection=conv)
338 trials.append((f
"leonardi_dtl convection={conv}",
339 lag[
"O"].tau_total, lag[
"F"].tau_total,
"leonardi_dtl"))
342 lag = timelag.compute_lags(streams, ch, model=
"d2_law", mix_fraction=MIX)
343 trials.append((f
"d2_law + mix k_g={k_g:.2f} W/mK",
344 lag[
"O"].tau_total, lag[
"F"].tau_total,
"d2_law"))
346 lag = timelag.compute_lags(streams, ch, model=
"d2_law", mix_fraction=0.0)
347 trials.append((
"d2_law, no mix (STAR before this change)",
348 lag[
"O"].tau_total, lag[
"F"].tau_total,
"d2_law_old"))
350 print(f
"\n {'lags from':<40}{'tau_O':>8}{'tau_F':>8}{'f':>8}{'f err':>8}"
351 f
"{'bnd':>7}{'bnd err':>9}{'score':>7}")
352 scores: Dict[str, float] = {}
353 for label, t_o, t_f, key
in trials:
355 fe = abs(f - F_CHUG_EXPERIMENT) / F_CHUG_EXPERIMENT * 100.0
356 be = abs(c - DP_OX_LIMIT) / DP_OX_LIMIT * 100.0
if c
else float(
"nan")
357 print(f
" {label:<40}{t_o*1e3:>7.2f}m{t_f*1e3:>7.2f}m{f:>8.1f}"
358 f
"{(f-F_CHUG_EXPERIMENT)/F_CHUG_EXPERIMENT*100:>7.0f}%{c if c else float('nan'):>7.2f}"
359 f
"{(c-DP_OX_LIMIT)/DP_OX_LIMIT*100 if c else float('nan'):>8.0f}%{fe+be:>7.0f}")
361 scores[label] = fe + be
363 winner =
min(scores, key=scores.get)
364 print(f
"\n score = |frequency error| + |boundary error|, in percent. Lower is better.")
365 print(f
" WINNER: {winner} (score {scores[winner]:.0f})")
366 d2 = [v
for k, v
in scores.items()
if k.startswith(
"d2_law + mix")]
367 print(f
" d2_law spread across the k_g band alone: {min(d2):.0f} to {max(d2):.0f}")
368 print(f
" leonardi_dtl convection=none has NO k_g dependence — the hot-gas conductivity")
369 print(f
" drops out of the lag entirely, and it is a property nobody on this program measures.")
370 ok = winner.startswith(
"leonardi_dtl convection=none")
371 print(f
"\n -> {'PASS' if ok else 'FAIL'}: shipping default should be "
372 f
"time_lag_model=leonardi_dtl, convection=none")
398 """(GM, f [Hz]) of the one-stream loop above, by bisection on the phase equation."""
399 lo, hi = 1e-6, 2.0 * math.pi * 1e5
400 phase =
lambda w: w * tau + math.atan(w * theta) + math.atan(w * I / Z) - math.pi
402 mid = 0.5 * (lo + hi)
408 return math.sqrt(1.0 + (w * theta) ** 2) * math.sqrt(Z * Z + (w * I) ** 2) / K_c, w / (2.0 * math.pi)
413 print(
"F. feed-line inertance: chug.py vs the closed-form one-stream boundary")
417 mdot, eta, pc, tau = 1.21, 0.35, 2.69e6, 17.0e-3
418 ch =
chug.ChugChamber(cstar=1567.0, A_t=1.795e-3, Lstar=1.359, gamma=1.13, R_gas=372.0, T_c=3205.0)
419 area = math.pi * 0.01092 ** 2 / 4.0
421 print(f
" K_c {ch.K_c():.4g} Pa.s/kg theta {ch.theta_c() * 1e3:.3f} ms tau {tau * 1e3:.1f} ms")
422 print(f
" {'I [1/m]':>10} {'GM code':>9} {'GM exact':>9} {'err':>8} {'f code':>8} {'f exact':>8}")
423 for I
in (0.0, 1495.0, 10297.0, 30000.0):
424 st =
chug.ChugStream(
"F", mdot=mdot, eta_inj=eta, Pc=pc, dP_feed=1.0e5, feed_length=I * area,
425 feed_area=area, tau_conv=tau, regulator=
chug.Regulator(enabled=
False))
426 Z = st.resistance() + 1.0 / st.G_inj()
428 fast = chug.chug_margin_fast([st], ch)
429 err = fast[
"gain_margin"] / gm_x - 1.0
430 ok &= abs(err) < 5e-3
and abs(fast[
"f_chug_hz"] / f_x - 1.0) < 1e-2
431 print(f
" {I:10.0f} {fast['gain_margin']:9.4f} {gm_x:9.4f} {err * 100:7.3f}% {fast['f_chug_hz']:8.2f} {f_x:8.2f}")
433 for f0
in (15.0, 25.0, 40.0):
434 w0, I = 2.0 * math.pi * f0, 10297.0
435 st =
chug.ChugStream(
"F", mdot=mdot, eta_inj=eta, Pc=pc, dP_feed=1.0e5, feed_length=I * area,
436 feed_area=area, tau_conv=0.0, regulator=
chug.Regulator(enabled=
False))
437 Z = st.resistance() + 1.0 / st.G_inj()
439 tau_b = (math.pi - math.atan(w0 * th) - math.atan(w0 * I / Z)) / w0
440 K_b = math.sqrt(1.0 + (w0 * th) ** 2) * math.sqrt(Z * Z + (w0 * I) ** 2)
442 chb =
chug.ChugChamber(cstar=K_b * ch.A_t, A_t=ch.A_t, Lstar=ch.Lstar, gamma=ch.gamma,
443 R_gas=ch.R_gas, T_c=ch.T_c * (K_b * ch.A_t / ch.cstar))
444 fast = chug.chug_margin_fast([st], chb)
445 okb = abs(fast[
"gain_margin"] - 1.0) < 5e-3
and abs(fast[
"f_chug_hz"] / f0 - 1.0) < 1e-2
447 print(f
" boundary at {f0:4.0f} Hz (tau* {tau_b * 1e3:6.2f} ms): GM {fast['gain_margin']:.4f} at "
448 f
"{fast['f_chug_hz']:.2f} Hz {'ok' if okb else 'OFF'}")
449 print(f
"\n -> {'PASS' if ok else 'FAIL'}")