289 throat_comp: Optional[Dict[str, float]]) -> Dict[str, Any]:
290 """Gas-side load functions q_in(T_s) per wall station, frozen over the next interval."""
291 abl = self.
config.ablative_cooling
292 gr = getattr(self.
config,
"graphite_insert",
None)
293 loads: Dict[str, Any] = {}
294 for name, w
in (self.
_walls or {}).items():
296 Ts_now = float(model.T[0])
297 eps_w = (gr.emissivity
or 0.8)
if w[
"kind"] ==
"graphite" else abl.surface_emissivity
298 st = gas_side.station_flux(gas, contour, w[
"x"], Ts_now, eps_w)
300 h_ref = st[
"h"] / gas_side.bartz_sigma(Ts_now, gas.T0, gas.gamma, M)
301 Taw, Tg, eg, ag = st[
"Taw"], st[
"T_static"], st[
"eps_gas"], st[
"alpha_gas"]
302 F_rad = eps_w / (1.0 - (1.0 - eps_w) * (1.0 - ag))
303 sig = STEFAN_BOLTZMANN_W_M2_K4
305 def h_of(s, h_ref=h_ref, M=M):
306 return h_ref * gas_side.bartz_sigma(s, gas.T0, gas.gamma, M)
308 def q_rad(s, F_rad=F_rad, eg=eg, ag=ag, Tg=Tg):
309 return F_rad * sig * (eg * Tg ** 4 - ag * s ** 4)
311 if w[
"kind"] ==
"graphite":
314 def chem(s, h_of=h_of, P_t=P_t):
315 return carbon_oxidation(s, P_t, throat_comp, throat_comp[
"MW"], h_of(s) / gas.cp, gr)
317 def q_in(s, h_of=h_of, Taw=Taw, q_rad=q_rad, chem=chem):
319 return ox[
"blowing_factor"] * h_of(s) * (Taw - s) + q_rad(s) - ox[
"q_chem"]
321 loads[name] = {
"q_in": q_in,
"chem": chem,
"h_of": h_of,
"Taw": Taw,
"q_rad": q_rad}
323 def q_in(s, h_of=h_of, Taw=Taw, q_rad=q_rad):
324 return blow * h_of(s) * (Taw - s) + q_rad(s)
326 loads[name] = {
"q_in": q_in,
"h_of": h_of,
"Taw": Taw,
"q_rad": q_rad}
391 previous_state: Optional[TimeVaryingState] =
None,
392 ) -> TimeVaryingState:
394 Solve one time step (explicit in the wall, exact in the chamber):
396 1. advance the wall conduction over [t - dt, t] under the previous state's gas loads;
397 2. rebuild the geometry from the cumulative recession;
398 3. solve Pc, and compute thrust, on that same geometry;
399 4. evaluate the gas-side loads at this state for the next interval.
401 ``previous_state`` None starts the walls cold (ambient) at the initial geometry.
403 if previous_state
is None or self.
_walls is None:
409 A_throat = geo[
"A_throat"]
410 A_exit = geo[
"A_exit"]
411 V_chamber = geo[
"V_chamber"]
412 eps = A_exit / A_throat
413 Lstar = V_chamber / A_throat
415 config_current = copy.deepcopy(self.
config)
416 cg = ensure_chamber_geometry(config_current)
417 cg.volume = V_chamber
418 cg.A_throat = A_throat
420 cg.expansion_ratio = eps
422 cg.chamber_diameter = geo[
"D_chamber"]
425 Pc, diagnostics = solver.solve(P_tank_O, P_tank_F, Pc_guess=
None)
427 MR = diagnostics[
"MR"]
428 mdot_total = diagnostics[
"mdot_total"]
429 Tc = diagnostics[
"Tc"]
430 gamma_chamber = diagnostics[
"gamma"]
431 R_chamber = diagnostics[
"R"]
432 cstar_actual = diagnostics[
"cstar_actual"]
433 cstar_ideal = diagnostics[
"cstar_ideal"]
434 eta_cstar = diagnostics[
"eta_cstar"]
437 chamber_intrinsics = calculate_chamber_intrinsics(
438 Pc=Pc, Tc=Tc, mdot_total=mdot_total, gamma=gamma_chamber, R=R_chamber,
439 V_chamber=V_chamber, A_throat=A_throat, Lstar=Lstar, MR=MR, P_back=self.
P_ambient,
441 mach_number = chamber_intrinsics[
"mach_number"]
444 reaction_progress_dict = calculate_chamber_reaction_progress(
445 Lstar, Pc, diagnostics[
"Tc_ideal"], cstar_ideal, gamma_chamber, R_chamber, MR,
446 self.
config, spray_diagnostics=diagnostics.get(
"spray_diagnostics"), Tc_kinetics=Tc,
450 thrust_results = calculate_thrust(Pc, MR, mdot_total, self.
cea_cache, config_current,
451 self.
P_ambient, reaction_progress=reaction_progress_dict)
455 cool = diagnostics.get(
"cooling", {})
or {}
456 abl_diag = cool.get(
"ablative", {})
or {}
457 blow = float(abl_diag.get(
"blowing_reduction", 1.0))
458 tr = self.
cea_cache.aux.transport(MR, Pc,
"chamber")
459 comp_c = self.
cea_cache.aux.composition(MR, Pc,
"chamber")
460 comp_t = self.
cea_cache.aux.composition(MR, Pc,
"throat")
461 gas =
gas_side.HotGasState(T0=float(diagnostics[
"Tc_ideal"]), P0=float(Pc), gamma=float(gamma_chamber),
462 mass_flux_throat=float(mdot_total) / A_throat, mu=tr[
"mu"],
463 cp=tr[
"cp"], Pr=tr[
"Pr"], x_H2O=comp_c[
"H2O"], x_CO2=comp_c[
"CO2"])
464 contour_now = gas_side.contour_for(cg)
470 abl_diag[
"recession_rate_quasi_steady"] = abl_diag.get(
"recession_rate")
471 abl_diag[
"recession_rate"] = report[
"rate_barrel"]
472 gr_cfg = getattr(self.
config,
"graphite_insert",
None)
473 if gr_cfg
is not None and gr_cfg.enabled:
474 cool[
"graphite"] = {
"enabled":
True,
"oxidation_rate": report[
"rate_throat"],
475 "recession_rate_thermal": 0.0,
476 "surface_temperature": report[
"T_graphite_surface"],
477 "q_chemical": report[
"q_chem_throat"]}
478 diagnostics[
"cooling"] = cool
480 chugging = calculate_chugging_frequency(V_chamber, A_throat, cstar_actual, gamma_chamber, Pc,
482 chugging_freq = chugging[
"frequency"]
483 stability_margin = float(
"nan")
484 acoustic = calculate_acoustic_modes(self.
L_chamber, geo[
"D_chamber"], Tc, gamma_chamber, R_chamber)
485 feed_stability = {
"pogo_frequency": np.nan,
"surge_frequency": np.nan,
"stability_margin": np.nan}
486 comprehensive_stability =
None
494 "mdot_O": mdot_total * MR / (1.0 + MR),
495 "mdot_F": mdot_total / (1.0 + MR),
496 "P_tank_O": P_tank_O,
497 "P_tank_F": P_tank_F,
502 comprehensive_stability = comprehensive_stability_analysis(
503 config=stability_config, Pc=Pc, MR=MR, mdot_total=mdot_total, cstar=cstar_actual,
504 gamma=gamma_chamber, R=R_chamber, Tc=Tc, diagnostics=stability_diag,
506 stability_margin = comprehensive_stability.get(
"chugging", {}).get(
"stability_margin", stability_margin)
507 chugging_freq = comprehensive_stability.get(
"chugging", {}).get(
"frequency", chugging_freq)
508 acoustic = comprehensive_stability.get(
"acoustic", acoustic)
509 feed_stability = comprehensive_stability.get(
"feed_system", feed_stability)
510 except Exception
as e:
512 warnings.warn(f
"Comprehensive stability analysis failed: {e}")
513 comprehensive_stability =
None
515 gr_thick = (self.
_walls[
"throat"][
"model"].thickness_first
516 if (self.
_walls and "throat" in self.
_walls and self.
_walls[
"throat"][
"kind"] ==
"graphite")
524 D_chamber=geo[
"D_chamber"],
525 D_throat=geo[
"D_throat"],
526 D_exit=geo[
"D_exit"],
528 recession_chamber=geo[
"recession_chamber"],
529 recession_throat=geo[
"recession_throat"],
530 recession_exit=geo[
"recession_exit"],
531 recession_graphite=geo[
"recession_throat"]
if (gr_cfg
is not None and gr_cfg.enabled)
else 0.0,
532 graphite_thickness_remaining=gr_thick,
534 "progress_injection": reaction_progress_dict[
"progress_injection"],
535 "progress_mid": reaction_progress_dict[
"progress_mid"],
536 "progress_throat": reaction_progress_dict[
"progress_throat"],
538 tau_residence=reaction_progress_dict[
"tau_residence"],
539 tau_effective=reaction_progress_dict[
"tau_effective"],
543 mdot_total=mdot_total,
544 F=thrust_results[
"F"],
545 Isp=thrust_results[
"Isp"],
546 v_exit=thrust_results[
"v_exit"],
547 P_exit=thrust_results[
"P_exit"],
548 T_exit=thrust_results[
"T_exit"],
549 M_exit=thrust_results[
"M_exit"],
550 gamma_chamber=gamma_chamber,
551 gamma_exit=thrust_results[
"gamma_exit"],
553 R_exit=thrust_results[
"R_exit"],
554 equilibrium_factor=thrust_results[
"equilibrium_factor"],
555 chugging_frequency=chugging_freq,
556 chugging_stability_margin=stability_margin,
557 stability_state=comprehensive_stability.get(
"stability_state",
"unstable")
if comprehensive_stability
else "unstable",
558 stability_score=comprehensive_stability.get(
"stability_score", 0.0)
if comprehensive_stability
else 0.0,
559 acoustic_modes=acoustic,
560 feed_stability=feed_stability,
561 heat_flux_chamber=report[
"q_conv_chamber"] + report[
"q_rad_chamber"],
562 heat_flux_throat=report[
"q_conv_throat"] + report[
"q_rad_throat"],
563 ablative_recession_rate=report[
"rate_barrel"],
564 graphite_recession_rate=report[
"rate_throat"]
if (gr_cfg
is not None and gr_cfg.enabled)
else 0.0,
565 throat_oxidation_recession_rate=report[
"rate_throat"]
if (gr_cfg
is not None and gr_cfg.enabled)
else 0.0,
566 throat_ablation_recession_rate=0.0,
567 mach_number=mach_number,
569 reynolds_number=chamber_intrinsics.get(
"reynolds_number", float(
"nan")),
570 residence_time=chamber_intrinsics.get(
"residence_time", float(
"nan")),
571 T_liner_surface=report[
"T_liner_surface"],
572 char_depth_chamber=report[
"char_depth_chamber"],
573 T_bondline=report[
"T_bondline"],
574 recession_liner_peak=report[
"recession_liner_peak"],
575 char_depth_peak=report[
"char_depth_peak"],
576 T_bondline_peak=report[
"T_bondline_peak"],
577 x_liner_peak=report[
"x_liner_peak"],
578 q_conv_chamber=report[
"q_conv_chamber"],
579 q_rad_chamber=report[
"q_rad_chamber"],
580 q_conv_throat=report[
"q_conv_throat"],
581 q_rad_throat=report[
"q_rad_throat"],
582 q_chem_throat=report[
"q_chem_throat"],
583 T_graphite_surface=report[
"T_graphite_surface"],
584 T_graphite_back=report[
"T_graphite_back"],
585 cstar_ideal=cstar_ideal,
586 cstar_actual=cstar_actual,
587 diagnostics=diagnostics,
722 first_step: float = 1.0e-2, growth: float = 1.05,
723 steps_per_duration: int = 1200, peak_resolution_K: float = 0.05) -> Dict[str, Any]:
724 """Soak-back after shutdown, with the time each peak is reached.
726 Each station's wall is copied and continued with its hot face adiabatic (no gas, no
727 re-radiation: an upper bound on what reaches the back) and its back face adiabatic as
728 always (wall_conduction.py: no backing is modelled behind the last layer). Backward Euler
729 on the same grid, with a step that grows geometrically from ``first_step`` to
730 ``duration / steps_per_duration``: the stack is fully implicit, so the long steps are
731 stable, and the short ones resolve the first seconds when a thin insert equilibrates.
732 Backward Euler slows a mode of rate lambda by ~lambda*h/2; at the default 1200 steps over
733 3 L^2/alpha the slowest mode (lambda = pi^2 alpha/L^2) has lambda*h = 0.025, so its clock
734 runs ~1 % slow. With constant properties the stored heat, and so the soaked peak, is
735 conserved exactly (both faces adiabatic); a cp(T) layer (graphite) has its capacity lagged
736 one step, as in the burn, so its energy is conserved to that lag.
738 ``soak_duration`` sizes the window from the slowest *single layer*. For a stack of layers
739 the slowest mode is slower than any one layer's: by eigen-decomposition of this grid, a
740 12 mm liner at the schema's default properties (k 0.35, rho 1600, cp 1500) on a 1/4 in
741 steel case (k 16, rho 8000, cp 500, typical 300-series) is 2.3x slower than the liner alone, and 3x the liner's L^2/alpha still leaves ~3e-6 of that mode; a 6 mm
742 graphite insert on the same case, 1.6x and ~2e-8. Inside the window for such stacks.
744 ``duration`` None is :meth:`soak_duration` (``factor`` x the longest L^2/alpha).
746 Per station: the peak back-face and first-interface temperatures and when they occur
747 (``t_*_peak_s``: the first time within ``peak_resolution_K`` of the peak, so a plateau
748 reached early is dated when it was reached, not by round-off along it), and
749 ``t_back_95_s``, when the back face had made 95 % of its rise. With both faces adiabatic
750 the back face rises monotonically to the stored-heat equilibrium; the 95 % time says when
754 duration = size[
"duration_s"]
755 duration = float(duration)
756 out: Dict[str, Any] = {
"duration_s": duration,
"sizing": size,
"stations": {}}
759 h_max =
max(duration /
max(int(steps_per_duration), 1), first_step)
760 for name, w
in (self.
_walls or {}).items():
761 m = copy.deepcopy(w[
"model"])
764 back = [float(m.T_back)]
765 bond = [float(m.interface_temperature(0))]
766 h = float(first_step)
767 while t < duration - 1e-12:
768 step =
min(h, duration - t)
769 m.step(step, q_in=
lambda s: 0.0)
772 back.append(float(m.T_back))
773 bond.append(float(m.interface_temperature(0)))
774 h =
min(h * growth, h_max)
775 back_a, bond_a, t_a = np.asarray(back), np.asarray(bond), np.asarray(ts)
776 tol = float(peak_resolution_K)
777 ib = int(np.argmax(back_a >= float(np.max(back_a)) - tol))
778 ii = int(np.argmax(bond_a >= float(np.max(bond_a)) - tol))
779 rise = float(np.max(back_a)) - back_a[0]
780 t95 = float(t_a[int(np.argmax(back_a >= back_a[0] + 0.95 * rise))])
if rise > 0
else 0.0
781 out[
"stations"][name] = {
782 "x": float(w[
"x"]),
"kind": w[
"kind"],
783 "T_back_start": float(back_a[0]),
"T_back_peak": float(np.max(back_a)),
"t_back_peak_s": float(t_a[ib]),
785 "T_interface_start": float(bond_a[0]),
"T_interface_peak": float(np.max(bond_a)),
786 "t_interface_peak_s": float(t_a[ii]),
787 "T_surface_end": float(m.T[0]),
"steps": len(ts) - 1,
839 Convert state history to results dictionary (compatible with existing code).
844 Dictionary with arrays of all metrics
847 raise ValueError(
"No state history - run solve_time_series first")
856 "mdot_total": np.array([s.mdot_total
for s
in self.
state_history]),
863 "gamma_chamber": np.array([s.gamma_chamber
for s
in self.
state_history]),
864 "gamma_exit": np.array([s.gamma_exit
for s
in self.
state_history]),
865 "R_chamber": np.array([s.R_chamber
for s
in self.
state_history]),
867 "equilibrium_factor": np.array([s.equilibrium_factor
for s
in self.
state_history]),
869 "V_chamber": np.array([s.V_chamber
for s
in self.
state_history]),
870 "A_throat": np.array([s.A_throat
for s
in self.
state_history]),
872 "D_chamber": np.array([s.D_chamber
for s
in self.
state_history]),
873 "A_chamber": np.array([np.pi * (s.D_chamber / 2.0)**2
for s
in self.
state_history]),
874 "contraction_ratio": np.array([(np.pi * (s.D_chamber / 2.0)**2) / s.A_throat
if s.A_throat > 0
else 1.0
for s
in self.
state_history]),
876 "recession_chamber": np.array([s.recession_chamber
for s
in self.
state_history]),
877 "recession_throat": np.array([s.recession_throat
for s
in self.
state_history]),
878 "recession_exit": np.array([s.recession_exit
for s
in self.
state_history]),
879 "recession_graphite": np.array([s.recession_graphite
for s
in self.
state_history]),
880 "D_throat": np.array([s.D_throat
for s
in self.
state_history]),
882 "chugging_frequency": np.array([s.chugging_frequency
for s
in self.
state_history]),
883 "chugging_stability_margin": np.array([s.chugging_stability_margin
for s
in self.
state_history]),
884 "stability_state": np.array([s.stability_state
for s
in self.
state_history]),
885 "stability_score": np.array([s.stability_score
for s
in self.
state_history]),
886 "heat_flux_chamber": np.array([s.heat_flux_chamber
for s
in self.
state_history]),
887 "heat_flux_throat": np.array([s.heat_flux_throat
for s
in self.
state_history]),
888 "ablative_recession_rate": np.array([s.ablative_recession_rate
for s
in self.
state_history]),
889 "graphite_recession_rate": np.array([s.graphite_recession_rate
for s
in self.
state_history]),
890 "throat_oxidation_recession_rate": np.array([s.throat_oxidation_recession_rate
for s
in self.
state_history]),
891 "throat_ablation_recession_rate": np.array([s.throat_ablation_recession_rate
for s
in self.
state_history]),
893 "mach_number": np.array([s.mach_number
for s
in self.
state_history]),
894 "eta_cstar": np.array([s.eta_cstar
for s
in self.
state_history]),
895 "reynolds_number": np.array([s.reynolds_number
for s
in self.
state_history]),
896 "residence_time": np.array([s.residence_time
for s
in self.
state_history]),
897 "T_liner_surface": np.array([s.T_liner_surface
for s
in self.
state_history]),
898 "char_depth_chamber": np.array([s.char_depth_chamber
for s
in self.
state_history]),
899 "T_bondline": np.array([s.T_bondline
for s
in self.
state_history]),
900 "recession_liner_peak": np.array([s.recession_liner_peak
for s
in self.
state_history]),
901 "char_depth_peak": np.array([s.char_depth_peak
for s
in self.
state_history]),
902 "T_bondline_peak": np.array([s.T_bondline_peak
for s
in self.
state_history]),
903 "x_liner_peak": np.array([s.x_liner_peak
for s
in self.
state_history]),
904 "q_conv_chamber": np.array([s.q_conv_chamber
for s
in self.
state_history]),
905 "q_rad_chamber": np.array([s.q_rad_chamber
for s
in self.
state_history]),
906 "q_conv_throat": np.array([s.q_conv_throat
for s
in self.
state_history]),
907 "q_rad_throat": np.array([s.q_rad_throat
for s
in self.
state_history]),
908 "q_chem_throat": np.array([s.q_chem_throat
for s
in self.
state_history]),
909 "T_graphite_surface": np.array([s.T_graphite_surface
for s
in self.
state_history]),
910 "T_graphite_back": np.array([s.T_graphite_back
for s
in self.
state_history]),
911 "cstar_ideal": np.array([s.cstar_ideal
for s
in self.
state_history]),
912 "cstar_actual": np.array([s.cstar_actual
for s
in self.
state_history]),
916 results[
"reaction_progress_throat"] = np.array([s.reaction_progress[
"progress_throat"]
for s
in self.
state_history])
917 results[
"reaction_progress_mid"] = np.array([s.reaction_progress[
"progress_mid"]
for s
in self.
state_history])
918 results[
"reaction_progress_injection"] = np.array([s.reaction_progress[
"progress_injection"]
for s
in self.
state_history])
919 results[
"tau_residence"] = np.array([s.tau_residence
for s
in self.
state_history])
920 results[
"tau_effective"] = np.array([s.tau_effective
for s
in self.
state_history])
923 results[
"diagnostics"] = [s.diagnostics
for s
in self.
state_history]
926 results[
"stations"] = [s.stations
for s
in self.
state_history]