STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
graphite_geometry.py
Go to the documentation of this file.
1"""Graphite throat insert geometry sizing based on physics-based wall model with oxidation heat feedback.
2
3This module implements the sizing methodology from graphite_geometry.tex to determine:
4- Radial thickness (recession allowance + conduction + mechanical + safety factor)
5- Axial length (based on heat flux profile)
6- Transient verification through burn time
7
8The algorithm follows the method described in the LaTeX document for sizing graphite inserts
9that keep the back surface within safe temperature limits while providing recession allowance
10and mechanical support.
11"""
12
13from __future__ import annotations
14
15from dataclasses import dataclass
16from typing import Dict, Tuple, Optional, Callable, Union
17import numpy as np
18
19
20@dataclass
22 """Structured results from graphite insert sizing calculation."""
23 # Inputs (echoed for logging)
24 peak_heat_flux: float
25 surface_temperature: float
26 recession_rate: float
27 burn_time: float
28 thermal_conductivity: float
29 backface_temperature_max: float
30 throat_diameter: float
31 mechanical_thickness: float
32 safety_factor: float
33
34 # Thickness components
35 recession_allowance: float
36 conduction_thickness: float
37 conduction_model: str # "steady_state" or "transient" or "max"
38 safety_margin: float
39 initial_thickness: float
40
41 # Axial lengths
42 axial_half_length_upstream: float
43 axial_half_length_downstream: float
44 total_axial_length: float
45 sizing_method: str # "heat_flux_profile" or "simple_rule"
46
47 # Throat growth
48 throat_diameter_initial: float
49 throat_diameter_end: float
50 throat_area_change: float
51 throat_area_change_pct: float
52
53 # Integrity flags and notes
54 throat_area_change_excessive: bool # True if area_change_pct > 3%
55 conduction_thickness_zero: bool # True if t_cond == 0 (T_s <= T_b,max)
56 transient_backface_warning: bool # True if transient backface check fails
57 integrity_note: str # Human-readable note about any issues
58
59 def to_dict(self) -> Dict[str, Union[float, str]]:
60 """Convert to dictionary for backward compatibility."""
61 return {
62 "peak_heat_flux": self.peak_heat_flux,
63 "surface_temperature": self.surface_temperature,
64 "recession_rate": self.recession_rate,
65 "burn_time": self.burn_time,
66 "thermal_conductivity": self.thermal_conductivity,
67 "backface_temperature_max": self.backface_temperature_max,
68 "throat_diameter": self.throat_diameter,
69 "mechanical_thickness": self.mechanical_thickness,
70 "safety_factor": self.safety_factor,
71 "recession_allowance": self.recession_allowance,
72 "conduction_thickness": self.conduction_thickness,
73 "conduction_model": self.conduction_model,
74 "safety_margin": self.safety_margin,
75 "initial_thickness": self.initial_thickness,
76 "axial_half_length_upstream": self.axial_half_length_upstream,
77 "axial_half_length_downstream": self.axial_half_length_downstream,
78 "total_axial_length": self.total_axial_length,
79 "sizing_method": self.sizing_method,
80 "throat_diameter_initial": self.throat_diameter_initial,
81 "throat_diameter_end": self.throat_diameter_end,
82 "throat_area_change": self.throat_area_change,
83 "throat_area_change_pct": self.throat_area_change_pct,
84 "throat_area_change_excessive": self.throat_area_change_excessive,
85 "conduction_thickness_zero": self.conduction_thickness_zero,
86 "transient_backface_warning": self.transient_backface_warning,
87 "integrity_note": self.integrity_note,
88 }
89
90
92 peak_heat_flux: float,
93 burn_time: float,
94 throat_diameter: float,
95 thermal_conductivity: float,
96 density: Optional[float] = None,
97 specific_heat: Optional[float] = None,
98 heat_flux_profile: Optional[Callable] = None,
99 allowable_heat_flux_adjacent: Optional[float] = None,
100) -> None:
101 """Validate inputs and raise ValueError if invalid."""
102 if peak_heat_flux <= 0:
103 raise ValueError(f"peak_heat_flux must be > 0, got {peak_heat_flux}")
104 if burn_time <= 0:
105 raise ValueError(f"burn_time must be > 0, got {burn_time}")
106 if throat_diameter <= 0:
107 raise ValueError(f"throat_diameter must be > 0, got {throat_diameter}")
108 if thermal_conductivity <= 0:
109 raise ValueError(f"thermal_conductivity must be > 0, got {thermal_conductivity}")
110 if density is not None and density <= 0:
111 raise ValueError(f"density must be > 0, got {density}")
112 if specific_heat is not None and specific_heat <= 0:
113 raise ValueError(f"specific_heat must be > 0, got {specific_heat}")
114 if heat_flux_profile is not None and allowable_heat_flux_adjacent is None:
115 raise ValueError(
116 "allowable_heat_flux_adjacent must be provided when heat_flux_profile is provided"
117 )
118 if allowable_heat_flux_adjacent is not None and allowable_heat_flux_adjacent <= 0:
119 raise ValueError(
120 f"allowable_heat_flux_adjacent must be > 0, got {allowable_heat_flux_adjacent}"
121 )
122
123
125 peak_heat_flux: float,
126 surface_temperature: float,
127 recession_rate: float,
128 burn_time: float,
129 thermal_conductivity: float,
130 backface_temperature_max: float,
131 throat_diameter: float,
132 density: Optional[float] = None,
133 specific_heat: Optional[float] = None,
134) -> None:
135 """Check that all inputs are finite (not NaN or Inf)."""
136 inputs = {
137 "peak_heat_flux": peak_heat_flux,
138 "surface_temperature": surface_temperature,
139 "recession_rate": recession_rate,
140 "burn_time": burn_time,
141 "thermal_conductivity": thermal_conductivity,
142 "backface_temperature_max": backface_temperature_max,
143 "throat_diameter": throat_diameter,
144 }
145 if density is not None:
146 inputs["density"] = density
147 if specific_heat is not None:
148 inputs["specific_heat"] = specific_heat
149
150 for name, value in inputs.items():
151 if not np.isfinite(value):
152 raise ValueError(f"{name} must be finite, got {value}")
153
154
156 heat_flux_profile: Callable[[np.ndarray], np.ndarray],
157 x: Union[float, np.ndarray],
158) -> Union[float, np.ndarray]:
159 """
160 Ensure heat_flux_profile is called with vectorized input.
161
162 Parameters:
163 -----------
164 heat_flux_profile : callable
165 Function that accepts np.ndarray and returns np.ndarray
166 x : float or np.ndarray
167 Axial position(s) [m]
168
169 Returns:
170 --------
171 q : float or np.ndarray
172 Heat flux [W/m²]
173 """
174 x_array = np.atleast_1d(x)
175 q_array = heat_flux_profile(x_array)
176 if np.isscalar(x):
177 return q_array[0] # Explicit indexing for numerical safety
178 return q_array
179
180
182 recession_rate: float,
183 burn_time: float,
184) -> float:
185 """
186 Calculate recession allowance from recession rate and burn time.
187
188 From equation (eq:q_to_rdot) in graphite_geometry.tex:
189 Δ_ablate = ṙ × t_b
190
191 This is the total material that will be removed during the burn.
192
193 Parameters:
194 -----------
195 recession_rate : float
196 Net recession rate [m/s] from wall model output
197 burn_time : float
198 Total burn time [s]
199
200 Returns:
201 --------
202 delta_ablate : float
203 Recession allowance [m]
204 """
205 return recession_rate * burn_time
206
207
209 thermal_conductivity: float,
210 surface_temperature: float,
211 backface_temperature_max: float,
212 peak_heat_flux: float,
213) -> float:
214 """
215 Calculate minimum conduction thickness to keep back face below temperature limit (steady-state).
216
217 From equation (eq:tcond) in graphite_geometry.tex:
218 t_cond ≥ k × (T_s - T_b,max) / q''_peak
219
220 This is a conservative estimate assuming steady-state conduction through
221 the remaining thickness under peak heat flux.
222
223 Parameters:
224 -----------
225 thermal_conductivity : float
226 Graphite thermal conductivity [W/(m·K)]
227 surface_temperature : float
228 Surface temperature T_s [K] from wall model at design point
229 backface_temperature_max : float
230 Maximum allowable back-face temperature T_b,max [K] for metal substrate or adhesive
231 peak_heat_flux : float
232 Peak convective heat flux at throat q''_peak [W/m²]
233
234 Returns:
235 --------
236 t_cond : float
237 Minimum conduction thickness [m]
238 """
239 if peak_heat_flux <= 0:
240 return 0.0
241
242 t_cond = thermal_conductivity * (surface_temperature - backface_temperature_max) / peak_heat_flux
243 return max(t_cond, 0.0)
244
245
247 thermal_conductivity: float,
248 density: float,
249 specific_heat: float,
250 surface_heat_flux: float,
251 burn_time: float,
252 delta_T_backface_max: float,
253 eta: float = 2.5,
254) -> Tuple[float, bool]:
255 """
256 Calculate minimum conduction thickness using transient semi-infinite solid bound.
257
258 For short burns, the back face heats by diffusion. This provides a conservative
259 transient option that often reduces over-thick designs or catches under-thick ones
260 when q'' spikes.
261
262 Diffusion scale:
263 t_cond ≥ η * sqrt(α t_b)
264
265 Temperature rise check (independent):
266 ΔT_back(t) ≈ (2 q'' / k) * sqrt(α t / π)
267
268 where η ≈ 2–3 is a safety factor capturing that finite slabs heat faster near backface.
269
270 Parameters:
271 -----------
272 thermal_conductivity : float
273 Thermal conductivity k [W/(m·K)]
274 density : float
275 Material density ρ_s [kg/m³]
276 specific_heat : float
277 Specific heat c_p [J/(kg·K)]
278 surface_heat_flux : float
279 Surface heat flux q'' [W/m²]
280 burn_time : float
281 Burn time t_b [s]
282 delta_T_backface_max : float
283 Maximum allowable back-face temperature rise T_b,max - T_init [K]
284 eta : float
285 Safety factor (default 2.5, range 2-3) for conservatism
286
287 Returns:
288 --------
289 t_cond : float
290 Minimum conduction thickness [m] (diffusion scale)
291 meets_backface : bool
292 True if backface temperature rise is within limit with this thickness
293 """
294 alpha = thermal_conductivity / (density * specific_heat)
295
296 # Diffusion scale: minimum penetration depth
297 t_cond = eta * np.sqrt(alpha * burn_time)
298
299 # Independently check temperature rise constraint
300 meets_backface = True
301 if delta_T_backface_max > 0 and surface_heat_flux > 0:
302 # From semi-infinite solution: ΔT = (2 q'' / k) * sqrt(α t / π)
303 delta_T_estimate = (2.0 * surface_heat_flux / thermal_conductivity) * np.sqrt(alpha * burn_time / np.pi)
304 meets_backface = delta_T_estimate <= delta_T_backface_max
305
306 return max(t_cond, 0.0), meets_backface
307
308
310 recession_allowance: float,
311 conduction_thickness: float,
312 mechanical_thickness: float = 0.001,
313 safety_factor: float = 0.3,
314) -> float:
315 """
316 Calculate initial radial thickness at throat.
317
318 From equation (eq:thickness) in graphite_geometry.tex:
319 t_insert,0 = Δ_ablate + t_cond + t_mech + φ_sf
320
321 where:
322 - Δ_ablate: recession allowance from wall model
323 - t_cond: conduction thickness for back-face temperature control
324 - t_mech: mechanical lip, fit, and strength requirements
325 - φ_sf: safety factor (typically 30-50% of (Δ_ablate + t_cond))
326
327 Parameters:
328 -----------
329 recession_allowance : float
330 Recession allowance Δ_ablate [m]
331 conduction_thickness : float
332 Conduction thickness t_cond [m]
333 mechanical_thickness : float
334 Mechanical thickness t_mech [m] for groove, seating, strength
335 Typical: 0.5-1.5 mm for small throats, thicker for large bores
336 safety_factor : float
337 Safety factor as fraction (0.3 = 30%, 0.5 = 50%)
338 Applied to (Δ_ablate + t_cond) to cover property scatter and profile uncertainty
339
340 Returns:
341 --------
342 t_insert_0 : float
343 Initial radial thickness at throat [m]
344 """
345 base_thickness = recession_allowance + conduction_thickness
346 safety_margin = safety_factor * base_thickness
347 t_insert_0 = base_thickness + mechanical_thickness + safety_margin
348 return max(t_insert_0, 0.0)
349
350
352 heat_flux_profile: Callable[[np.ndarray], np.ndarray],
353 allowable_heat_flux_adjacent: float,
354 throat_diameter: float,
355) -> Tuple[float, float]:
356 """
357 Calculate axial half-lengths upstream and downstream separately.
358
359 The upstream shoulder sees a different q''(x) than downstream. This function
360 solves for both half-lengths independently.
361
362 From section "Axial length" in graphite_geometry.tex:
363 q''(|x| = L_±) ≤ q''_allow,adjacent
364
365 where x < 0 is upstream and x > 0 is downstream.
366
367 Parameters:
368 -----------
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]
377
378 Returns:
379 --------
380 L_upstream : float
381 Axial half-length upstream [m] (negative x direction)
382 L_downstream : float
383 Axial half-length downstream [m] (positive x direction)
384 """
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
389
390 # Binary search: find smallest |L| where q''(±L) ≤ q_allow
391 # When q <= q_allow, move upper bound down (to find smaller L)
392 # When q > q_allow, move lower bound up (need larger L)
393 for _ in range(24):
394 L_mid = 0.5 * (L_min + L_max)
395 x_test = np.array([sign * L_mid])
396 q_array = _vectorized_heat_flux_profile(heat_flux_profile, x_test)
397 q = q_array[0] if isinstance(q_array, np.ndarray) else q_array # Explicit indexing
398
399 if q <= allowable_heat_flux_adjacent:
400 L_max = L_mid # Move upper bound down to find smaller L
401 else:
402 L_min = L_mid # Move lower bound up, need larger L
403
404 if abs(L_max - L_min) < 1e-6:
405 break
406
407 return L_max # Return the smallest L that satisfies q <= q_allow
408
409 L_upstream = find_L(-1.0)
410 L_downstream = find_L(+1.0)
411
412 return L_upstream, L_downstream
413
414
416 throat_diameter: float,
417 length_factor: float = 0.75,
418) -> Tuple[float, float]:
419 """
420 Simple rule-of-thumb for axial half-lengths when heat flux profile is not available.
421
422 From graphite_geometry.tex:
423 L_± = (0.5 to 1.0) × D_t on each side of throat
424
425 Many teams converge to a total graphite length of 1.5 to 2.5 × D_t.
426
427 Parameters:
428 -----------
429 throat_diameter : float
430 Throat diameter D_t [m]
431 length_factor : float
432 Factor to multiply D_t (default 0.75, range 0.5-1.0)
433
434 Returns:
435 --------
436 L_upstream : float
437 Axial half-length upstream [m]
438 L_downstream : float
439 Axial half-length downstream [m]
440 """
441 L = length_factor * throat_diameter
442 return L, L
443
444
446 thermal_conductivity: float,
447 density: float,
448 specific_heat: float,
449 time: float,
450) -> float:
451 """
452 Calculate thermal penetration depth for 1D semi-infinite solid.
453
454 From section "Transient conduction and penetration depth" in graphite_geometry.tex:
455 δ(t) = √(α × t)
456 α = k / (ρ_s × c_p)
457
458 This is a conservative bound for thickness estimation. The heat footprint is narrow
459 and flux decays strongly away from throat, so this over-predicts for real nozzles.
460 Use it to bound the thickness, not to set it directly.
461
462 Parameters:
463 -----------
464 thermal_conductivity : float
465 Thermal conductivity k [W/(m·K)]
466 density : float
467 Material density ρ_s [kg/m³]
468 specific_heat : float
469 Specific heat c_p [J/(kg·K)]
470 time : float
471 Time t [s]
472
473 Returns:
474 --------
475 penetration_depth : float
476 Thermal penetration depth δ [m]
477 """
478 alpha = thermal_conductivity / (density * specific_heat)
479 penetration_depth = np.sqrt(alpha * time)
480 return penetration_depth
481
482
484 throat_diameter_initial: float,
485 recession_allowance: float,
486) -> Dict[str, float]:
487 """
488 Check throat growth due to recession and calculate area change.
489
490 Recession at the throat increases A_t → lowers q''_peak but also changes performance.
491 This provides a simple post-check to warn if ΔA_t/A_t > 2-3%.
492
493 Parameters:
494 -----------
495 throat_diameter_initial : float
496 Initial throat diameter D_t,0 [m]
497 recession_allowance : float
498 Recession allowance Δ_ablate [m] (radial loss on each side)
499
500 Returns:
501 --------
502 results : dict
503 Dictionary containing:
504 - throat_diameter_end: D_t,end [m]
505 - throat_area_initial: A_t,0 [m²]
506 - throat_area_end: A_t,end [m²]
507 - throat_area_change: ΔA_t [m²]
508 - throat_area_change_pct: ΔA_t/A_t,0 [%]
509 """
510 # Radial loss on each side of bore
511 # Note: recession reduces the inner diameter, so D_t increases
512 Dt_end = max(throat_diameter_initial + 2.0 * recession_allowance, 1e-6)
513
514 At_initial = (np.pi / 4.0) * throat_diameter_initial ** 2
515 At_end = (np.pi / 4.0) * Dt_end ** 2
516 area_change = At_end - At_initial
517 area_change_pct = (area_change / At_initial) * 100.0 if At_initial > 0 else 0.0
518
519 return {
520 "throat_diameter_end": float(Dt_end),
521 "throat_area_initial": float(At_initial),
522 "throat_area_end": float(At_end),
523 "throat_area_change": float(area_change),
524 "throat_area_change_pct": float(area_change_pct),
525 }
526
527
529 time: np.ndarray,
530 recession_rate: np.ndarray,
531 initial_thickness: float,
532 mechanical_thickness: float = 0.001,
533) -> Dict[str, float]:
534 """
535 Verify thickness throughout burn by integrating recession rate over time.
536
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
539 consumed thickness.
540
541 Parameters:
542 -----------
543 time : np.ndarray
544 Time array [s]
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
551
552 Returns:
553 --------
554 results : dict
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
560 """
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)}")
563
564 if len(time) < 2:
565 raise ValueError("time array must have at least 2 elements")
566
567 # Calculate time steps
568 dt = np.diff(time, prepend=time[0])
569
570 # Integrate recession
571 consumed = np.sum(recession_rate * dt)
572 remaining = initial_thickness - consumed
573
574 # Track minimum remaining (accounting for cumulative consumption)
575 cumulative_consumed = np.cumsum(recession_rate * dt)
576 remaining_history = initial_thickness - cumulative_consumed
577 min_remaining = float(np.min(remaining_history))
578
579 meets_mechanical = min_remaining >= mechanical_thickness
580
581 return {
582 "consumed": float(consumed),
583 "remaining": float(remaining),
584 "min_remaining": float(min_remaining),
585 "meets_mechanical": bool(meets_mechanical),
586 }
587
588
590 peak_heat_flux: float,
591 surface_temperature: float,
592 recession_rate: float,
593 burn_time: 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:
608 """
609 Size graphite throat insert following the algorithm from graphite_geometry.tex.
610
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
619
620 Parameters:
621 -----------
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
628 burn_time : float
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%)
647 transient : bool
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)
660
661 Returns:
662 --------
663 sizing : GraphiteInsertSizing
664 Structured sizing results
665 """
666 # Validate inputs
668 peak_heat_flux,
669 burn_time,
670 throat_diameter,
671 thermal_conductivity,
672 density,
673 specific_heat,
674 heat_flux_profile,
675 allowable_heat_flux_adjacent,
676 )
677
678 # Check for finite values (NaN/Inf)
680 peak_heat_flux,
681 surface_temperature,
682 recession_rate,
683 burn_time,
684 thermal_conductivity,
685 backface_temperature_max,
686 throat_diameter,
687 density,
688 specific_heat,
689 )
690
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")
693
694 # Step 3: Calculate recession allowance
695 delta_ablate = calculate_recession_allowance(recession_rate, burn_time)
696
697 # Step 4: Calculate conduction thickness
698 t_cond_steady = calculate_conduction_thickness(
699 thermal_conductivity,
700 surface_temperature,
701 backface_temperature_max,
702 peak_heat_flux,
703 )
704
705 transient_backface_warning = False
706 if transient:
707 if backface_temperature_initial is None:
708 backface_temperature_initial = 300.0
709 delta_T_backface_max = backface_temperature_max - backface_temperature_initial
710
711 t_cond_transient, meets_backface = calculate_conduction_thickness_transient(
712 thermal_conductivity,
713 density,
714 specific_heat,
715 peak_heat_flux,
716 burn_time,
717 delta_T_backface_max,
718 transient_eta,
719 )
720
721 if not meets_backface:
722 transient_backface_warning = True
723 # Take max of steady and transient, but flag the warning
724 t_cond = max(t_cond_steady, t_cond_transient)
725 conduction_model = "max"
726 else:
727 # Take maximum of steady and transient
728 t_cond = max(t_cond_steady, t_cond_transient)
729 conduction_model = "max"
730 else:
731 t_cond = t_cond_steady
732 conduction_model = "steady_state"
733
734 # Step 5: Calculate initial thickness
735 base_thickness = delta_ablate + t_cond
736 safety_margin = safety_factor * base_thickness
737 t_insert_0 = calculate_initial_thickness(
738 delta_ablate,
739 t_cond,
740 mechanical_thickness,
741 safety_factor,
742 )
743
744 # Step 6: Calculate axial lengths
745 if heat_flux_profile is not None and allowable_heat_flux_adjacent is not None:
746 L_upstream, L_downstream = calculate_axial_lengths_from_profile(
747 heat_flux_profile,
748 allowable_heat_flux_adjacent,
749 throat_diameter,
750 )
751 sizing_method = "heat_flux_profile"
752 else:
753 L_upstream, L_downstream = calculate_axial_length_simple(throat_diameter, axial_length_factor)
754 sizing_method = "simple_rule"
755
756 # Step 7: Check throat growth
757 growth_check = throat_growth_check(throat_diameter, delta_ablate)
758
759 # Calculate integrity flags
760 throat_area_change_excessive = growth_check["throat_area_change_pct"] > 3.0
761 conduction_thickness_zero = t_cond == 0.0
762
763 # Build integrity note
764 notes = []
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")
771
772 integrity_note = "; ".join(notes) if notes else "All checks passed"
773
775 # Inputs
776 peak_heat_flux=peak_heat_flux,
777 surface_temperature=surface_temperature,
778 recession_rate=recession_rate,
779 burn_time=burn_time,
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,
785 # Thickness components
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,
791 # Axial lengths
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,
796 # Throat growth
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"],
801 # Integrity flags
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,
806 )
807
808
810 peak_heat_flux: float,
811 surface_temperature: float,
812 burn_time: 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]:
819 """
820 Quick back-of-envelope sizing using shop-note numbers.
821
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)
826
827 Uses conservative material bounds: k = 1500 W/(m·K)
828
829 Parameters:
830 -----------
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)
835 burn_time : float
836 Burn time [s]
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%)
847
848 Returns:
849 --------
850 results : dict
851 Dictionary with sizing results
852 """
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)
856
857 base_thickness = delta_ablate + t_cond
858 safety_margin = safety_factor * base_thickness
859 t_insert_0 = base_thickness + mechanical_thickness + safety_margin
860
861 return {
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",
868 }
#define max(a, b)
Definition Arduino.h:43
None _check_finite(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)
None _validate_inputs(float peak_heat_flux, float burn_time, float throat_diameter, float thermal_conductivity, Optional[float] density=None, Optional[float] specific_heat=None, Optional[Callable] heat_flux_profile=None, Optional[float] allowable_heat_flux_adjacent=None)
float calculate_initial_thickness(float recession_allowance, float conduction_thickness, float mechanical_thickness=0.001, float safety_factor=0.3)
Dict[str, float] quick_back_of_envelope_sizing(float peak_heat_flux, float surface_temperature, float burn_time, float backface_temperature_max, float thermal_conductivity=1500.0, float recession_rate=6e-5, float mechanical_thickness=0.001, float safety_factor=0.3)
Tuple[float, bool] calculate_conduction_thickness_transient(float thermal_conductivity, float density, float specific_heat, float surface_heat_flux, float burn_time, float delta_T_backface_max, float eta=2.5)
Dict[str, float] verify_transient_thickness(np.ndarray time, np.ndarray recession_rate, float initial_thickness, float mechanical_thickness=0.001)
Union[float, np.ndarray] _vectorized_heat_flux_profile(Callable[[np.ndarray], np.ndarray] heat_flux_profile, Union[float, np.ndarray] x)
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)
Dict[str, float] throat_growth_check(float throat_diameter_initial, float recession_allowance)
float calculate_conduction_thickness(float thermal_conductivity, float surface_temperature, float backface_temperature_max, float peak_heat_flux)
Tuple[float, float] calculate_axial_lengths_from_profile(Callable[[np.ndarray], np.ndarray] heat_flux_profile, float allowable_heat_flux_adjacent, float throat_diameter)
float calculate_recession_allowance(float recession_rate, float burn_time)
Tuple[float, float] calculate_axial_length_simple(float throat_diameter, float length_factor=0.75)
float calculate_thermal_penetration_depth(float thermal_conductivity, float density, float specific_heat, float time)