383 ) -> Tuple[float, float, Dict[str, Any]]:
389 discharge_O = effective_discharge(config,
"oxidizer")
390 discharge_F = effective_discharge(config,
"fuel")
391 feed_O = config.feed_system[
"oxidizer"]
392 feed_F = config.feed_system[
"fuel"]
393 spray_cfg = config.spray
394 fluids = config.fluids
396 rho_O = fluids[
"oxidizer"].density
397 mu_O = fluids[
"oxidizer"].viscosity
398 sigma_O = fluids[
"oxidizer"].surface_tension
400 rho_F = fluids[
"fuel"].density
401 mu_F = fluids[
"fuel"].viscosity
402 sigma_F = fluids[
"fuel"].surface_tension
405 T_tank_O = getattr(fluids[
"oxidizer"],
"temperature", 90.0)
406 T_tank_F = getattr(fluids[
"fuel"],
"temperature", 300.0)
408 A_O = geometry.oxidizer.n_elements * np.pi * (geometry.oxidizer.d_jet / 2.0) ** 2
409 A_F = geometry.fuel.n_elements * np.pi * (geometry.fuel.d_jet / 2.0) ** 2
410 d_hyd_O = geometry.oxidizer.d_jet
411 d_hyd_F = geometry.fuel.d_jet
416 if float(config.solver.closure.Cd_reduction_factor) != 1.0:
417 assume(
"solver.closure.Cd_reduction_factor", 1.0,
418 reason=
"Cd is orifice geometry and Re; a spray-constraint violation is reported, not traded for Cd")
419 for _side, _dc
in ((
"oxidizer", discharge_O), (
"fuel", discharge_F)):
420 if cd_inf_from_inlet_geometry(_dc)
is None and getattr(_dc,
"use_geometry_cd",
False):
421 assume(f
"discharge.{_side}.inlet_geometry",
"thin plate",
422 reason=
"no inlet declared: thin-plate Cd, not the drilled L/d passage the layout draws "
423 "(sharp, L/d 4: 0.79, Lichtarowicz 1965)")
424 Cd_O_eff = cd_inf_from_orifice_diameter(d_hyd_O, discharge_O)
425 Cd_F_eff = cd_inf_from_orifice_diameter(d_hyd_F, discharge_F)
429 "constraints_satisfied":
False,
439 "injector_type":
"impinging",
440 "turbulence_intensity_O":
None,
441 "turbulence_intensity_F":
None,
442 "turbulence_length_O":
None,
443 "turbulence_length_F":
None,
444 "turbulence_intensity_mix":
None,
449 _imp_sep_deg = float(geometry.oxidizer.impingement_angle + geometry.fuel.impingement_angle)
450 _imp_sep_deg = float(np.clip(_imp_sep_deg, 1.0, 179.0))
451 imp_angle_rad = np.deg2rad(_imp_sep_deg)
453 def _injector_turbulence_fields(uo: float, uf: float) -> Dict[str, float]:
454 """Same Reynolds-based turbulence model as pintle (shear-layer mixing intensity)."""
455 Re_Oloc = calculate_reynolds_number(rho_O, uo, d_hyd_O, mu_O)
456 Re_Floc = calculate_reynolds_number(rho_F, uf, d_hyd_F, mu_F)
457 ti_O = 0.16 * (Re_Oloc ** -0.125)
if Re_Oloc > 0
else 0.1
458 ti_F = 0.16 * (Re_Floc ** -0.125)
if Re_Floc > 0
else 0.1
459 ti_O = float(np.clip(ti_O, 0.02, 0.3))
460 ti_F = float(np.clip(ti_F, 0.02, 0.3))
461 tl_O = float(0.07 * d_hyd_O)
462 tl_F = float(0.07 * d_hyd_F)
463 velocity_total =
max(uo + uf, 1e-6)
464 ti_mix = (ti_O * uo + ti_F * uf) / velocity_total
465 ti_mix = float(np.clip(ti_mix, 0.02, 0.35))
467 "turbulence_intensity_O": ti_O,
468 "turbulence_intensity_F": ti_F,
469 "turbulence_length_O": tl_O,
470 "turbulence_length_F": tl_F,
471 "turbulence_intensity_mix": ti_mix,
474 def _converge_feed_orifice_coupling(
492 """Feed loss, inlet pressure and orifice flow at this Pc, each stream by ``_stream_flow``.
494 With Pc fixed the streams do not interact, so each is its own bracketed root. The relaxed
495 fixed point this replaces stalled at its 150-step cap whenever a probed Pc sat above a
496 tank, and stopped ~2e-6 short of the root otherwise.
500 mdot_o, mdot_f, cd_o, cd_f, Pi_o, Pi_f, dpf_o, dpf_f, dpi_o, dpi_f, evaluations
504 return delta_p_feed(m, rho_O, feed_O, P_tank_O, mu=mu_O)
507 dp = delta_p_feed(m, rho_F, feed_F, P_tank_F, mu=mu_F)
508 if config.regen_cooling
is not None and config.regen_cooling.enabled:
509 dp += delta_p_regen_channels(m, rho_F, mu_F, config.regen_cooling, P_tank_F)
513 Pc, P_tank_O, rho_O, A_O, d_hyd_O, mu_O, discharge_O, T_tank_O, cd_eff_o, _feed_o,
514 cav=cav_O, network=net_O, detail=net_detail[
"O"],
517 Pc, P_tank_F, rho_F, A_F, d_hyd_F, mu_F, discharge_F, T_tank_F, cd_eff_f, _feed_f,
518 cav=cav_F, network=net_F, detail=net_detail[
"F"],
522 mdot_bn_o = float(Cdo * A_O * np.sqrt(2.0 * rho_O * dpi_o))
if dpi_o > 0
else 0.0
523 mdot_bn_f = float(Cdf * A_F * np.sqrt(2.0 * rho_F * dpi_f))
if dpi_f > 0
else 0.0
526 "impinging feed-orifice closure: fp_iters=%d mdot_solver_O=%s mdot_from_Bernoulli_O=%s "
527 "mdot_solver_F=%s mdot_from_Bernoulli_F=%s",
534 if _PRINT_FEED_ORIFICE_CLOSURE:
536 "impinging feed-orifice closure",
538 f
"mdot_solver_O={mo}",
539 f
"mdot_from_Bernoulli_O={mdot_bn_o}",
540 f
"mdot_solver_F={mf}",
541 f
"mdot_from_Bernoulli_F={mdot_bn_f}",
544 return mo, mf, Cdo, Cdf, Pi_o, Pi_f, dpf_o, dpf_f, dpi_o, dpi_f, fp_it
550 pv = getattr(fluids[side],
"vapor_pressure",
None)
551 if pv
is None or not np.isfinite(float(pv)):
553 return float(pv), contraction_coefficient(inlet_radius_ratio_of(dc))
555 cav_O, cav_F = _cav(
"oxidizer", discharge_O), _cav(
"fuel", discharge_F)
558 geometry.oxidizer.n_elements, geometry.oxidizer.d_jet)
560 geometry.fuel.n_elements, geometry.fuel.d_jet)
561 net_detail = {
"O": {},
"F": {}}
563 feed_orifice_fp_last = 0
568 mdot_O, mdot_F, Cd_O, Cd_F, P_inj_O, P_inj_F, delta_p_feed_O, delta_p_feed_F, delta_p_inj_O, delta_p_inj_F, feed_orifice_fp_last = _converge_feed_orifice_coupling(
575 u_O = mdot_O / (rho_O * A_O)
if A_O > 0
else 0.0
576 u_F = mdot_F / (rho_F * A_F)
if A_F > 0
else 0.0
583 np.sqrt(u_O ** 2 + u_F ** 2 - 2 * u_O * u_F * np.cos(imp_angle_rad))
585 turb_fields = _injector_turbulence_fields(u_O, u_F)
592 max(Pc / (spray_cfg.smd.chamber_gas_R * spray_cfg.smd.chamber_gas_T), 1e-6)
595 J = momentum_flux_ratio(rho_O, u_O, rho_F, u_F)
596 MR = mdot_O / mdot_F
if mdot_F > 0
else np.inf
597 TMR = thrust_momentum_ratio(J, MR)
599 if spray_cfg.spray_angle.model ==
"J":
600 theta = spray_angle_from_J(J, spray_cfg.spray_angle.k, spray_cfg.spray_angle.n)
602 theta = spray_angle_from_TMR(TMR)
604 Oh_O = ohnesorge_number(mu_O, rho_O, sigma_O, geometry.oxidizer.d_jet)
605 Oh_F = ohnesorge_number(mu_F, rho_F, sigma_F, geometry.fuel.d_jet)
610 We_O = weber_number(rho_gas, u_rel, geometry.oxidizer.d_jet, sigma_O)
611 We_F = weber_number(rho_gas, u_rel, geometry.fuel.d_jet, sigma_F)
614 _prop = bool(getattr(_smd,
"smd_property_scaling",
True))
615 _scale = float(getattr(_smd,
"smd_scale", 1.0)
or 1.0)
617 _transfer = tn4222_transfer_model(_smd)
619 D32_O = (_scale * smd_impinging_tn4222(
620 geometry.oxidizer.d_jet, u_O, rho_O, mu_O, sigma_O, rho_gas,
621 scale_properties=_prop, transfer=_transfer)
if u_O > 0
else float(
"nan"))
622 D32_F = (_scale * smd_impinging_tn4222(
623 geometry.fuel.d_jet, u_F, rho_F, mu_F, sigma_F, rho_gas,
624 scale_properties=_prop, transfer=_transfer)
if u_F > 0
else float(
"nan"))
626 _mO, _mF = getattr(_smd,
"d32_measured_O",
None), getattr(_smd,
"d32_measured_F",
None)
635 _ev = spray_cfg.evaporation
636 _use_derived = getattr(_ev,
"model",
"derived") ==
"derived"
637 k_evap_O = k_evap_F = float(
"nan")
639 k_evap_O = evaporation_constant_m2_s(
640 Tc=spray_cfg.smd.chamber_gas_T, Pc=Pc, rho_g=rho_gas, rho_l=rho_O,
641 L_vap=float(getattr(fluids[
"oxidizer"],
"latent_heat", 0.0)
or 0.0),
642 T_boil=float(getattr(fluids[
"oxidizer"],
"boiling_point", 0.0)
or 0.0),
643 cp_g=float(getattr(_ev,
"cp_gas", 2200.0)),
644 C_evap=float(getattr(_ev,
"C_evap", 1.562)),
646 k_evap_F = evaporation_constant_m2_s(
647 Tc=spray_cfg.smd.chamber_gas_T, Pc=Pc, rho_g=rho_gas, rho_l=rho_F,
648 L_vap=float(getattr(fluids[
"fuel"],
"latent_heat", 0.0)
or 0.0),
649 T_boil=float(getattr(fluids[
"fuel"],
"boiling_point", 0.0)
or 0.0),
650 cp_g=float(getattr(_ev,
"cp_gas", 2200.0)),
651 C_evap=float(getattr(_ev,
"C_evap", 1.562)),
653 if np.isfinite(k_evap_O)
and np.isfinite(k_evap_F):
654 tau_evap_O = tau_evap_from_k(D32_O, k_evap_O)
655 tau_evap_F = tau_evap_from_k(D32_F, k_evap_F)
657 tau_evap_O = tau_evap(D32_O, _ev.K)
658 tau_evap_F = tau_evap(D32_F, _ev.K)
664 u_axial = spray_axial_velocity(
665 mdot_O=mdot_O, u_O=u_O, theta_O_deg=geometry.oxidizer.impingement_angle,
666 mdot_F=mdot_F, u_F=u_F, theta_F_deg=geometry.fuel.impingement_angle,
668 u_transport = u_axial
if np.isfinite(u_axial)
and u_axial > 0
else u_rel
669 _xs = [v
for v
in (xstar(u_transport, tau_evap_O), xstar(u_transport, tau_evap_F))
if np.isfinite(v)]
670 x_star =
max(_xs)
if _xs
else float(
"nan")
683 theta_O = float(np.deg2rad(geometry.oxidizer.impingement_angle))
684 theta_F = float(np.deg2rad(geometry.fuel.impingement_angle))
685 s_O = float(getattr(geometry.oxidizer,
"spacing", 0.0)
or 0.0)
686 s_F = float(getattr(geometry.fuel,
"spacing", 0.0)
or 0.0)
687 s_pair = 0.5 * (s_O + s_F)
688 tan_sum = float(np.tan(theta_O) + np.tan(theta_F))
690 0.5 * (geometry.oxidizer.n_elements + geometry.fuel.n_elements),
692 geometry.oxidizer.impingement_angle,
693 geometry.fuel.impingement_angle,
695 nO_geom =
max(1, int(geometry.oxidizer.n_elements))
696 nF_geom =
max(1, int(geometry.fuel.n_elements))
697 D_pitch_O = float(nO_geom * s_O / np.pi)
698 D_pitch_F = float(nF_geom * s_F / np.pi)
700 gap_O = float(s_O - geometry.oxidizer.d_jet)
701 gap_F = float(s_F - geometry.fuel.d_jet)
711 _d_avg = 0.5 * (float(geometry.oxidizer.d_jet) + float(geometry.fuel.d_jet))
712 _u_sheet =
max(float(u_rel), 1e-6)
713 _rho_l_avg = 0.5 * (float(rho_O) + float(rho_F))
720 if np.isfinite(L_imp)
and L_imp > 0
and _d_avg > 0
and rho_gas > 1e-4:
721 _h_sheet = (_d_avg ** 2) / (4.0 * L_imp)
722 L_b = float(_h_sheet * np.sqrt(_rho_l_avg / rho_gas))
723 L_b = float(np.clip(L_b, 0.0, 20.0 * _d_avg))
726 vaporization_length_total = float(
727 (L_imp
if np.isfinite(L_imp)
else 0.0) + L_b + x_star
730 constraints_ok, violations = check_spray_constraints(We_O, We_F, x_star, spray_cfg)
735 "constraints_satisfied": constraints_ok,
736 "violations": violations,
740 "u_rel": float(u_rel),
741 "rho_gas_breakup": float(rho_gas),
749 "u_axial_spray": float(u_transport),
750 "k_evap_O": float(k_evap_O),
751 "k_evap_F": float(k_evap_F),
752 "tau_evap_O": float(tau_evap_O),
753 "tau_evap_F": float(tau_evap_F),
755 "L_sheet_breakup": float(L_b),
756 "D_pitch_O": D_pitch_O,
757 "D_pitch_F": D_pitch_F,
758 "s_pair": float(s_pair),
759 "element_gap_O": gap_O,
760 "element_gap_F": gap_F,
761 "vaporization_length_total": vaporization_length_total,
767 "apply_tau_res_correction": bool(
768 getattr(spray_cfg.evaporation,
"apply_tau_res_correction",
False)
770 "impingement_angle_deg": np.rad2deg(imp_angle_rad),
771 "V_rel": float(u_rel),
772 "breakup_multiplier": 1.0,
773 "penetration_multiplier": 1.0,
782 delta_p_feed_O_final = float(delta_p_feed_O)
783 delta_p_feed_F_final = float(delta_p_feed_F)
784 P_inj_O_final = float(P_inj_O)
785 P_inj_F_final = float(P_inj_F)
786 delta_p_inj_O_final = float(delta_p_inj_O)
787 delta_p_inj_F_final = float(delta_p_inj_F)
789 mdot_from_bernoulli_O = (
790 float(Cd_O * A_O * np.sqrt(2.0 * rho_O * delta_p_inj_O_final))
791 if delta_p_inj_O_final > 0
794 mdot_from_bernoulli_F = (
795 float(Cd_F * A_F * np.sqrt(2.0 * rho_F * delta_p_inj_F_final))
796 if delta_p_inj_F_final > 0
800 u_O_final = mdot_O / (rho_O * A_O)
if A_O > 0
else 0.0
801 u_F_final = mdot_F / (rho_F * A_F)
if A_F > 0
else 0.0
808 - 2 * u_O_final * u_F_final * np.cos(imp_angle_rad)
811 turb_final = _injector_turbulence_fields(u_O_final, u_F_final)
815 n_O =
max(1, int(geometry.oxidizer.n_elements))
816 n_F =
max(1, int(geometry.fuel.n_elements))
817 djo = float(geometry.oxidizer.d_jet)
818 djf = float(geometry.fuel.d_jet)
819 A_jet_O = np.pi * (djo / 2.0) ** 2
820 A_jet_F = np.pi * (djf / 2.0) ** 2
821 denom_O = rho_O * float(n_O) * A_jet_O
822 denom_F = rho_F * float(n_F) * A_jet_F
823 v_O_bulk = mdot_O / denom_O
if denom_O > 0
else np.nan
824 v_F_bulk = mdot_F / denom_F
if denom_F > 0
else np.nan
826 float(rho_O), float(rho_F), float(v_O_bulk), float(v_F_bulk)
829 float(rho_O), float(v_O_bulk), djo, float(rho_F), float(v_F_bulk), djf
832 mom_update: Dict[str, Any] = {
833 "A_jet_O": float(A_jet_O),
834 "A_jet_F": float(A_jet_F),
835 "momentum_ratio_n_elements_O": int(n_O),
836 "momentum_ratio_n_elements_F": int(n_F),
837 "d_jet_O": float(djo),
838 "d_jet_F": float(djf),
840 if np.isfinite(v_O_bulk):
841 mom_update[
"v_O_bulk"] = float(v_O_bulk)
842 if np.isfinite(v_F_bulk):
843 mom_update[
"v_F_bulk"] = float(v_F_bulk)
845 mom_update[
"rho_O_momentum"] = float(rho_O)
847 mom_update[
"rho_F_momentum"] = float(rho_F)
848 if np.isfinite(momentum_ratio_R)
and momentum_ratio_R > 0:
849 mom_update[
"momentum_ratio_R"] = momentum_ratio_R
850 if np.isfinite(rupe_M)
and rupe_M > 0:
851 mom_update[
"rupe_M"] = rupe_M
856 "u_O": float(u_O_final),
857 "u_F": float(u_F_final),
858 "V_rel": u_rel_final,
860 "P_injector_O": float(P_inj_O_final),
861 "P_injector_F": float(P_inj_F_final),
862 "delta_p_injector_O": float(delta_p_inj_O_final),
863 "delta_p_injector_F": float(delta_p_inj_F_final),
864 "delta_p_feed_O": float(delta_p_feed_O_final),
865 "delta_p_feed_F": float(delta_p_feed_F_final),
866 "feed_orifice_coupling_iterations": int(feed_orifice_fp_last),
868 "mdot_from_bernoulli_O": float(mdot_from_bernoulli_O),
869 "mdot_from_bernoulli_F": float(mdot_from_bernoulli_F),
875 A_eff_O, A_eff_F, eff_warns = effective_flow_areas_from_cd(diagnostics, A_O, A_F)
876 diagnostics[
"A_geom_O"] = float(A_O)
877 diagnostics[
"A_geom_F"] = float(A_F)
878 diagnostics[
"A_eff_O"] = float(A_eff_O)
879 diagnostics[
"A_eff_F"] = float(A_eff_F)
881 merge_effective_area_warnings(diagnostics, eff_warns)
883 return mdot_O, mdot_F, diagnostics