STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
utils.py
Go to the documentation of this file.
1"""Utility functions for optimization layers.
2
3Contains parameter extraction and other helper utilities.
4"""
5
6from __future__ import annotations
7
8from typing import Dict, Any
9import numpy as np
10
11from engine.pipeline.config_schemas import PintleEngineConfig
12
13# Impinging injector bounds (Layer-1 + feasibility scripts): scale with chamber bore.
14DEFAULT_IMPINGING_WALL_THICKNESS_M = 0.0254
15IMPINGING_JET_PITCH_ESTIMATE_M = 0.004 # heuristic min center-to-center spacing for n_elements ceiling
16IMPINGING_N_ELEMENTS_ABSOLUTE_CAP = 200
17
18
19def total_wall_thickness_m(config: Any) -> float:
20 """Total chamber-wall thickness (both sides) subtracted from the max OD to get
21 the gas-side inner diameter: ``2 x (metal case + ablative liner)``.
22
23 Derived from its physical parts instead of stored as a separate lump field:
24 * metal — ``design_requirements.metal_wall_thickness_per_side_m`` (geometry
25 envelope for the structural case; default 0.25 in/side), and
26 * liner — ``ablative_cooling.initial_thickness`` when ablative cooling is
27 enabled (the same field Layer 3 sizes, so the wall tracks the sized liner
28 on reruns automatically — no write-back step to lose the metal term).
29
30 The graphite insert is a THROAT insert (axial), not part of the chamber-barrel
31 radial stack, so it does not contribute. Film/regen cooling contribute no liner
32 thickness here (channel geometry is not modeled as barrel thickness).
33
34 Falls back to the legacy 1-inch total (``DEFAULT_IMPINGING_WALL_THICKNESS_M``)
35 only when the config lacks the fields (mocks / legacy config objects).
36 """
37 dr = getattr(config, "design_requirements", None)
38 metal = getattr(dr, "metal_wall_thickness_per_side_m", None) if dr is not None else None
39 try:
40 metal = float(metal)
41 except (TypeError, ValueError):
42 return DEFAULT_IMPINGING_WALL_THICKNESS_M
43 if not (np.isfinite(metal) and metal > 0.0):
44 return DEFAULT_IMPINGING_WALL_THICKNESS_M
45 liner = 0.0
46 ab = getattr(config, "ablative_cooling", None)
47 if ab is not None and bool(getattr(ab, "enabled", False)):
48 try:
49 t = float(getattr(ab, "initial_thickness", 0.0))
50 except (TypeError, ValueError):
51 t = 0.0
52 if np.isfinite(t) and t > 0.0:
53 liner = t
54 return 2.0 * (metal + liner)
55
56
57def extract_all_parameters(config: PintleEngineConfig) -> Dict[str, Any]:
58 """Extract all optimized parameters from config."""
59 params = {}
60 injector_type = getattr(getattr(config, "injector", None), "type", None)
61 if injector_type is not None:
62 params["injector_type"] = injector_type
63
64 # Injector parameters
65 if hasattr(config, 'injector') and config.injector.type == "pintle":
66 geometry = config.injector.geometry
67 if hasattr(geometry, 'fuel'):
68 params["d_pintle_tip"] = geometry.fuel.d_pintle_tip
69 params["h_gap"] = geometry.fuel.h_gap
70 if hasattr(geometry.fuel, 'd_reservoir_inner'):
71 params["d_reservoir_inner"] = geometry.fuel.d_reservoir_inner
72 if hasattr(geometry, 'lox'):
73 params["n_orifices"] = geometry.lox.n_orifices
74 params["d_orifice"] = geometry.lox.d_orifice
75 params["theta_orifice"] = geometry.lox.theta_orifice
76 elif hasattr(config, 'injector') and config.injector.type == "impinging":
77 g = config.injector.geometry
78 nd = int(min(int(g.oxidizer.n_elements), int(g.fuel.n_elements)))
79 params["n_doublets"] = nd
80 # Canonical frontend keys for impinging geometry
81 params["n_elements_O"] = int(g.oxidizer.n_elements)
82 params["d_jet_O"] = g.oxidizer.d_jet
83 params["imp_angle_O"] = g.oxidizer.impingement_angle
84 params["spacing_O"] = g.oxidizer.spacing
85 params["n_elements_F"] = int(g.fuel.n_elements)
86 params["d_jet_F"] = g.fuel.d_jet
87 params["imp_angle_F"] = g.fuel.impingement_angle
88 params["spacing_F"] = g.fuel.spacing
89 # Axial standoff from the injector face to where each doublet's jets collide.
90 # Purely geometric (spacings + angles), but it is the number that says whether the
91 # collision is far enough off the plate not to erode it, so it belongs in the
92 # reported geometry rather than only in the spray diagnostics.
93 from engine.core.injectors.impinging import impingement_standoff_m
94 params["L_imp"] = impingement_standoff_m(
95 nd, g.oxidizer.spacing, g.fuel.spacing,
96 g.oxidizer.impingement_angle, g.fuel.impingement_angle,
97 )
98 # Pitch-circle diameters: the circles the jet CENTRES lie on. ``spacing`` is the arc
99 # between adjacent holes on one ring, which reads as a tiny number and is routinely
100 # mistaken for the ring size -- report both so there is nothing to misread.
101 import math as _math
102 params["D_pitch_O"] = nd * g.oxidizer.spacing / _math.pi
103 params["D_pitch_F"] = nd * g.fuel.spacing / _math.pi
104 params["ring_radial_offset"] = 0.5 * abs(params["D_pitch_O"] - params["D_pitch_F"])
105 # Included angle between the two jet centrelines (what a drawing dimensions).
106 params["included_angle_deg"] = float(g.oxidizer.impingement_angle) + float(g.fuel.impingement_angle)
107 params["jet_angle_asymmetry_deg"] = abs(
108 float(g.fuel.impingement_angle) - float(g.oxidizer.impingement_angle))
109 # Backward-compatible aliases used by older views/scripts.
110 params["d_jet_oxidizer"] = g.oxidizer.d_jet
111 params["impingement_angle_oxidizer"] = g.oxidizer.impingement_angle
112 params["spacing_oxidizer"] = g.oxidizer.spacing
113 params["d_jet_fuel"] = g.fuel.d_jet
114 params["impingement_angle_fuel"] = g.fuel.impingement_angle
115 params["spacing_fuel"] = g.fuel.spacing
116
117 # Chamber parameters
118 from engine.pipeline.config_schemas import ensure_chamber_geometry
119 cg = ensure_chamber_geometry(config)
120 params["A_throat"] = cg.A_throat
121 params["Lstar"] = cg.Lstar
122 params["chamber_volume"] = cg.volume
123 params["chamber_length"] = cg.length
124 params["chamber_diameter"] = cg.chamber_diameter
125
126 # Nozzle parameters
127 params["A_exit"] = cg.A_exit
128 params["expansion_ratio"] = cg.expansion_ratio
129
130 # Ablative liner parameters
131 if hasattr(config, 'ablative_cooling') and config.ablative_cooling and config.ablative_cooling.enabled:
132 params["ablative_thickness"] = config.ablative_cooling.initial_thickness
133 params["ablative_enabled"] = True
134 else:
135 params["ablative_thickness"] = 0.0
136 params["ablative_enabled"] = False
137
138 # Graphite insert parameters
139 if hasattr(config, 'graphite_insert') and config.graphite_insert and config.graphite_insert.enabled:
140 params["graphite_thickness"] = config.graphite_insert.initial_thickness
141 params["graphite_enabled"] = True
142 else:
143 params["graphite_thickness"] = 0.0
144 params["graphite_enabled"] = False
145
146 return params
147
148
150 config: PintleEngineConfig,
151 *,
152 max_chamber_outer_diameter_m: float,
153 wall_thickness_m: float = DEFAULT_IMPINGING_WALL_THICKNESS_M,
154) -> float:
155 """Bore used for impinging packing / n_elements limits.
156
157 Prefer ``chamber_geometry.chamber_diameter``; else frozen outer diameter minus wall;
158 else legacy heuristic ``0.5 * max_od - wall`` (underestimates full-OD designs).
159 """
160 cg = getattr(config, "chamber_geometry", None)
161 if cg is not None:
162 d = getattr(cg, "chamber_diameter", None)
163 if d is not None and float(d) > 0.0:
164 return float(d)
165 dr = getattr(config, "design_requirements", None)
166 if dr is not None:
167 fp = getattr(dr, "frozen_parameters", None)
168 if fp is not None:
169 domm = getattr(fp, "D_chamber_outer_mm", None)
170 if domm is not None and float(domm) > 0.0:
171 return float(domm) * 1e-3 - wall_thickness_m
172 min_outer = float(max_chamber_outer_diameter_m) * 0.5
173 return float(max(min_outer - wall_thickness_m, 0.02))
174
175
177 chamber_inner_diameter_m: float,
178 *,
179 pitch_est_m: float = IMPINGING_JET_PITCH_ESTIMATE_M,
180 absolute_cap: int = IMPINGING_N_ELEMENTS_ABSOLUTE_CAP,
181) -> int:
182 """Upper integer count ~ circumference / pitch, capped for optimizer stability."""
183 if chamber_inner_diameter_m <= 0.0:
184 return 12
185 n_circ = int(np.pi * chamber_inner_diameter_m / max(pitch_est_m, 1e-9))
186 return int(max(12, min(absolute_cap, n_circ)))
187
188
189def impinging_d_jet_upper_bound_m(chamber_inner_diameter_m: float) -> float:
190 """Allow larger jets on larger bores; keep 4 mm floor for small chambers."""
191 d = float(chamber_inner_diameter_m)
192 return float(min(0.008, max(0.004, 0.038 * d)))
193
194
195def impinging_spacing_upper_bound_m(chamber_inner_diameter_m: float) -> float:
196 """Wider spacing cap for large injector faces (packing uses π D²/4)."""
197 d = float(chamber_inner_diameter_m)
198 return float(min(0.14, max(0.06, 0.42 * d)))
199
#define min(a, b)
Definition Arduino.h:42
#define max(a, b)
Definition Arduino.h:43
Dict[str, Any] extract_all_parameters(PintleEngineConfig config)
Definition utils.py:57
float impinging_spacing_upper_bound_m(float chamber_inner_diameter_m)
Definition utils.py:195
float impinging_d_jet_upper_bound_m(float chamber_inner_diameter_m)
Definition utils.py:189
int impinging_n_elements_hi_int(float chamber_inner_diameter_m, *, float pitch_est_m=IMPINGING_JET_PITCH_ESTIMATE_M, int absolute_cap=IMPINGING_N_ELEMENTS_ABSOLUTE_CAP)
Definition utils.py:181
float total_wall_thickness_m(Any config)
Definition utils.py:19
float impinging_chamber_inner_diameter_for_bounds(PintleEngineConfig config, *, float max_chamber_outer_diameter_m, float wall_thickness_m=DEFAULT_IMPINGING_WALL_THICKNESS_M)
Definition utils.py:154