352 heat_flux_profile: Callable[[np.ndarray], np.ndarray],
353 allowable_heat_flux_adjacent: float,
354 throat_diameter: float,
355) -> Tuple[float, float]:
357 Calculate axial half-lengths upstream and downstream separately.
359 The upstream shoulder sees a different q''(x) than downstream. This function
360 solves for both half-lengths independently.
362 From section "Axial length" in graphite_geometry.tex:
363 q''(|x| = L_±) ≤ q''_allow,adjacent
365 where x < 0 is upstream and x > 0 is downstream.
369 heat_flux_profile : callable
370 Function q''(x) that returns heat flux [W/m²] at axial position x [m]
371 Must accept np.ndarray and return np.ndarray (vectorized).
372 x=0 is at throat, x < 0 is upstream, x > 0 is downstream
373 allowable_heat_flux_adjacent : float
374 Maximum heat flux q''_allow,adjacent [W/m²] that adjacent material can handle
375 throat_diameter : float
376 Throat diameter D_t [m]
381 Axial half-length upstream [m] (negative x direction)
383 Axial half-length downstream [m] (positive x direction)
385 def find_L(sign: float) -> float:
386 """Find half-length in direction given by sign (-1 for upstream, +1 for downstream)."""
387 L_min = 0.1 * throat_diameter
388 L_max = 2.5 * throat_diameter
394 L_mid = 0.5 * (L_min + L_max)
395 x_test = np.array([sign * L_mid])
397 q = q_array[0]
if isinstance(q_array, np.ndarray)
else q_array
399 if q <= allowable_heat_flux_adjacent:
404 if abs(L_max - L_min) < 1e-6:
409 L_upstream = find_L(-1.0)
410 L_downstream = find_L(+1.0)
412 return L_upstream, L_downstream
530 recession_rate: np.ndarray,
531 initial_thickness: float,
532 mechanical_thickness: float = 0.001,
533) -> Dict[str, float]:
535 Verify thickness throughout burn by integrating recession rate over time.
537 Most of the error comes from assuming constant q'' and ṙ. This function allows
538 you to feed per-time-step wall-model results (q''(t), T_s(t), ṙ(t)) and integrate
545 recession_rate : np.ndarray
546 Recession rate ṙ(t) [m/s] from wall model per step
547 initial_thickness : float
548 Initial thickness t_insert,0 [m]
549 mechanical_thickness : float
550 Minimum mechanical thickness [m] to check against
555 Dictionary containing:
556 - consumed: Total consumed thickness [m]
557 - remaining: Remaining thickness at end [m]
558 - min_remaining: Minimum remaining thickness during burn [m]
559 - meets_mechanical: bool, whether min_remaining >= mechanical_thickness
561 if len(time) != len(recession_rate):
562 raise ValueError(f
"time and recession_rate must have same length, got {len(time)} and {len(recession_rate)}")
565 raise ValueError(
"time array must have at least 2 elements")
568 dt = np.diff(time, prepend=time[0])
571 consumed = np.sum(recession_rate * dt)
572 remaining = initial_thickness - consumed
575 cumulative_consumed = np.cumsum(recession_rate * dt)
576 remaining_history = initial_thickness - cumulative_consumed
577 min_remaining = float(np.min(remaining_history))
579 meets_mechanical = min_remaining >= mechanical_thickness
582 "consumed": float(consumed),
583 "remaining": float(remaining),
584 "min_remaining": float(min_remaining),
585 "meets_mechanical": bool(meets_mechanical),
590 peak_heat_flux: float,
591 surface_temperature: float,
592 recession_rate: float,
594 thermal_conductivity: float,
595 backface_temperature_max: float,
596 throat_diameter: float,
597 density: Optional[float] =
None,
598 specific_heat: Optional[float] =
None,
599 backface_temperature_initial: Optional[float] =
None,
600 mechanical_thickness: float = 0.001,
601 safety_factor: float = 0.3,
602 transient: bool =
False,
603 transient_eta: float = 2.5,
604 heat_flux_profile: Optional[Callable[[np.ndarray], np.ndarray]] =
None,
605 allowable_heat_flux_adjacent: Optional[float] =
None,
606 axial_length_factor: float = 0.75,
607) -> GraphiteInsertSizing:
609 Size graphite throat insert following the algorithm from graphite_geometry.tex.
611 This implements the full sizing algorithm (section "Algorithm you can implement"):
612 1. Compute q''_peak at throat (input)
613 2. Get T_s, ṙ from wall model (inputs)
614 3. Set Δ_ablate = ṙ × t_b
615 4. Compute t_cond from equation (eq:tcond) or transient bound
616 5. Choose t_insert,0 from equation (eq:thickness)
617 6. Select L_± based on heat flux profile or simple rule
618 7. Check throat growth
622 peak_heat_flux : float
623 Peak convective heat flux at throat q''_peak [W/m²]
624 surface_temperature : float
625 Surface temperature T_s [K] from wall model at design point
626 recession_rate : float
627 Net recession rate ṙ [m/s] from wall model output
629 Total burn time t_b [s]
630 thermal_conductivity : float
631 Graphite thermal conductivity k [W/(m·K)]
632 backface_temperature_max : float
633 Maximum allowable back-face temperature T_b,max [K]
634 throat_diameter : float
635 Throat diameter D_t [m]
636 density : float, optional
637 Material density ρ_s [kg/m³]. Required if transient=True
638 specific_heat : float, optional
639 Specific heat c_p [J/(kg·K)]. Required if transient=True
640 backface_temperature_initial : float, optional
641 Initial back-face temperature T_b,init [K]. If None, assumed 300 K.
642 Used for transient calculation: delta_T = T_b,max - T_b,init
643 mechanical_thickness : float
644 Mechanical thickness t_mech [m] (default 1 mm)
645 safety_factor : float
646 Safety factor as fraction (default 0.3 = 30%)
648 If True, use transient conduction model (or max of steady and transient)
649 If False, use only steady-state model
650 transient_eta : float
651 Safety factor for transient model (default 2.5, range 2-3)
652 heat_flux_profile : callable, optional
653 Function q''(x) for axial heat flux profile. Must accept np.ndarray and return np.ndarray.
654 If provided, used to calculate L_± separately for upstream and downstream.
655 x=0 is at throat, x < 0 is upstream, x > 0 is downstream
656 allowable_heat_flux_adjacent : float, optional
657 Maximum heat flux for adjacent material [W/m²]. Required if heat_flux_profile provided
658 axial_length_factor : float
659 Factor for simple L_± calculation if profile not available (default 0.75)
663 sizing : GraphiteInsertSizing
664 Structured sizing results
671 thermal_conductivity,
675 allowable_heat_flux_adjacent,
684 thermal_conductivity,
685 backface_temperature_max,
691 if transient
and (density
is None or specific_heat
is None):
692 raise ValueError(
"density and specific_heat must be provided when transient=True")
699 thermal_conductivity,
701 backface_temperature_max,
705 transient_backface_warning =
False
707 if backface_temperature_initial
is None:
708 backface_temperature_initial = 300.0
709 delta_T_backface_max = backface_temperature_max - backface_temperature_initial
712 thermal_conductivity,
717 delta_T_backface_max,
721 if not meets_backface:
722 transient_backface_warning =
True
724 t_cond =
max(t_cond_steady, t_cond_transient)
725 conduction_model =
"max"
728 t_cond =
max(t_cond_steady, t_cond_transient)
729 conduction_model =
"max"
731 t_cond = t_cond_steady
732 conduction_model =
"steady_state"
735 base_thickness = delta_ablate + t_cond
736 safety_margin = safety_factor * base_thickness
740 mechanical_thickness,
745 if heat_flux_profile
is not None and allowable_heat_flux_adjacent
is not None:
748 allowable_heat_flux_adjacent,
751 sizing_method =
"heat_flux_profile"
754 sizing_method =
"simple_rule"
760 throat_area_change_excessive = growth_check[
"throat_area_change_pct"] > 3.0
761 conduction_thickness_zero = t_cond == 0.0
765 if throat_area_change_excessive:
766 notes.append(f
"Throat area change {growth_check['throat_area_change_pct']:.2f}% exceeds 3% threshold")
767 if conduction_thickness_zero:
768 notes.append(
"Conduction thickness is zero (T_s <= T_b,max) - verify inputs")
769 if transient_backface_warning:
770 notes.append(
"Transient backface temperature rise would exceed limit with current eta; consider thicker insert or lower q''_peak")
772 integrity_note =
"; ".join(notes)
if notes
else "All checks passed"
776 peak_heat_flux=peak_heat_flux,
777 surface_temperature=surface_temperature,
778 recession_rate=recession_rate,
780 thermal_conductivity=thermal_conductivity,
781 backface_temperature_max=backface_temperature_max,
782 throat_diameter=throat_diameter,
783 mechanical_thickness=mechanical_thickness,
784 safety_factor=safety_factor,
786 recession_allowance=delta_ablate,
787 conduction_thickness=t_cond,
788 conduction_model=conduction_model,
789 safety_margin=safety_margin,
790 initial_thickness=t_insert_0,
792 axial_half_length_upstream=L_upstream,
793 axial_half_length_downstream=L_downstream,
794 total_axial_length=L_upstream + L_downstream,
795 sizing_method=sizing_method,
797 throat_diameter_initial=throat_diameter,
798 throat_diameter_end=growth_check[
"throat_diameter_end"],
799 throat_area_change=growth_check[
"throat_area_change"],
800 throat_area_change_pct=growth_check[
"throat_area_change_pct"],
802 throat_area_change_excessive=throat_area_change_excessive,
803 conduction_thickness_zero=conduction_thickness_zero,
804 transient_backface_warning=transient_backface_warning,
805 integrity_note=integrity_note,
810 peak_heat_flux: float,
811 surface_temperature: float,
813 backface_temperature_max: float,
814 thermal_conductivity: float = 1500.0,
815 recession_rate: float = 6e-5,
816 mechanical_thickness: float = 0.001,
817 safety_factor: float = 0.3,
818) -> Dict[str, float]:
820 Quick back-of-envelope sizing using shop-note numbers.
822 From section "Quick numeric back-of-envelope" in graphite_geometry.tex:
823 Δ_ablate ≈ 0.6 mm (for t_b = 10 s, ṙ = 6e-5 m/s)
824 t_cond ≈ k × (T_s - T_b,max) / q''_peak
825 t_insert,0 ≈ Δ_ablate + t_cond + t_mech + 0.3 × (Δ_ablate + t_cond)
827 Uses conservative material bounds: k = 1500 W/(m·K)
831 peak_heat_flux : float
832 Peak heat flux at throat [W/m²]
833 surface_temperature : float
834 Surface temperature [K] (from model or estimate)
837 backface_temperature_max : float
838 Maximum back-face temperature [K]
839 thermal_conductivity : float
840 Thermal conductivity [W/(m·K)] (default 1500, conservative lower bound)
841 recession_rate : float
842 Recession rate [m/s] (default 6e-5 for LOX/RP-1)
843 mechanical_thickness : float
844 Mechanical thickness [m] (default 1 mm)
845 safety_factor : float
846 Safety factor (default 0.3 = 30%)
851 Dictionary with sizing results
853 delta_ablate = recession_rate * burn_time
854 t_cond = thermal_conductivity * (surface_temperature - backface_temperature_max) / peak_heat_flux
855 t_cond =
max(t_cond, 0.0)
857 base_thickness = delta_ablate + t_cond
858 safety_margin = safety_factor * base_thickness
859 t_insert_0 = base_thickness + mechanical_thickness + safety_margin
862 "recession_allowance": float(delta_ablate),
863 "conduction_thickness": float(t_cond),
864 "mechanical_thickness": float(mechanical_thickness),
865 "safety_margin": float(safety_margin),
866 "initial_thickness": float(t_insert_0),
867 "method":
"back_of_envelope",
GraphiteInsertSizing size_graphite_insert(float peak_heat_flux, float surface_temperature, float recession_rate, float burn_time, float thermal_conductivity, float backface_temperature_max, float throat_diameter, Optional[float] density=None, Optional[float] specific_heat=None, Optional[float] backface_temperature_initial=None, float mechanical_thickness=0.001, float safety_factor=0.3, bool transient=False, float transient_eta=2.5, Optional[Callable[[np.ndarray], np.ndarray]] heat_flux_profile=None, Optional[float] allowable_heat_flux_adjacent=None, float axial_length_factor=0.75)