163class InjectorPlateConfig(BaseModel):
164 """The injector as a plug in the chamber sleeve: how its face and back are machined.
165 Geometry, plus a closed-form plate-bending estimate -- FEA, seals, the manifold cover and the
166 plug's retention are the designer's; engine/core/injectors/layout.py reports the lands they get. The holes' L/d is
167 discharge.<side>.orifice_l_over_d: one number sets both the Cd and, with channels, how deep
169 face: Literal["flat", "contoured"] = Field(default="contoured",
170 description="flat: orifices leave an axis-normal face at the jet angle (elliptical exits, "
171 "the drill enters off square). contoured: the face is turned with an annular "
172 "groove whose flanks are normal to the jets, so each orifice leaves square and "
173 "round (SP-8089 p.43, 'local grooving ... to increase the effective angle').")
174 exit_land: Optional[float] = Field(default=None, ge=0.0,
175 description="Metal between a round exit and the edge of its flank, and between a passage "
176 "and its channel wall [m]. None => 0.5 mm (assumed).")
177 groove_bottom: Literal["flat", "v"] = Field(default="flat",
178 description="Contoured face: flat-bottomed groove just deep enough for each exit's land "
179 "(default), or the sharp V where the flanks meet.")
180 back: Literal["plenum", "channels"] = Field(default="channels",
181 description="plenum: passages run through to a flat back face (a manifold volume behind "
182 "it). channels: the back face is flat with one annular channel per ring; each "
183 "passage runs from its exit to its channel floor and the channel is centred on "
184 "where the passage meets it.")
185 channel_width: Optional[float] = Field(default=None, gt=0.0,
186 description="Channels back: channel width [m]. None or narrower than the passage footprint "
187 "=> footprint + 2 x exit_land.")
188 channel_floor: Literal["flat", "coned", "spot"] = Field(default="flat",
189 description="Channels back: flat floor (holes break through off square; deburr and "
190 "flow-test), a floor turned as a cone normal to the passages (square "
191 "breakthrough), or spot: a flat floor channel_width wide beside a drill-spot "
192 "facet square to the passages, channel_spot_length across (square breakthrough; "
193 "the stand's plate).")
194 channel_spot_length: Optional[float] = Field(default=None, gt=0.0,
195 description="Spot floor: the facet's length across the hole [m]. None => d + 2 x exit_land.")
196 channel_inlets: int = Field(default=1, ge=1,
197 description="Channels back: feed ports into each channel. Each splits two ways around "
198 "the ring, so a branch carries mdot / (2 x inlets) at the port; the layout "
199 "checks that branch's velocity head against the injector drop.")
200 support: Literal["simply_supported", "clamped"] = Field(default="simply_supported",
201 description="How the plug's edge is held, for the layout's plate-bending check (circular "
202 "plate under Pc over the bore). simply_supported (conservative for a plug on a "
203 "shoulder or retaining ring) or clamped (edge cannot rotate).")
204 yield_strength: Optional[float] = Field(default=None, gt=0.0,
205 description="Plate material yield strength at temperature [Pa]. None => the plate-bending "
206 "check reports stress only; declared, stress above yield / 1.5 is flagged.")
207 poisson_ratio: Optional[float] = Field(default=None, gt=0.0, lt=0.5,
208 description="Plate material Poisson ratio for the plate-bending check. None => 0.3 (assumed).")
209 channel_flow_height_O: Optional[float] = Field(default=None, gt=0.0,
210 description="Channels back: height [m] of the LOX manifold passage the flow sees (the "
211 "channel plus any groove in the cover). None => the channel depth the layout "
212 "draws. Flow area = channel_width x this.")
213 channel_flow_height_F: Optional[float] = Field(default=None, gt=0.0,
214 description="As channel_flow_height_O, for the fuel channel [m].")
215 manifold_model: Literal["ring_network", "plenum"] = Field(default="ring_network",
216 description="Channels back: 'ring_network' solves each channel as a dividing-flow ring "
217 "(each port splits two ways; holes fed by the local static pressure, NASA TN "
218 "D-5467; friction and momentum regain between holes, Acrivos/Bajura). 'plenum' "
219 "treats the manifold as still fluid at one pressure (the old model).")
220 channel_entry_K: float = Field(default=0.5, ge=0.0,
221 description="Loss from the feed port into each channel branch, on the branch velocity head "
222 "(sharp entrance, Idelchik diagram 3-1: 0.5). Assumed until flow-tested.")
223 channel_pressure_regain: float = Field(default=1.0, ge=0.0, le=1.0,
224 description="Static-pressure regain fraction as the branch slows past each hole (dividing-"
225 "flow manifold; 1 = ideal momentum recovery, Acrivos, Babcock & Pigford 1959; "
226 "measured 0.6-1). Assumed until flow-tested.")
227 channel_roughness: float = Field(default=3.2e-6, ge=0.0,
228 description="Channel wall roughness [m] for Churchill friction (machined, Ra ~0.8 um).")
229 back_grooves: List[BackGrooveConfig] = Field(default_factory=list,
230 description="Grooves cut in the back face (face-seal glands), as drawn. The layout draws "
231 "them, keeps them in the plate section (bending) and flags one that runs into "
232 "a channel, the centre port or another groove.")
233 rim_gland: Optional[RimGlandConfig] = Field(default=None,
234 description="Groove round the plug's rim (radial seal gland), as drawn.")
235 port_thread: Optional[Literal["1/8 NPT", "1/4 NPT", "3/8 NPT", "1/2 NPT", "3/4 NPT"]] = Field(
237 description="Feed ports in the cover plate above the channels, channel_inlets per ring. "
238 "Drawn in the port-plate view; each port's bore is checked against the "
239 "back-face grooves it lands over.")
240 port_bore: Optional[float] = Field(default=None, gt=0.0,
241 description="Diameter the port opens with onto the injector's back face [m]. None => the "
242 "thread's tap drill.")
243 port_radius_O: Optional[float] = Field(default=None, gt=0.0,
244 description="Radius of the LOX ports [m]. None => the middle of the LOX channel.")
245 port_radius_F: Optional[float] = Field(default=None, gt=0.0,
246 description="Radius of the fuel ports [m]. None => the middle of the fuel channel.")
247 port_clock_F_deg: Optional[float] = Field(default=None,
248 description="Angle from the first LOX port to the first fuel port [deg]. None => half the "
250 profile_dxf_mode: Literal["geometry", "check"] = Field(default="geometry",
251 description="geometry: the drawing IS the plate (its section replaces the model's). check: "
252 "the plate is built from the fields above and compared with the drawing, line "
253 "by line; where they differ the layout says so.")
254 profile_dxf: Optional[str] = Field(default=None,
255 description="The plate's revolved half-section as drawn (a DXF of the revolve sketch, "
256 "units set; path absolute or from EngineDesign/). When set it is the geometry: "
257 "plate thickness, face groove, the channels' sections (flow area, hydraulic "
258 "diameter, centroid radius) and each passage's exit, length and entry are read "
259 "from it (engine/core/injectors/plate_dxf.py), and where the config's rings, L/d "
260 "or igniter disagree with it the layout says so. channel_width and "
261 "channel_flow_height_* are then not used.")
264class ImpingingInjectorConfig(InjectorBaseConfig):
265 """Impinging-element injector configuration"""
266 type: Literal["impinging"] = "impinging"
267 geometry: ImpingingInjectorGeometry
268 igniter: Optional[IgniterPortConfig] = Field(default=None,
269 description="Igniter port through the plate centre. None => no port modelled.")
270 plate: Optional[InjectorPlateConfig] = Field(default=None,
271 description="How the plug's face and back are machined, and its stress-check material. "
272 "None => drawn as a contoured face with a channel back (the defaults); Layer 1 applies the plug's constraints only when declared.")
275#: Named feed-line sizes -> flow bore [m]. A line size is a NAME, not a diameter: "3/8 NPT"
276#: names a *thread*, and its flow area is set by the fitting's through-bore (~0.380"), not by
277#: the 0.375" thread nominal. Those two differ by only 1.3% in diameter but the pair is exactly
278#: the kind of near-miss that looks right forever -- so the mapping lives here, once, by name.
279FEED_LINE_SIZES: Dict[str, float] = {
280 # NPT fittings — keyed on the thread callout the user actually buys, valued at the bore.
281 "1/4_NPT": 0.00635, # 0.250" through-bore
282 "3/8_NPT": 0.0096520, # 0.380" through-bore (NOT 0.375" — that is the thread nominal)
283 "1/2_NPT": 0.0127000, # 0.500" through-bore
284 "3/4_NPT": 0.0190500, # 0.750" through-bore
285 # Tube ODs quote a wall, so the bore depends on it; these assume 0.035" wall.
286 # Both values encoded a 0.0325" wall, not the 0.035" their names and comments claim
287 # (and the 1/2" comment's own arithmetic was wrong: 0.500 - 2(0.035) = 0.430, not 0.435).
288 # Corrected to the stated wall. The old values overstated flow area 2.3-3.3%, which is
289 # 4.7-6.7% of understated dp -- small, but this table exists to be the one place the
290 # near-miss cannot hide.
291 "3/8_TUBE_035": 0.0077470, # 0.375" OD - 2(0.035") = 0.305"
292 "1/2_TUBE_035": 0.0109220, # 0.500" OD - 2(0.035") = 0.430"
309class FeedSystemConfig(BaseModel):
310 """Feed system configuration for one branch (O or F).
312 The feed system is the PLUMBING between tank and injector — line bore plus lumped loss
313 coefficients. It sets how much tank pressure is spent getting propellant to the injector
314 face, so it directly moves the required tank pressure. It is vehicle hardware and is
315 deliberately independent of which propellant flows through it.
317 Give ``line_size`` (preferred — a name from FEED_LINE_SIZES) and the bore and area are
318 derived for you. ``d_inlet``/``A_hydraulic`` remain available for a non-standard passage,
319 and an explicitly-given area always wins over the derived one.
321 line_size: Optional[str] = Field(
324 "Named feed-line size, e.g. '3/8_NPT'. Sets d_inlet (and hence A_hydraulic) from "
325 f"the standard bore table. Valid: {', '.join(sorted(FEED_LINE_SIZES))}."
328 d_inlet: float = Field(
331 "Inlet flow-path diameter [m] — the actual bore, not a thread size. Normally derived "
332 "from line_size; set directly only for a non-standard passage."
335 A_hydraulic: float = Field(
337 description="Flow area of the feed line [m²]. Derived as πd²/4 from d_inlet when omitted.",
339 K0: float = Field(ge=0, description="Base loss coefficient")
340 supply_K: float = Field(
343 "The part of K0 that is the pressurant supply sagging under flow (regulator droop and "
344 "supply effect), not the line: it lowers the injector inlet exactly as K0 does, but at "
345 "chug frequencies it is the regulator's compliance (modelled separately in stability), so "
346 "the chug loop's feed resistance leaves it out. Written by Layer X's feed fit. 0: K0 is "
347 "all line, as before."
350 K1: float = Field(ge=0, description="Pressure dependence coefficient")
351 phi_type: Literal["none", "sqrtP", "logP"] = Field(
353 description="Pressure function type"
355 length: Optional[float] = Field(
359 "Feed-line length from tank outlet to injector manifold [m]. Sets the line inertance "
360 "(length / area) in the chug model. Leave unset and the stability model records a "
361 "0.305 m assumption instead of using it silently."
364 K_exit: float = Field(
368 "Loss where the line discharges into the injector manifold, in velocity heads of the "
369 "exit bore (d_exit). 1.0: a plenum takes the whole velocity head (Borda-Carnot, "
370 "(1 - A_exit/A_manifold)^2 -> 1; Crane TP-410 pipe exit K = 1.0). Set 0 only when K0 "
374 d_exit: Optional[float] = Field(
378 "Bore that discharges into the manifold [m], e.g. the 1/2 NPT fitting after a 1/2 in "
379 "tube run. None => the line itself (A_hydraulic)."
382 fittings: List[FeedFitting] = Field(
383 default_factory=list,
385 "Itemised valves, bends, tees, filters and fittings, each {name, K, source} with K on "
386 "the line velocity head. Summed into K_eff on both paths (K0 and roughness_m). Empty "
390 roughness_m: Optional[float] = Field(
394 "Absolute wall roughness [m] (drawn stainless tube 1.5e-6, Crane TP-410 A-23). When "
395 "set, K_eff = K_entrance + f L/d_inlet + sum(fittings) with Darcy f from Colebrook-"
396 "White at the actual Re = rho v d_inlet / mu of each call, and K0 is NOT used. None "
397 "=> K0 is the loss (the measured-override path), exactly as before. Assumes one "
398 "circular passage of bore d_inlet; requires length."
401 K_entrance: float = Field(
405 "Tank-to-line entrance loss, line velocity heads; used only when roughness_m is set. "
406 "0.5 = sharp-edged entrance (Crane TP-410 A-29)."
409 derived_from: Optional[Dict[str, Any]] = Field(
412 "Where K0 came from when it was fitted rather than typed: Layer X writes the feed-twin "
413 "drawing it burned, the run, the fitted line and supply terms, the flow they hold at, and "
414 "the K0 it replaced (engine/layerx/feedfit.py). Read by people, not by the physics."
418 @model_validator(mode="after")
419 def _roughness_needs_length(self):
420 if self.roughness_m is not None and self.length is None:
422 "feed_system: roughness_m is set but length is not -- the friction path needs "
423 "f L/D. Give the line length, or drop roughness_m to use K0."
427 @model_validator(mode="before")
429 def _resolve_line_size(cls, data):
430 """Fill d_inlet from line_size, and A_hydraulic from d_inlet, before validation.
432 d_inlet and A_hydraulic describe ONE passage two ways, and both used to be required and
433 independently editable, so nothing stopped them from disagreeing -- a stale area silently
434 outlived the diameter next to it. Deriving here means the pair cannot drift, while an
435 explicit A_hydraulic still wins for a genuinely non-circular passage.
437 if not isinstance(data, dict):
441 size = d.get("line_size")
443 key = str(size).strip().replace(" ", "_").replace("-", "_").upper()
444 bore = FEED_LINE_SIZES.get(key)
447 f"Unknown feed line_size {size!r}. Valid sizes: {', '.join(sorted(FEED_LINE_SIZES))}. "
448 f"For a non-standard passage give d_inlet directly instead."
451 if d.get("d_inlet") is None:
453 elif abs(float(d["d_inlet"]) - bore) > 1e-9:
455 f"feed line_size={key} implies d_inlet={bore:.7f} m but d_inlet="
456 f"{float(d['d_inlet']):.7f} m was also given. Drop one -- a named size and a "
457 f"contradicting bore is exactly the drift this field exists to prevent."
460 di = d.get("d_inlet")
461 if di is not None and d.get("A_hydraulic") is None:
462 d["A_hydraulic"] = float(np.pi) / 4.0 * float(di) ** 2
463 elif di is None and d.get("A_hydraulic") is not None:
464 # Area alone (twin line, non-circular passage): the equal-area bore.
465 d["d_inlet"] = float(np.sqrt(4.0 * float(d["A_hydraulic"]) / np.pi))
468 @model_validator(mode="after")
469 def _warn_area_diameter_mismatch(self):
470 """Flag an A_hydraulic that does not match its own d_inlet (>1% off)."""
471 implied = float(np.pi) / 4.0 * self.d_inlet ** 2
472 if implied > 0 and abs(self.A_hydraulic - implied) / implied > 0.01:
474 logging.getLogger(__name__).warning(
475 "feed_system: A_hydraulic=%.6e m2 disagrees with pi/4*d_inlet^2=%.6e m2 "
476 "(d_inlet=%.6f m, %.1f%% off). Intentional only for a non-circular passage; "
477 "otherwise omit A_hydraulic and let it derive.",
478 self.A_hydraulic, implied, self.d_inlet,
479 100.0 * (self.A_hydraulic - implied) / implied,
484class RegenCoolingConfig(BaseModel):
485 """Regenerative cooling channel configuration"""
486 enabled: bool = Field(default=False, description="Enable regen cooling model")
487 d_inlet: float = Field(gt=0, description="Inlet pipe diameter [m] (e.g., 3/8\" = 0.009525 m)")
488 L_inlet: float = Field(gt=0, description="Inlet pipe length [m]")
489 n_channels: int = Field(gt=0, description="Number of parallel cooling channels")
490 channel_width: float = Field(gt=0, description="Channel width [m]")
491 channel_height: float = Field(gt=0, description="Channel height [m]")
492 channel_length: float = Field(gt=0, description="Channel length [m] (typically chamber length)")
493 d_outlet: Optional[float] = Field(default=None, description="Outlet pipe diameter [m] (default: same as inlet)")
494 L_outlet: float = Field(gt=0, description="Outlet pipe length [m] (from merge to injector)")
495 roughness: float = Field(default=0.0, ge=0, description="Surface roughness [m] (0 = smooth)")
496 K_manifold_split: float = Field(default=0.5, ge=0, description="Manifold split loss coefficient")
497 K_manifold_merge: float = Field(default=0.3, ge=0, description="Manifold merge loss coefficient")
498 # Dynamic discharge coefficient configuration (similar to injector Cd)
499 Cd_entrance_inf: float = Field(default=0.8, gt=0, le=1, description="Asymptotic Cd at high Re for channel entrance")
500 a_Re_entrance: float = Field(default=0.1, ge=0, description="Reynolds correction parameter for entrance")
501 Cd_entrance_min: float = Field(default=0.6, ge=0, le=1, description="Minimum Cd for entrance")
502 Cd_exit_inf: float = Field(default=0.9, gt=0, le=1, description="Asymptotic Cd at high Re for channel exit")
503 a_Re_exit: float = Field(default=0.1, ge=0, description="Reynolds correction parameter for exit")
504 Cd_exit_min: float = Field(default=0.7, ge=0, le=1, description="Minimum Cd for exit")
505 # Heat-transfer coupling (Phase 2)
506 use_heat_transfer: bool = Field(default=False, description="Enable coupled heat-transfer calculations for regen cooling")
507 wall_thickness: float = Field(default=0.002, gt=0, description="Hot-wall thickness between gas and coolant [m]")
508 wall_thermal_conductivity: float = Field(default=300.0, gt=0, description="Wall material thermal conductivity [W/(m·K)]")
509 chamber_inner_diameter: Optional[float] = Field(default=None, gt=0, description="Chamber inner diameter for hot-side area [m]")
510 hot_gas_prandtl: float = Field(default=0.7, gt=0, description="Assumed hot-gas Prandtl number")
511 hot_gas_viscosity: float = Field(default=4.0e-5, gt=0, description="Effective hot-gas viscosity [Pa·s]")
512 hot_gas_thermal_conductivity: float = Field(default=0.1, gt=0, description="Effective hot-gas thermal conductivity [W/(m·K)]")
513 radiation_emissivity_hot: float = Field(default=0.8, ge=0, le=1, description="Effective hot-side emissivity for radiation")
514 radiation_view_factor: float = Field(default=1.0, ge=0, le=1, description="Radiation view factor to coolant surface")
515 n_segments: int = Field(default=20, gt=0, description="Number of axial segments for heat-transfer integration")
516 gas_turbulence_intensity: float = Field(default=0.1, ge=0, description="Estimated turbulence intensity of hot gas (0-1)")
517 coolant_turbulence_intensity: float = Field(default=0.05, ge=0, description="Estimated turbulence intensity of coolant (0-1)")
518 recovery_factor: Optional[float] = Field(default=None, gt=0, le=1, description="Turbulent boundary layer recovery factor for adiabatic wall temperature (Taw = Tc × recovery_factor). Typical range: 0.90-0.98. If None, uses default from constants.")
521class FilmCoolingConfig(BaseModel):
522 """Film cooling configuration"""
523 enabled: bool = Field(default=False, description="Enable film cooling model")
524 mass_fraction: float = Field(default=0.05, ge=0, le=0.5, description="Fraction of total mass flow used for film injection")
525 injection_temperature: Optional[float] = Field(default=None, gt=0, description="Film injection temperature [K] (defaults to fuel temperature)")
526 effectiveness_ref: float = Field(default=0.4, ge=0, le=1, description="Reference film effectiveness at injection location")
527 decay_length: float = Field(default=0.1, gt=0, description="Characteristic decay length for film effectiveness [m]")
528 apply_to_fraction_of_length: float = Field(default=1.0, gt=0, description="Portion of chamber length covered by film cooling")
529 slot_height: float = Field(default=3.0e-4, gt=0, description="Annular slot height for film injection [m]")
530 reference_blowing_ratio: float = Field(default=0.5, gt=0, description="Reference blowing ratio for effectiveness correlation")
531 blowing_exponent: float = Field(default=0.6, gt=0, description="Exponent on blowing ratio for effectiveness correlation")
532 turbulence_reference_intensity: float = Field(default=0.08, gt=0, description="Reference turbulence intensity for film erosion")
533 turbulence_sensitivity: float = Field(default=1.0, ge=0, description="Sensitivity of film effectiveness to turbulence intensity")
534 turbulence_exponent: float = Field(default=1.0, gt=0, description="Exponent governing turbulence erosion scaling")
535 turbulence_min_multiplier: float = Field(default=0.4, ge=0, le=1, description="Minimum multiplier applied to effectiveness due to turbulence erosion")
536 reference_wall_temperature: float = Field(default=1100.0, gt=0, description="Reference hot wall temperature used for heat-flux estimation [K]")
537 density_override: Optional[float] = Field(default=None, gt=0, description="Override density for film coolant if different from bulk fuel [kg/m³]")
538 cp_override: Optional[float] = Field(default=None, gt=0, description="Override specific heat for film coolant if different from bulk fuel [J/(kg·K)]")
549class GraphiteInsertConfig(BaseModel):
550 """Graphite throat insert configuration (separate from chamber ablator)"""
551 enabled: bool = Field(default=False, description="Enable graphite throat insert")
552 material_density: float = Field(default=1800.0, gt=0, description="Graphite BULK density [kg/m³], from the grade's datasheet (isomolded 1750-1900). Not the 2260 of a single crystal: recession is carbon mass flux over this density")
553 heat_of_ablation: float = Field(default=8.0e6, gt=0, description="Effective heat of ablation [J/kg] (graphite: ~8-12 MJ/kg)")
554 thermal_conductivity: float = Field(default=100.0, gt=0, description="Graphite thermal conductivity [W/(m·K)] (typical: 50-150)")
555 specific_heat: float = Field(default=710.0, gt=0, description="Graphite specific heat [J/(kg·K)]; the insert's heat capacity when specific_heat_model is 'constant'")
556 specific_heat_model: Literal["constant", "butland_maddison_1973"] = Field(
559 "How the insert's specific heat follows temperature (engine/pipeline/thermal/graphite_properties.py). "
560 "'constant' uses specific_heat at every temperature; 'butland_maddison_1973' is graphite's published "
561 "cp(T), 0.71 kJ/(kg K) at 300 K rising to 2.0 at 2000 K, the same for every polygranular grade."
564 material_source: Optional[str] = Field(
566 description="Where the insert's material numbers came from: grade, manufacturer, datasheet.",
568 initial_thickness: float = Field(default=0.005, gt=0, description="Initial graphite insert thickness [m]")
569 surface_temperature_limit: float = Field(default=2500.0, gt=0, description="Maximum surface temperature before failure [K]")
570 oxidation_temperature: float = Field(default=800.0, gt=0, description="Onset temperature for oxidation [K]")
571 oxidation_rate: float = Field(default=1e-6, ge=0, description="Unused by the species model (kept so old configs load)")
572 activation_energy: Optional[float] = Field(default=180e3, gt=0, description="Activation energy for Arrhenius oxidation rate [J/mol]. Typical: 150-200 kJ/mol")
573 oxidation_reference_temperature: float = Field(default=1500.0, gt=0, description="Reference temperature where oxidation_rate is defined [K]. Typical: 1500-1800 K")
574 oxidation_reference_pressure: float = Field(default=1.0e6, gt=0, description="Reference pressure where oxidation_rate is defined [Pa]. Typical: 1 MPa")
575 recession_multiplier: Optional[float] = Field(default=None, gt=0, description="Recession multiplier vs chamber (if None, calculated from flow conditions). Typically 1.3-2.5")
576 sizing_only_mode: bool = Field(default=False, description="If True, suppress recession for sizing iterations. Graphite does recede in reality; use only for design phase.")
577 simplified_graphite_oxidation: bool = Field(default=False, description="If True, use the constant `simplified_oxidation_rate` instead of instead of the physics-based model.")
578 simplified_oxidation_rate: float = Field(
582 "Radial recession rate used when `simplified_graphite_oxidation` is true [m/s]. "
583 "Default 1e-5 m/s = 0.01 mm/s, which was hardcoded. Measure your own stock and set "
584 "it here rather than inheriting a number from someone else's graphite."
587 sizing_recession_rate: float = Field(
591 "Recession rate assumed while SIZING the chamber [m/s]. Default 1e-8 is effectively "
592 "zero -- the sizing pass has always treated graphite as non-eroding. Raise it if your "
593 "insert measurably recedes over a burn and you want the bore sized for it."
596 axial_half_length_ratio: float = Field(
600 "Graphite insert axial half-length as a multiple of throat DIAMETER, used when "
601 "`axial_half_length` is unset. 0.75 was hardcoded in three geometry modules."
604 axial_half_length: Optional[float] = Field(
607 description="Explicit graphite insert axial half-length [m]. Overrides axial_half_length_ratio.",
609 char_layer_conductivity: float = Field(default=5.0, gt=0, description="Thermal conductivity of protective layer [W/(m·K)]")
610 char_layer_thickness: float = Field(default=0.0005, gt=0, description="Thickness of protective layer [m]")
611 coverage_fraction: float = Field(default=1.0, gt=0, le=1.0, description="Fraction of throat/nozzle with graphite insert")
612 emissivity: Optional[float] = Field(default=None, ge=0, le=1, description="Surface emissivity for radiation (default: 0.8)")
613 ambient_temperature: Optional[float] = Field(default=None, gt=0, description="Ambient temperature for radiation [K] (default: 300 K)")
614 feedback_fraction_min: Optional[float] = Field(default=None, ge=0, le=1, description="Minimum oxidation heat feedback fraction (default: 0.0)")
615 feedback_fraction_max: Optional[float] = Field(default=None, ge=0, le=1, description="Maximum oxidation heat feedback fraction (default: 0.2)")
616 oxidation_enthalpy: Optional[float] = Field(default=None, gt=0, description="Unused: reaction enthalpies are per species (graphite_cooling.CARBON_OXIDISERS)")
617 ablation_surface_temperature: Optional[float] = Field(default=None, gt=0, description="Surface temperature at which thermal ablation pins T_s [K] (default: 3000 K). Above this, T_s is fixed and m_dot_th balances energy.")
618 ablation_transition_width: float = Field(default=200.0, gt=0, description="Temperature width [K] for smooth transition to thermal ablation regime.")
619 oxidation_pressure_exponent: Optional[float] = Field(default=None, ge=0, description="Pressure exponent for oxidation kinetics (default: 0.5)")
620 oxidation_pre_exponential: Optional[float] = Field(default=None, gt=0, description="Pre-exponential factor for Arrhenius oxidation (default: calculated from oxidation_rate)")
621 mixture_mw: Optional[float] = Field(default=None, gt=0, description="Average molecular weight of combustion products [kg/mol] (default: 0.024)")
622 oxidation_stoichiometry_ratio: Optional[float] = Field(default=None, gt=0, description="Moles of C per mole of O2 (1.0 for CO2, 2.0 for CO) (default: 1.0)")
623 oxygen_mass_fraction: Optional[float] = Field(
627 description="Unused: the throat composition comes from CEA.",
629 oxygen_mole_fraction: Optional[float] = Field(
633 description="Unused: the throat composition comes from CEA.",
635 friction_coefficient_override: Optional[float] = Field(default=None, gt=0, description="Override skin friction coefficient Cf for blowing parameter calculation.")
636 reference_diffusivity: Optional[float] = Field(default=None, gt=0, description="Reference O2 diffusivity [m²/s] at reference temperature and pressure.")
637 reference_diffusivity_temperature: float = Field(default=1500.0, gt=0, description="Reference temperature [K] for oxygen diffusivity scaling.")
638 reference_diffusivity_pressure: float = Field(default=1.0e6, gt=0, description="Reference pressure [Pa] for oxygen diffusivity scaling.")
639 # Carbon oxidation by the throat gas (H2O, CO2, OH kinetic; O2, O diffusion-limited). Rates
640 # of Bradley et al. 1984, as tabulated by Thakre & Yang, J. Propulsion Power 24(4), 2008.
641 oxidation_H2O: SurfaceReactionConfig = Field(
642 default_factory=lambda: SurfaceReactionConfig(A=4.8e5, E=288.0e3, n=0.5),
643 description="C + H2O -> CO + H2 surface rate (Bradley 1984 via Thakre & Yang 2008)")
644 oxidation_CO2: SurfaceReactionConfig = Field(
645 default_factory=lambda: SurfaceReactionConfig(A=9.0e3, E=285.0e3, n=0.5),
646 description="C + CO2 -> 2 CO surface rate (Bradley 1984 via Thakre & Yang 2008)")
647 oxidation_OH: SurfaceReactionConfig = Field(
648 default_factory=lambda: SurfaceReactionConfig(A=361.0, E=0.0, n=1.0, T_exponent=-0.5),
649 description="C + OH -> CO + H surface rate (Bradley 1984 via Thakre & Yang 2008)")
663class AblativeCoolingConfig(BaseModel):
664 """Ablative cooling configuration for chamber liner (phenolic)"""
665 enabled: bool = Field(default=False, description="Enable ablative cooling model")
666 material_density: float = Field(default=1600.0, gt=0, description="Ablator (phenolic) density [kg/m³]")
667 heat_of_ablation: float = Field(default=2.5e6, gt=0, description="Heat of ablation [J/kg]: energy per kg consumed beyond the sensible heat from ambient_temperature to ablation_surface_temperature (quasi-steady Landau balance)")
668 thermal_conductivity: float = Field(default=0.35, gt=0, description="Ablator (phenolic) thermal conductivity [W/(m·K)]")
669 specific_heat: float = Field(default=1500.0, gt=0, description="Ablator (phenolic) specific heat [J/(kg·K)]")
670 initial_thickness: float = Field(default=0.01, gt=0, description="Initial ablative (phenolic) thickness [m]")
671 surface_temperature_limit: float = Field(default=1200.0, gt=0, description="Allowable surface temperature [K]. A check only; the thermal model runs at ablation_surface_temperature")
672 ablation_surface_temperature: float = Field(default=1986.0, gt=0, description="Char surface temperature while the liner ablates [K]. Default 1986 K is the SiO2 melting point (CRC Handbook), the bound on a silica-phenolic char surface; set it for the reinforcement actually used")
673 coverage_fraction: float = Field(default=1.0, gt=0, le=1.0, description="Fraction of chamber surface protected by ablative liner")
674 pyrolysis_temperature: float = Field(default=900.0, gt=0, description="Characteristic pyrolysis temperature of ablator [K]")
675 blowing_efficiency: float = Field(default=0.8, ge=0, le=1, description="Effectiveness of ablative gases in blocking convective heat flux (legacy constant factor, used if use_physics_based_blowing=False)")
676 use_physics_based_blowing: bool = Field(default=True, description="If True, use physics-based blowing parameter B = m_dot_pyrolysis/m_dot_external. If False, use constant blowing_efficiency factor.")
677 blowing_coefficient: float = Field(default=0.5, gt=0, description="Blowing coefficient c in empirical function f(B) = 1/(1 + c*B). Typical range: 0.3-0.8. Higher values = stronger blowing effect.")
678 blowing_min_reduction_factor: float = Field(default=0.1, ge=0, le=1, description="Minimum convective reduction factor (maximum blowing effectiveness). Prevents blowing from reducing convective heat transfer below this fraction. Default 0.1 means maximum 90% reduction. Lower values allow stronger blowing effect.")
679 turbulence_reference_intensity: float = Field(default=0.08, gt=0, description="Unused: Bartz already carries the chamber's turbulence (kept so old configs load)")
680 turbulence_sensitivity: float = Field(default=1.5, ge=0, description="Unused: Bartz already carries the chamber's turbulence (kept so old configs load)")
681 turbulence_exponent: float = Field(default=1.0, gt=0, description="Unused: Bartz already carries the chamber's turbulence (kept so old configs load)")
682 turbulence_max_multiplier: float = Field(default=3.0, gt=0, description="Unused: Bartz already carries the chamber's turbulence (kept so old configs load)")
683 throat_recession_multiplier: Optional[float] = Field(default=None, gt=0, description="Throat recession multiplier vs chamber (if None, calculated from flow conditions). Typically 1.2-2.0")
684 char_layer_conductivity: float = Field(default=0.2, gt=0, description="Thermal conductivity of char layer [W/(m·K)]")
685 char_layer_thickness: float = Field(default=0.001, gt=0, description="Thickness of protective char layer [m]")
686 surface_emissivity: float = Field(default=0.85, ge=0, le=1, description="Surface emissivity for radiative heat transfer (0-1, typical 0.8-0.9 for charred ablators)")
687 ambient_temperature: float = Field(default=300.0, gt=0, description="Liner temperature at ignition [K] (the wall starts at ambient)")
688 radiative_sink_minimum_threshold: float = Field(default=400.0, gt=0, description="Unused: the hot face radiates only to the gas (kept so old configs load)")
689 radiative_sink_fallback_temperature: float = Field(default=600.0, gt=0, description="Unused: the hot face radiates only to the gas (kept so old configs load)")
690 track_geometry_evolution: bool = Field(default=True, description="Enable time-varying geometry tracking (L* evolution)")
691 nozzle_ablative: bool = Field(default=False, description="If True, nozzle exit also recedes (A_exit grows). If False, only throat recedes (expansion ratio decreases)")
694class DischargeConfig(BaseModel):
695 """Discharge coefficient configuration"""
696 Cd_inf: float = Field(
700 "Baseline Cd at infinite Re for the reference orifice diameter d_ref_m "
701 "(thin-plate sharp hole, typically 0.60 for machined impinging jets)."
704 a_Re: float = Field(ge=0, description="Reynolds number correction parameter")
705 Cd_min: float = Field(default=0.2, ge=0, le=1, description="Minimum Cd")
706 use_geometry_cd: bool = Field(
709 "When True, Cd_inf is scaled from jet/orifice diameter d_hyd via cd_inf_from_orifice_diameter "
710 "before the Re correction (impinging jets; pintle orifice OD / fuel gap hydraulic diameter)."
713 d_ref_m: float = Field(
716 description="Reference hole diameter [m] where Cd_inf equals the Cd_inf baseline (default 2 mm).",
718 cd_small_hole_exponent: float = Field(
723 "For d < d_ref: Cd_inf scales as Cd_inf * (d/d_ref)^exponent. 0 (default): the thin-plate "
724 "asymptote holds below d_ref; ISO 5167 and Sutton & Biblarz Table 8-2 have small sharp "
725 "holes flowing slightly more, not less, so the old 0.2 penalty had the wrong sign."
728 cd_large_hole_log_gain: float = Field(
731 description="For d > d_ref: Cd_inf += gain * ln(d/d_ref), capped at cd_inf_max.",
733 cd_inf_max: float = Field(
737 description="Upper cap on geometry-based Cd_inf (slightly radiused / large-hole asymptote).",
739 cd_inf_min_geom: float = Field(
743 description="Lower floor on geometry-based Cd_inf for very small holes.",
745 # --- Orifice inlet geometry: the real, per-orifice Cd knob -------------------------
746 # Cd is set by what the INLET EDGE looks like and by L/d, not by hole diameter. Because
747 # discharge.oxidizer and discharge.fuel are separate blocks, filleting only ONE side's
748 # inlet raises that propellant's Cd and moves the momentum ratio / O/F without touching
749 # hole size, element count or angles. One extra machining op, one tuning knob.
750 # Both default to None => the existing diameter-based Cd_inf path is unchanged.
751 inlet_geometry: Optional[str] = Field(
754 "Orifice inlet treatment: sharp (0.80) | chamfered (0.84) | conical (0.86) | "
755 "rounded_light (0.85) | rounded (0.88) | bellmouth (0.95). Short-tube values at "
756 "Re > 1e4 and L/d ~ 2-5 (engine.core.discharge.INLET_GEOMETRY_CD); the thin-plate "
757 "0.61 is recovered by orifice_l_over_d -> 0. Overrides the "
758 "diameter-scaled Cd_inf when set. Use inlet_radius_ratio instead for a "
762 inlet_radius_ratio: Optional[float] = Field(
767 "Inlet fillet radius / orifice diameter (r/d). Continuous alternative to "
768 "inlet_geometry: 0 = sharp (Cd 0.80 short tube), 0.125 = Cd 0.88, >= 0.2 = Cd 0.95 and "
769 "saturating. Nurick (1976): cavitation inception margin also rises with inlet "
770 "roundness, so this buys flow AND cavitation headroom."
773 orifice_l_over_d: Optional[float] = Field(
777 "Orifice length / diameter. Lichtarowicz et al. (1965): Cd peaks near L/d ~ 2 "
778 "where expansion past the vena contracta recovers dynamic pressure, falls below "
779 "~1 (thin-plate, no recovery) and above ~10 (wall friction). Only applied when "
780 "an inlet geometry is set. None => no length correction. With "
781 "l_over_d_source='plate' this is the LAND the counterbore leaves, and the L/d the "
782 "Cd model uses is what the plate actually gives."
785 l_over_d_source: Literal["declared", "plate"] = Field(
788 "Where the orifice L/d comes from. 'declared' (default, previous behaviour): "
789 "orifice_l_over_d as typed. 'plate': derived from the injector plate -- "
790 "layer1_injector_plate_thickness_m and layer1_injector_counterbore_dia_m -- for the "
791 "current hole: with a counterbore the land is min(orifice_l_over_d * d, t/cos theta); "
792 "without one the small drill runs the whole passage, L/d = t / (d cos theta). Also "
793 "applies the counterbore's velocity of approach and entrance loss (approach_beta)."
796 length_model: Literal["piecewise", "lichtarowicz"] = Field(
797 default="lichtarowicz",
799 "Cd vs L/d. 'lichtarowicz' (default): the published sharp-inlet fit Cd_u = 0.827 - "
800 "0.0085 L/d (Lichtarowicz, Duggins & Markland 1965, valid 2 <= L/d <= 10), "
801 "normalised so the inlet table's short-tube value sits at L/d 3.18. 'piecewise' "
802 "(legacy, unsourced): flat over L/d 2-5, linear roll-off above."
805 approach_beta: Optional[float] = Field(
806 default=None, gt=0.0, lt=1.0,
808 "Orifice / feed-passage diameter ratio d/D when the orifice is fed from a "
809 "counterbore, not a plenum. Applies the velocity of approach and a sharp entrance "
810 "(K = 0.5, Idelchik) into the counterbore: Cd_eff = 1/sqrt((1-b^4)/Cd^2 + "
811 "b^4 (1+K)). Set by l_over_d_source='plate'; None => fed from a plenum."
814 d_min_m: float = Field(
817 description="Minimum diameter used in Cd_geom [m] (below this, clamp to d_min_m).",
819 # Pressure and temperature dependence (optional)
820 use_pressure_correction: bool = Field(default=False, description="Enable pressure-dependent Cd (compressibility effects)")
821 P_ref: float = Field(default=5.0e6, gt=0, description="Reference pressure for pressure correction [Pa]")
822 a_P: float = Field(default=0.0, description="Pressure correction coefficient")
823 use_temperature_correction: bool = Field(default=False, description="Enable temperature-dependent Cd (viscosity effects)")
824 T_ref: float = Field(default=300.0, gt=0, description="Reference temperature for temperature correction [K]")
825 a_T: float = Field(default=0.0, description="Temperature correction coefficient")
835class SMDConfig(BaseModel):
836 """Sauter Mean Diameter configuration"""
837 model: Literal["lefebvre", "nukiyama_tanasawa", "ingebo"] = Field(
840 "SMD model type. Impinging doublets always use NACA TN 4222 (Ingebo 1958) per stream; "
841 "'lefebvre' is the legacy We^-m·(1+Oh)^p form used by the pintle path."
844 C: float = Field(default=0.5, gt=0, description="Lefebvre constant C")
845 m: float = Field(default=0.6, gt=0, description="Lefebvre exponent m")
846 p: float = Field(default=0.0, description="Lefebvre exponent p (viscous term (1+Oh)^p)")
847 C_ingebo: Optional[float] = Field(
849 description="[DEPRECATED — no effect] Prefactor of the former SMD function. Impinging D32 is NACA TN 4222; calibrate with smd_scale.",
851 d32_measured_O: Optional[float] = Field(default=None, gt=0,
852 description="Measured oxidizer D32 [m]; set from measurements.d32_O_um. Held at this value.")
853 d32_measured_F: Optional[float] = Field(default=None, gt=0,
854 description="Measured fuel D32 [m]; set from measurements.d32_F_um. Held at this value.")
855 smd_scale: float = Field(
859 "Multiplier on the impinging-jet D32 of NACA TN 4222 (Ingebo 1958). 1.0 = the "
860 "correlation; set it from a measured D32 (PDPA / laser diffraction cold flow)."
863 smd_property_scaling: bool = Field(
866 "Carry TN 4222 (n-heptane) to the actual liquid and gas with Ingebo's TN 4087 exponents: "
867 "D ~ (sigma mu_l / rho_l)^(1/4) rho_gas^(-1/4). Off = heptane in air as measured."
870 smd_property_transfer: Literal["tn4087", "dombrowski_johns", "none"] = Field(
873 "How TN 4222's n-heptane/air D32 is carried to the real liquid and gas (used when "
874 "smd_property_scaling is on; spray.tn4222_property_factor). 'tn4087': Ingebo's crossflow "
875 "exponents, (sigma mu_l/rho_l)^(1/4) rho_g^(-1/4). 'dombrowski_johns': inviscid "
876 "sheet-breakup (Dombrowski & Johns 1963), (sigma^2/(rho_g rho_l))^(1/6). 'none': heptane "
877 "in air. An unverified transfer either way: on the 6.5 kN ethalox doublet the three "
878 "span eta_vap 0.961-0.989 (injector audit 2026-09-28). Measure D32 and set smd_scale."
881 chamber_gas_R: float = Field(
885 "Representative specific gas constant [J/(kg·K)] of the combustion gas, used only to "
886 "estimate the chamber gas density (rho_g = Pc/(R·T)) for the aerodynamic Weber number "
887 "in the Ingebo correlation. The injector solve runs before CEA chamber state is "
888 "available, so this is a configured representative value (LOX/CH4 ≈ 360)."
891 chamber_gas_T: float = Field(
895 "Representative combustion-gas temperature [K] for the chamber gas density estimate "
896 "rho_g = Pc/(R·T) used in the Ingebo aerodynamic Weber number (LOX/CH4 ≈ 3500)."
899 we_corr_max: Optional[float] = Field(
903 "Optional cap applied to the liquid Weber number **only** when evaluating the "
904 "Lefebvre-style ``smd_lefebvre`` correlation (D32). Full We is still used for "
905 "``check_spray_constraints``. Jet-like impingement can produce enormous We with "
906 "liquid ρ and mm jets, which drives D32 to sub-micron values outside the correlation's "
907 "useful regime; capping We_corr is a pragmatic surrogate for secondary breakup / "
908 "turbulence limits. Default None = no cap."
1005class CombustionEfficiencyConfig(BaseModel):
1006 """c* efficiency: eta_c* = eta_vap * eta_mix * eta_HL (engine/pipeline/combustion_physics.py,
1007 combustion_eff.py). Chemical kinetics carry no chamber c* loss."""
1008 model: Literal["constant", "linear", "exponential"] = Field(
1009 default="exponential",
1010 description="Vaporization term. exponential (legacy name): the Priem-Heidmann spray march, "
1011 "eta_vap = fraction vaporized at the throat. constant: eta_vap = 1 - C. "
1012 "linear: eta_vap = 1 - C (1 - L*/1 m)."
1014 C: float = Field(default=0.3, ge=0, le=1, description="Vaporization loss for the constant and linear models only.")
1015 K: float = Field(default=0.15, ge=0, description="[DEPRECATED — no effect] Read by no model.")
1016 use_spray_correction: bool = Field(default=False, description="[DEPRECATED — no effect] Read by no model.")
1017 spray_penalty_factor: float = Field(default=0.8, ge=0, le=1, description="[DEPRECATED — no effect] Read by no model.")
1018 use_mixture_coupling: bool = Field(
1020 description="[DIAGNOSTICS ONLY] Enable mixture diagnostics logging (does NOT affect efficiency calculation)"
1022 use_cooling_coupling: bool = Field(
1024 description="Charge heat lost through the chamber wall to c*: eta_HL = sqrt(1 - Q/(mdot cp Tc)) "
1025 "(c* ~ sqrt(T0), Sutton eq. 3-32). Regenerative and film heat stay with the propellant."
1027 use_turbulence_coupling: bool = Field(
1029 description="[DEPRECATED] Turbulence is folded into eta_mixing; the standalone eta_turbulence penalty was removed (non-physical/double-counted)."
1031 # --- Mixing: eta_mix = sum_i w_i c*(O/F_i) / c*(O/F) over stream tubes whose O/F spread
1032 # reproduces Rupe's mixing factor E_m (combustion_physics.stream_tube_mixing_efficiency).
1033 # M = rho_O v_O^2 d_O / (rho_F v_F^2 d_F), Elverum & Morey (JPL Memo 30-5) eq. 1.
1034 Em_peak: float = Field(
1035 default=0.96, ge=0.5, le=1.0,
1036 description="[DEPRECATED — no effect] Was an assumed c* mixing efficiency; E_m is not a c* "
1037 "efficiency. Mixing is now rupe_Em_opt through the stream-tube c* integral."
1039 mixing_sigma: float = Field(
1040 default=1.5, gt=0.0,
1041 description="[DEPRECATED — no effect] Width of the retired log-Gaussian mixing falloff; the "
1042 "falloff is Rupe's correlation (rupe_Em_curvature)."
1044 rupe_Em_opt: float = Field(
1045 default=0.80, gt=0.0, le=1.0,
1046 description="Rupe mixing factor E_m (cold-flow mixture-ratio uniformity, 0-1; JPL TR 32-1546 "
1047 "eq. 1) of the element at its optimum M. Circular 1-on-1 unlike doublets with "
1048 "fully developed jets: 0.75-0.85 (Rupe, as summarized in Hoehn, Rupe & Sotter, "
1049 "JPL TR 32-1546, 1972, p. 1); Rupe's correlation peaks at 0.82 and JPL's "
1050 "noncavitating orifices at 0.815 (ibid. p. 10); short or cavitating orifices "
1051 "0.70 (Nurick & McHale, ibid.). 0.80 is the middle of the band: replace it with "
1052 "a cold-flow measurement of the element."
1054 rupe_Em_curvature: float = Field(
1055 default=6.07, ge=0.0,
1056 description="Fall-off of E_m away from the optimum, E_m = rupe_Em_opt (1 - a (N_R - N_opt)^2), "
1057 "N_R = M/(1+M): a = 6.07 fitted to Rupe's mean line digitized from JPL TR 32-1546 "
1058 "Fig. 1 (N_R 0.26-0.78, within 2.4 points; data scatter +-5)."
1060 mixing_distribution: Literal["gaussian", "two_tube"] = Field(
1062 description="Shape of the O/F spread that reproduces E_m in the stream-tube c* integral: "
1063 "normal in oxidizer mass fraction (default) or two equal-mass tubes. The other "
1064 "shape's eta_mix is reported beside it; at E_m 0.8 on LOX/ethanol they differ "
1065 "by ~1.3 points (two tubes lose less)."
1067 droplet_blowing_model: Literal["abramzon_sirignano", "none"] = Field(
1068 default="abramzon_sirignano",
1069 description="Stefan-flow corrections in the vaporization march while a drop evaporates: "
1070 "Nu* = 2 + (Nu0 - 2)/F(B) (Abramzon & Sirignano 1989) and C_D (1+B)^-0.2 "
1071 "(Yuen & Chen 1976). 'none' = uncorrected Ranz-Marshall and solid-sphere drag, "
1072 "which do not hold at the transfer numbers here (B_T 16-80)."
1074 rupe_M_opt: float = Field(
1075 default=1.0, gt=0.0,
1076 description="Elverum-Morey mixing parameter at which mixing is best: 1.0 for a 1-on-1 unlike "
1077 "doublet (SP-8089 Table IV; 2-on-1 1.6, 3-on-1 3.5)."
1079 R_opt: Optional[float] = Field(
1081 description="[DEPRECATED — no effect] The mixing optimum is rupe_M_opt; the resultant-tilt "
1082 "momentum ratio no longer enters c*."
1084 spray_size_spread_q: float = Field(
1085 default=3.0, gt=1.0,
1086 description="ASSUMED Rosin-Rammler spread q of each stream's drop sizes (volume basis, "
1087 "X = D32 Gamma(1 - 1/q)) in the vaporization march. Lefebvre: 1.5-4 for most "
1088 "sprays; larger q is a narrower spray."
1090 mixture_efficiency_floor: float = Field(default=0.25, ge=0, le=1, description="[DEPRECATED] No longer used")
1091 cooling_efficiency_floor: float = Field(
1092 default=0.25, ge=0, le=1,
1093 description="Floor on the chamber solver's legacy cooling_efficiency diagnostic only; the c* "
1094 "heat-loss factor has none."
1096 turbulence_efficiency_floor: float = Field(default=0.3, ge=0, le=1, description="[DEPRECATED] No longer used")
1097 target_turbulence_intensity: Optional[float] = Field(
1099 description="[DEPRECATED] Design target only, not used in efficiency calculation"
1101 turbulence_penalty_exponent: Optional[float] = Field(
1103 description="[DEPRECATED] No longer used"
1105 target_smd_microns: Optional[float] = Field(
1107 description="[DEPRECATED] Design target only, not used in efficiency calculation"
1109 xstar_limit_mm: Optional[float] = Field(
1111 description="[DEPRECATED] No longer used"
1113 xstar_penalty_exponent: Optional[float] = Field(
1115 description="[DEPRECATED] No longer used"
1117 we_reference: Optional[float] = Field(
1119 description="[DEPRECATED] Design target only, not used in efficiency calculation"
1121 we_penalty_exponent: Optional[float] = Field(
1123 description="[DEPRECATED] No longer used"
1125 smd_penalty_exponent: Optional[float] = Field(
1127 description="[DEPRECATED] No longer used"
1129 use_advanced_model: bool = Field(
1131 description="Lets the numba accelerator run the chamber solve (engine/accel can_handle_chamber). "
1132 "The physics is the same either way."
1134 Pc_gate: float = Field(
1137 description="[DEPRECATED — no effect] Read by no model."
1139 use_finite_rate_chemistry: bool = Field(
1141 description="[DEPRECATED — no effect] Chamber kinetics carry no c* loss (products relax in "
1142 "~1 us against a ~1 ms stay time); nozzle kinetics are bracketed by CEA's Cf."
1144 use_shifting_equilibrium: bool = Field(
1146 description="[DEPRECATED — no effect] The iterative shifting-equilibrium nozzle was retired; "
1147 "exit-composition shift is captured exactly by CEA's Cf_vac table "
1148 "(RPA delivered thrust). Kept for config compatibility only."
1150 # Retired RP-1 kinetics surrogate (tau_chem switched on O/F plateaus); read by no model.
1151 tau_ref: float = Field(default=1e-5, gt=0, description="[DEPRECATED — no effect] Retired kinetics surrogate [s].")
1152 tau_ref_P: float = Field(default=4.0e6, gt=0, description="[DEPRECATED — no effect] Retired kinetics surrogate [Pa].")
1153 tau_ref_T: float = Field(default=3500.0, gt=0, description="[DEPRECATED — no effect] Retired kinetics surrogate [K].")
1154 n_pressure: float = Field(default=0.8, ge=0, description="[DEPRECATED — no effect] Retired kinetics surrogate.")
1155 tau_Tc_floor_K: Optional[float] = Field(default=None, description="[DEPRECATED — no effect] Retired kinetics surrogate [K].")
1156 T_star_fuel_cap_K: float = Field(
1159 description="[DEPRECATED — no effect] Drops now evaporate at their saturation temperature at Pc "
1160 "(Clausius-Clapeyron from fluids.*.boiling_point) [K]."
1162 # Arrhenius kinetics parameters (fuel-specific, can be overridden per fuel type)
1163 A0_hydrocarbon: float = Field(
1166 description="[Progress diagnostic only: reaction_chemistry, time-varying solver; no effect on c*] Pre-exponential factor for hydrocarbon fuels (RP-1, Kerosene) [1/s]. Default 1e7."
1168 Ea_hydrocarbon: float = Field(
1171 description="[Progress diagnostic only: reaction_chemistry, time-varying solver; no effect on c*] Activation energy for hydrocarbon fuels (RP-1, Kerosene) [J/mol]. Default 80 kJ/mol."
1173 n_pre_hydrocarbon: float = Field(
1176 description="[Progress diagnostic only: reaction_chemistry, time-varying solver; no effect on c*] Pre-exponential pressure exponent for hydrocarbons. Default 0.3."
1178 A0_ethanol: float = Field(
1181 description="[Progress diagnostic only: reaction_chemistry, time-varying solver; no effect on c*] Pre-exponential factor for ethanol [1/s]. Default 5e7 (faster than RP-1)."
1183 Ea_ethanol: float = Field(
1186 description="[Progress diagnostic only: reaction_chemistry, time-varying solver; no effect on c*] Activation energy for ethanol [J/mol]. Default 140 kJ/mol (higher than RP-1)."
1188 n_pre_ethanol: float = Field(
1191 description="[Progress diagnostic only: reaction_chemistry, time-varying solver; no effect on c*] Pre-exponential pressure exponent for ethanol. Default 0.25."
1193 A0_hydrogen: float = Field(
1196 description="[Progress diagnostic only: reaction_chemistry, time-varying solver; no effect on c*] Pre-exponential factor for hydrogen [1/s]. Default 1e9 (much faster than hydrocarbons)."
1198 Ea_hydrogen: float = Field(
1201 description="[Progress diagnostic only: reaction_chemistry, time-varying solver; no effect on c*] Activation energy for hydrogen [J/mol]. Default 40 kJ/mol (lower than hydrocarbons)."
1203 n_pre_hydrogen: float = Field(
1206 description="[Progress diagnostic only: reaction_chemistry, time-varying solver; no effect on c*] Pre-exponential pressure exponent for hydrogen. Default 0.2."
1216class StabilityConfig(BaseModel):
1217 """Inputs to the combustion / feed stability model that belong to neither the propellant
1218 (``fluids``) nor the plumbing (``feed_system``): the combustion-response calibration, the
1219 nozzle-entrance Mach the acoustic damping uses, the acoustic damping coefficients, and the
1220 dome-regulator dynamics. A ``None`` here means "derive it" and the derivation is recorded in
1221 the assumptions registry (rich report -> assumptions.fallbacks_used), never substituted silently.
1223 n_interaction: float = Field(
1225 description="Crocco interaction index n (calibration range 0.3-0.6). The forward-mode slider overrides it per run.",
1227 chi_acoustic: float = Field(
1228 default=0.15, gt=0, le=1,
1229 description="Sensitive-lag fraction chi: tau_sens = chi * tau_vap for the acoustic n-tau driving.",
1231 mach_nozzle_entrance: Optional[float] = Field(
1232 default=None, gt=0, lt=1,
1233 description="Mean Mach at the nozzle entrance (sets nozzle damping). None = solve it from the contraction ratio (isentropic, subsonic).",
1235 damping_injector_frac: float = Field(
1237 description="Injector-face acoustic damping as a fraction of pi*f [-]. ASSUMED, uncalibrated (no source); calibrate against a cold ring-down test (T7).",
1239 damping_twophase_frac: float = Field(
1241 description="Two-phase (droplet) acoustic damping as a fraction of pi*f*droplet_loading [-]. ASSUMED, uncalibrated (no source).",
1243 droplet_loading: float = Field(
1245 description="Relative liquid loading near the injector face for the two-phase damping term [-].",
1247 acoustic_gate_alpha_offset: float = Field(
1248 default=350.0, ge=0,
1250 "UNUSED. Was an allowance that let a mode growing at up to this rate [1/s] pass the old "
1251 "tanh-remapped acoustic gate. The acoustic margin is now damping/driving (1 = neutral) and "
1252 "stability.acoustic_gate decides whether it gates. Kept so older configs still load."
1255 acoustic_gate: Literal["report_only", "nominal_phase", "worst_phase"] = Field(
1256 default="report_only",
1258 "Which acoustic (HF) margin reaches the stability gate. 'report_only': none -- the modes, "
1259 "damping budget and margins are reported but gate nothing, because the injector-face and "
1260 "two-phase damping are uncalibrated and omega*tau_sens spans many periods of 2*pi (Harrje & "
1261 "Reardon SP-194 rate HF stability by test). 'nominal_phase': damping/driving at the model's "
1262 "tau_sens. 'worst_phase': damping/driving at omega*tau = pi (Crocco's n_min/n)."
1265 time_lag_model: Literal["leonardi_dtl", "d2_law"] = Field(
1266 default="leonardi_dtl",
1268 "Named model for the injection->heat-release conversion lag that drives the chug loop. "
1269 "'leonardi_dtl' is the double-time-lag decomposition tau_atom + tau_vap + tau_mix of "
1270 "Leonardi et al., Acta Astronautica 139 (2017); 'd2_law' is STAR's historical quiescent "
1271 "droplet lifetime, kept for reproducing older results. See "
1272 "scripts/chug_timelag_benchmark.py for the measurement that chose the default."
1275 convection_model: Literal["none", "leonardi_eq8", "ranz_marshall"] = Field(
1278 "Convective speed-up applied to the droplet lifetime. Default 'none': the Leonardi eq. 9 "
1279 "evaporation constant is already calibrated against real chamber data, and applying "
1280 "eq. 8 on top of it triples the chug-frequency error against the reference experiment."
1283 mixing_lag_fraction: float = Field(
1284 default=0.5, ge=0.0, le=3.0,
1286 "Mixing lag as a fraction of the rate-limiting vaporization lag, shared by every stream "
1287 "(a gaseous propellant carries this and nothing else). Default 0.5 is Leonardi's own "
1288 "calibration for the validation engine: tau_mix = 2.2 ms at tau_vap = 4.4 ms. Set 0 to "
1289 "drop the mixing lag entirely."
1292 regulator_enabled: bool = Field(default=True, description="Model the dome regulator upstream of each tank in the chug loop.")
1293 regulator_corner_hz: float = Field(default=3.0, gt=0, description="Regulator response corner frequency [Hz].")
1294 chug_band_mixing_lag_fraction_min: Optional[float] = Field(
1295 default=0.0, ge=0.0, le=3.0,
1297 "Low end of the mixing-lag fraction band the chug gate must hold over. The fraction's only "
1298 "source is Leonardi 2017's GH2/LOX coaxial rig (0.5); nothing measures it for this "
1299 "propellant or injector, so the gate takes the lowest gain margin over [min, max]. Set both "
1300 "ends null to gate on mixing_lag_fraction alone once it is measured. leonardi_dtl only."
1303 chug_band_mixing_lag_fraction_max: Optional[float] = Field(
1304 default=1.0, ge=0.0, le=3.0,
1305 description="High end of the chug gate's mixing-lag fraction band (see the _min field).",
1307 regulator_Z_hf: float = Field(
1310 "Regulator high-frequency series impedance [Pa*s/kg]. 0 = regulator NOT MODELLED: the feed "
1311 "sees an ideal pressure source, and the report says so instead of printing a with/without "
1312 "pair. Set it only from a measured step response (T6)."
1315 regulator_max_excursion_psi: float = Field(
1317 description="Regulator outlet pressure excursion bound [psi]. Reporting only; not a pole-shifter.",
1321class ChamberGeometryConfig(BaseModel):
1323 Unified chamber geometry configuration for solve_chamber_geometry_with_cea.
1324 Groups all design inputs (chamber + nozzle) in one place.
1326 # Design requirements
1327 design_pressure: float = Field(gt=0, description="Target chamber pressure Pc [Pa]")
1328 design_thrust: float = Field(gt=0, description="Target thrust F [N]")
1329 design_MR: float = Field(gt=0, description="Design mixture ratio O/F")
1331 # Chamber dimensions
1332 chamber_diameter: float = Field(gt=0, description="Inner chamber diameter [m]")
1333 Lstar: float = Field(gt=0, description="Characteristic length [m] (0.95-1.27 for LOX/RP-1)")
1336 exit_diameter: float = Field(gt=0, description="Nozzle exit diameter [m]")
1337 expansion_ratio: float = Field(gt=1, description="Area ratio A_exit/A_throat")
1338 nozzle_efficiency: float = Field(default=0.95, ge=0, le=1, description="Nozzle efficiency (0.94-0.98)")
1340 # Solver outputs (populated after running solver)
1341 A_throat: Optional[float] = Field(default=None, gt=0, description="Throat area [m²] - SOLVED")
1342 A_exit: Optional[float] = Field(default=None, gt=0, description="Exit area [m²] - SOLVED")
1343 volume: Optional[float] = Field(default=None, gt=0, description="Chamber volume [m³] - SOLVED")
1344 length: Optional[float] = Field(default=None, gt=0, description="Total chamber length [m] - SOLVED")
1345 length_cylindrical: Optional[float] = Field(default=None, gt=0, description="Cylindrical section length [m] - SOLVED")
1346 length_contraction: Optional[float] = Field(default=None, gt=0, description="Contraction section length [m] - SOLVED")
1347 Cf: Optional[float] = Field(default=None, gt=0, description="Thrust coefficient - SOLVED")
1488class RocketConfig(BaseModel):
1489 """Rocket configuration for flight simulation.
1491 NEW Mass Model (recommended):
1492 - airframe_mass: Rocket body without propulsion (fuselage, fins, nosecone, avionics, payload)
1493 - engine_mass: Engine + ALL plumbing (chamber, nozzle, injector, valves, fittings, lines)
1494 - lox_tank_structure_mass: Empty LOX tank only (walls, no fittings)
1495 - fuel_tank_structure_mass: Empty fuel tank only (walls, no fittings)
1496 - copv_dry_mass: Empty COPV tank only (walls, no pressurant gas)
1497 - engine_cm_offset: Height of engine CM above nozzle exit
1498 - rocket_length: Total rocket length (tail to nose tip) for MoI estimation
1500 propulsion_dry_mass and propulsion_cm_offset are COMPUTED from the above.
1502 Total dry mass = airframe_mass + engine_mass + lox_tank_structure_mass + fuel_tank_structure_mass + copv_dry_mass
1503 Total wet mass = dry mass + lox_tank.mass + fuel_tank.mass (propellants) + press_tank.initial_gas_mass
1505 LEGACY fields (mass, motor.dry_mass) still supported for backward compatibility.
1507 # NEW detailed mass model
1508 airframe_mass: Optional[float] = Field(default=None, gt=0, description="Airframe mass (fuselage, fins, nosecone, avionics, payload) - NO propulsion [kg]")
1509 engine_mass: Optional[float] = Field(default=None, gt=0, description="Engine + plumbing mass (chamber, nozzle, injector, valves, ALL fittings & lines) [kg]")
1510 lox_tank_structure_mass: Optional[float] = Field(default=None, gt=0, description="Empty LOX tank only (walls, no fittings) [kg]")
1511 fuel_tank_structure_mass: Optional[float] = Field(default=None, gt=0, description="Empty fuel tank only (walls, no fittings) [kg]")
1512 engine_cm_offset: float = Field(default=0.15, ge=0, description="Height of engine+plumbing CM above nozzle exit [m]. Typical: 0.1-0.3m.")
1514 # COMPUTED fields (calculated from detailed breakdown)
1515 propulsion_dry_mass: Optional[float] = Field(default=None, gt=0, description="COMPUTED: Total propulsion dry mass (engine + tanks) [kg]")
1516 propulsion_cm_offset: float = Field(default=0.3, description="COMPUTED: Propulsion system CM above nozzle exit [m]")
1518 # COPV tank structure mass (for blowdown systems)
1519 copv_dry_mass: Optional[float] = Field(default=None, gt=0, description="COPV tank structure mass (tank walls only, no pressurant gas) [kg]")
1522 inertia: List[float] = Field(description="AIRFRAME inertia only (without motor/propulsion), relative to airframe CM [Ixx, Iyy, Izz] [kg·m²]. Motor inertia is added separately by RocketPy from propulsion_dry_mass.")
1523 radius: float = Field(gt=0, description="Rocket body radius (outer diameter / 2) [m]")
1524 rocket_length: Optional[float] = Field(default=None, gt=0, description="Total rocket length from tail to nose tip [m]. Used for MoI estimation.")
1525 motor_position: float = Field(default=0.0, description="Nozzle exit position from rocket tail (z=0 at tail, positive toward nose) [m]")
1526 fins: Optional[FinsConfig] = Field(default=None, description="Fins configuration")
1527 # Nosecone: von Kármán (LD-Haack) is the minimum-drag transonic ogive. Length is derived from the
1528 # fineness ratio (nose length / body DIAMETER) — ~4.5 is a good low-drag transonic/supersonic value.
1529 # Set nose_length to override the derived value.
1530 nose_kind: str = Field(default="vonKarman", description="Nosecone profile (RocketPy): vonKarman | lvhaack | ogive | conical | …")
1531 nose_fineness_ratio: float = Field(default=4.5, gt=0, description="Nose length / body diameter (von Kármán ~4.5:1 is near-optimal transonic). Used when nose_length is unset.")
1532 nose_length: Optional[float] = Field(default=None, gt=0, description="Explicit nosecone length [m]. Overrides nose_fineness_ratio when set.")
1533 avionics_payload_length_m: float = Field(default=4.0, ge=0, description="Length of avionics/payload/recovery section ABOVE the propulsion stack, before the nosecone [m].")
1535 # Drag. Either Cd(M) tables with their source (OpenRocket / RASAero export, wind tunnel, flight
1536 # data), or the component build-up of engine/pipeline/vehicle_drag.py on the vehicle the sim
1537 # assembles. There is no constant Cd. The three build-up inputs default to OpenRocket's own
1538 # component defaults, not to this vehicle's drawing; the flight report lists them.
1539 drag_curve_power_off: Optional[List[List[float]]] = Field(default=None, description="Axial Cd with the motor off, [[Mach, Cd], ...]. Replaces the build-up; needs drag_curve_power_on and drag_curve_source.")
1540 drag_curve_power_on: Optional[List[List[float]]] = Field(default=None, description="Axial Cd while thrusting, [[Mach, Cd], ...]. Give the power-off table again if the source has only one.")
1541 drag_curve_source: Optional[str] = Field(default=None, description="Where the drag tables came from: tool and version, file, test.")
1542 surface_roughness_m: float = Field(default=60e-6, ge=0, description="Skin roughness height for the drag build-up [m]. Default 60 um is OpenRocket's default finish, 'regular paint' (Niskanen 2013 table 3.2); 20 um smooth paint, 0 hydraulically smooth.")
1543 fin_thickness_m: float = Field(default=0.003, gt=0, description="Fin thickness for the drag build-up [m]. Default 3 mm is OpenRocket's default fin, not the drawing.")
1544 fin_profile: Literal["square", "rounded", "airfoil"] = Field(default="square", description="Fin edge profile for the drag build-up (Niskanen 2013 sec. 3.4.4). Default 'square' is OpenRocket's default, not the drawing.")
1545 rail_button_upper_pos_m: Optional[float] = Field(default=None, description="Forward rail button, distance from the tail [m]. With the lower button set, rail exit is when this one leaves the rail; unset, the full rail length is flown.")
1546 rail_button_lower_pos_m: Optional[float] = Field(default=None, description="Aft rail button, distance from the tail [m].")
1548 @model_validator(mode="after")
1549 def _drag_tables_complete(self):
1550 tables = (self.drag_curve_power_off, self.drag_curve_power_on)
1551 if all(t is None for t in tables):
1553 if any(t is None for t in tables) or not (self.drag_curve_source or "").strip():
1554 raise ValueError("rocket.drag_curve_power_off, drag_curve_power_on and drag_curve_source go together")
1555 for name, table in zip(("drag_curve_power_off", "drag_curve_power_on"), tables):
1556 if len(table) < 2 or any(len(row) != 2 for row in table):
1557 raise ValueError(f"rocket.{name} must be at least two [Mach, Cd] rows")
1558 mach = [row[0] for row in table]
1559 if mach[0] < 0 or any(b <= a for a, b in zip(mach, mach[1:])) or any(row[1] <= 0 for row in table):
1560 raise ValueError(f"rocket.{name}: Mach must start >= 0 and increase, Cd must be > 0")
1563 # LEGACY fields - kept for backward compatibility
1564 mass: Optional[float] = Field(default=None, gt=0, description="LEGACY: Airframe mass. Use airframe_mass instead.")
1565 cm_wo_motor: Optional[float] = Field(default=None, description="LEGACY: CM without motor. Now auto-calculated from airframe_mass and propulsion positions.")
1566 dry_mass: Optional[float] = Field(default=None, gt=0, description="LEGACY: Unused field.")
1567 motor_inertia: Optional[List[float]] = Field(default=None, description="LEGACY: Motor inertia. Now estimated from propulsion_dry_mass.")
1568 motor: Optional[MotorConfig] = Field(default=None, description="LEGACY: Motor config. Use propulsion_dry_mass instead.")
1594class FrozenParametersConfig(BaseModel):
1595 """Frozen parameter values for Layer 1 optimization.
1597 When a parameter is set (not None), the optimizer will use that exact
1598 value instead of optimizing it. Values use user-friendly units.
1601 A_throat_mm2: Optional[float] = Field(default=None, gt=0, description="Frozen throat area [mm²]")
1602 Lstar_mm: Optional[float] = Field(default=None, gt=0, description="Frozen characteristic length L* [mm]")
1603 expansion_ratio: Optional[float] = Field(default=None, gt=1, description="Frozen expansion ratio (A_exit/A_throat)")
1604 D_chamber_outer_mm: Optional[float] = Field(default=None, gt=0, description="Frozen chamber outer diameter [mm]")
1606 # Injector geometry — PINTLE (applies when injector.type == 'pintle')
1607 d_pintle_tip_mm: Optional[float] = Field(default=None, gt=0, description="[pintle] Frozen pintle tip diameter [mm]")
1608 h_gap_mm: Optional[float] = Field(default=None, gt=0, description="[pintle] Frozen annular gap height [mm]")
1609 n_orifices: Optional[int] = Field(default=None, gt=0, description="[pintle] Frozen number of LOX orifices")
1610 d_orifice_mm: Optional[float] = Field(default=None, gt=0, description="[pintle] Frozen LOX orifice diameter [mm]")
1611 # Injector geometry — IMPINGING / doublet (applies when injector.type == 'impinging'). These were
1612 # already honored by the Layer-1 frozen-param mapping; the schema just needed to declare them so the
1613 # UI offers them and they validate (INJECTOR_PARITY_PLAN W1). Key names match the optimizer mapping.
1614 n_doublets: Optional[int] = Field(default=None, gt=0, description="[impinging] Frozen number of paired unlike doublets")
1615 d_jet_O_mm: Optional[float] = Field(default=None, gt=0, description="[impinging] Frozen LOX jet diameter [mm]")
1616 d_jet_F_mm: Optional[float] = Field(default=None, gt=0, description="[impinging] Frozen fuel jet diameter [mm]")
1617 impingement_angle_O_deg: Optional[float] = Field(default=None, gt=0, le=180, description="[impinging] Frozen LOX jet angle from the chamber axis [deg] (not the included angle)")
1618 impingement_angle_F_deg: Optional[float] = Field(default=None, gt=0, le=180, description="[impinging] Frozen fuel jet angle from the chamber axis [deg] (not the included angle)")
1619 spacing_O_mm: Optional[float] = Field(default=None, gt=0, description="[impinging] Frozen LOX element spacing [mm]")
1620 spacing_F_mm: Optional[float] = Field(default=None, gt=0, description="[impinging] Frozen fuel element spacing [mm]")
1622 # Initial tank pressures
1623 P_O_start_psi: Optional[float] = Field(default=None, gt=0, description="Frozen initial LOX tank pressure [psi]")
1624 P_F_start_psi: Optional[float] = Field(default=None, gt=0, description="Frozen initial fuel tank pressure [psi]")
2650class HybridOptimizerConfig(BaseModel):
2651 """Configuration for Hybrid CMA + Block Re-optimization"""
2652 elite_k: int = Field(default=50, gt=0, description="Size of elite pool for block building")
2654 block_method: Literal["random", "corr_greedy"] = Field(
2656 description="Method to build variable blocks: 'random' or 'corr_greedy' (correlation-based)"
2658 num_blocks: int = Field(default=3, gt=0, description="Number of blocks")
2659 overlap_fraction: float = Field(default=0.0, ge=0.0, le=0.5, description="Fraction of block indices that overlap with the previous block")
2661 cycles: int = Field(default=3, gt=0, description="Number of re-optimization cycles")
2663 # Soft freezing / Penalty parameters.
2665 # NOT WIRED. ``run_hybrid_optimization`` computes ``base_lambda`` and ``f_scale`` from
2666 # these every cycle and then never applies them: the block objective stitches the block's
2667 # coordinates into the incumbent and evaluates the plain objective, with no penalty on
2668 # leaving the incumbent. Blocks are therefore hard-frozen, and changing any of these four
2669 # fields changes nothing. Kept so shipped configs still validate; do not tune them.
2670 lambda0: float = Field(default=1e-3, gt=0, description="Initial penalty weight base (currently unused -- see note above)")
2671 lambda_mult: float = Field(default=10.0, gt=1.0, description="Multiplier for lambda per cycle")
2672 lambda_max: float = Field(default=1.0, gt=0, description="Maximum lambda (relative to f-scale)")
2673 lambda_normalize: bool = Field(default=True, description="Normalize lambda using objective function scale magnitude")
2676 per_block_budget_fraction: float = Field(default=0.5, gt=0.0, le=0.9, description="Fraction of TOTAL budget allocated to block optimization cycles")
2679 refresh_every_pass: bool = Field(default=True, description="Run a global refresh after every full pass of blocks")
2680 refresh_budget_fraction: float = Field(default=0.1, gt=0.0, le=1.0, description="Fraction of initial global budget for each refresh")
2681 refresh_sigma_scale: float = Field(default=0.2, gt=0.0, le=1.0, description="Scale of sigma for refresh relative to initial sigma")
2684 num_tracks: int = Field(default=1, gt=0, description="Number of independent optimization tracks")
2735class PintleEngineConfig(BaseModel):
2736 """Complete pintle engine configuration"""
2737 # Propellant preset name (configs/propellants/<name>.yaml). Resolved by io.load_config BEFORE
2738 # validation: preset supplies fluids/CEA baseline, explicit YAML fields override. Plain str (not
2739 # Literal) on purpose — adding a new propellant must require zero code changes (UNIFICATION P7).
2740 propellant_preset: Optional[str] = Field(default=None, description="Propellant preset to merge (e.g. 'methalox', 'ethalox', 'kerolox'); explicit fields win over preset")
2741 fluids: Dict[str, FluidConfig]
2742 injector: InjectorConfig
2743 feed_system: Dict[str, FeedSystemConfig] # "oxidizer" and "fuel"
2744 regen_cooling: Optional[RegenCoolingConfig] = Field(default=None, description="Regenerative cooling configuration (fuel only)")
2745 film_cooling: Optional[FilmCoolingConfig] = Field(default=None, description="Film cooling configuration")
2746 ablative_cooling: Optional[AblativeCoolingConfig] = Field(default=None, description="Ablative cooling configuration for chamber liner")
2747 graphite_insert: Optional[GraphiteInsertConfig] = Field(default=None, description="Graphite throat insert configuration (separate from chamber ablator)")
2748 stainless_steel_case: Optional[StainlessSteelCaseConfig] = Field(default=None, description="Stainless steel case configuration (structural wall behind ablative/graphite)")
2749 discharge: Dict[str, DischargeConfig] # "oxidizer" and "fuel"
2750 spray: SprayConfig = Field(default_factory=SprayConfig)
2751 combustion: CombustionConfig = Field(default_factory=CombustionConfig)
2752 # Chamber geometry - unified section for solve_chamber_geometry_with_cea
2753 chamber_geometry: Optional[ChamberGeometryConfig] = Field(default=None, description="Unified chamber geometry config (design inputs + solver outputs)")
2754 # Legacy chamber/nozzle sections (for backward compatibility - optional if chamber_geometry is provided)
2755 chamber: Optional[ChamberConfig] = Field(default=None, description="Legacy chamber config (use chamber_geometry instead)")
2756 nozzle: Optional[NozzleConfig] = Field(default=None, description="Legacy nozzle config (use chamber_geometry instead)")
2757 solver: SolverConfig = Field(default_factory=SolverConfig)
2758 stability: StabilityConfig = Field(default_factory=StabilityConfig, description="Combustion / feed stability model inputs (calibration, regulator, acoustic damping)")
2759 measurements: Optional[MeasurementsConfig] = Field(default=None, description="Measured values that replace the model's assumptions (cold flow, hot fire).")
2760 optimizer: Optional[OptimizerConfig] = Field(default=None, description="Optimizer configuration")
2761 # Flight simulation fields (optional)
2762 lox_tank: Optional[LOXTankConfig] = Field(default=None, description="LOX tank configuration for flight simulation")
2763 fuel_tank: Optional[FuelTankConfig] = Field(default=None, description="Fuel tank configuration for flight simulation")
2764 press_tank: Optional[PressTankConfig] = Field(default=None, description="Pressurant tank configuration for flight simulation")
2765 rocket: Optional[RocketConfig] = Field(default=None, description="Rocket configuration for flight simulation")
2766 environment: Optional[EnvironmentConfig] = Field(default=None, description="Environment configuration for flight simulation")
2767 thrust: Optional[ThrustConfig] = Field(default=None, description="Thrust configuration for flight simulation")
2768 design_requirements: Optional[DesignRequirementsConfig] = Field(default=None, description="Design requirements for optimizer")
2769 pressure_curves: Optional[PressureCurvesConfig] = Field(default=None, description="Optimized pressure curves from Layer 2 optimization")
2770 # Provenance stamp: which {injector, propellant} the current chamber was last solved/seeded for.
2771 # Forward mode warns ("needs re-optimization") when the live injector/propellant no longer match —
2772 # see engine.pipeline.config_switch.design_staleness. Set on canonical load & propellant overlay.
2773 design_valid_for: Optional[Dict[str, Optional[str]]] = Field(default=None, description="{'injector','propellant'} the chamber geometry was solved for (forward-mode staleness flag)")
2775 @field_validator("feed_system", "discharge")
2777 def validate_branches(cls, v):
2778 """Ensure both oxidizer and fuel branches are present"""
2779 if "oxidizer" not in v or "fuel" not in v:
2780 raise ValueError("Must specify both 'oxidizer' and 'fuel' branches")
2783 @model_validator(mode="after")
2784 def injector_aware_geometry_cd_default(self):
2785 """FINDING F1 fix (UNIFICATION P3): ``use_geometry_cd`` schema-defaults to True (drilled
2786 impinging orifices). For PINTLE the geometry-Cd correlation doesn't apply (annulus/orifice
2787 Cd is fixed Cd_inf; the solver never calls it) — so when the field was NOT explicitly given,
2788 derive False for pintle. Explicit YAML values are respected (model_fields_set check)."""
2789 if getattr(getattr(self, "injector", None), "type", None) == "pintle":
2790 for side in ("oxidizer", "fuel"):
2791 d = self.discharge.get(side) if isinstance(self.discharge, dict) else None
2792 if d is not None and "use_geometry_cd" not in d.model_fields_set:
2793 d.use_geometry_cd = False
2796 @model_validator(mode="after")
2797 def align_expansion_ratio_with_exit_throat_areas(self):
2798 """Nozzle thrust uses eps == A_exit/A_throat (strict); YAML often labels eps rounded."""
2799 cg = self.chamber_geometry
2804 if at is None or ae is None:
2809 except (TypeError, ValueError):
2811 if not np.isfinite(at_f) or not np.isfinite(ae_f) or at_f <= 0 or ae_f <= 0:
2813 eps_geo = ae_f / at_f
2816 cg.expansion_ratio = float(eps_geo)
2818 self.combustion.cea.expansion_ratio = float(eps_geo)
2824 nz.expansion_ratio = float(eps_geo)
2829 @model_validator(mode="after")
2830 def sync_burn_time_fields(self):
2831 """Keep thrust.burn_time and pressure_curves.target_burn_time_s aligned with design_requirements."""
2832 from engine.pipeline.burn_time_sync import sync_burn_time_fields
2834 sync_burn_time_fields(self)
2837 model_config = ConfigDict(extra="allow")
2840def ensure_chamber_geometry(config: PintleEngineConfig) -> ChamberGeometryConfig:
2842 Ensure chamber_geometry exists, creating from legacy sections if needed.
2844 This helper function provides backward compatibility by creating chamber_geometry
2845 from legacy chamber/nozzle sections if chamber_geometry doesn't exist.
2849 config : PintleEngineConfig
2850 Configuration object
2854 ChamberGeometryConfig
2855 The chamber_geometry config (created if needed)
2860 If neither chamber_geometry nor legacy sections exist
2862 if config.chamber_geometry is not None:
2863 return config.chamber_geometry
2865 # Create from legacy sections if they exist
2866 if config.chamber is None or config.nozzle is None:
2868 "Must provide either 'chamber_geometry' section or both 'chamber' and 'nozzle' sections"
2871 # Create chamber_geometry from legacy sections
2872 chamber = config.chamber
2873 nozzle = config.nozzle
2875 # Get design parameters (use defaults if not in legacy sections)
2876 design_pressure = getattr(chamber, 'design_pressure', 2.0e6)
2877 design_thrust = getattr(chamber, 'design_thrust', 5000.0)
2878 design_MR = getattr(chamber, 'design_MR', 2.55)
2881 chamber_diameter = getattr(chamber, 'chamber_inner_diameter', 0.08)
2882 Lstar = getattr(chamber, 'Lstar', 1.0)
2883 exit_diameter = getattr(chamber, 'exit_diameter', None) or getattr(nozzle, 'exit_diameter', 0.1)
2884 expansion_ratio = getattr(nozzle, 'expansion_ratio', 8.0)
2885 nozzle_efficiency = getattr(nozzle, 'efficiency', 0.95)
2887 # Get solved outputs (if available)
2888 A_throat = getattr(chamber, 'A_throat', None) or getattr(nozzle, 'A_throat', None)
2889 A_exit = getattr(nozzle, 'A_exit', None)
2890 volume = getattr(chamber, 'volume', None)
2891 length = getattr(chamber, 'length', None)
2892 length_cylindrical = getattr(chamber, 'length_cylindrical', None)
2893 length_contraction = getattr(chamber, 'length_contraction', None)
2894 Cf = getattr(chamber, 'design_force_coefficient', None)
2896 # Create and assign chamber_geometry
2897 config.chamber_geometry = ChamberGeometryConfig(
2898 design_pressure=design_pressure,
2899 design_thrust=design_thrust,
2900 design_MR=design_MR,
2901 chamber_diameter=chamber_diameter,
2903 exit_diameter=exit_diameter,
2904 expansion_ratio=expansion_ratio,
2905 nozzle_efficiency=nozzle_efficiency,
2910 length_cylindrical=length_cylindrical,
2911 length_contraction=length_contraction,
2915 return config.chamber_geometry
2918# Injector-agnostic alias (UNIFICATION P3 naming): the config models any bipropellant engine
2919# (pintle / impinging / coaxial), not just pintle. New code should prefer `EngineConfig`; the
2920# `PintleEngineConfig` name is retained for backward compatibility with the 57 existing references.