STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
dispatch.py
Go to the documentation of this file.
1"""Injector-type physics bindings — single source of truth (UNIFICATION_PLAN Phase 2b, decision D4).
2
3Audited reality (2026-06-11), which these bindings encode rather than invent:
4 * SMD: ``PintleInjector.solve`` hard-binds ``smd_pintle`` (``spray.smd.model`` is IGNORED on the
5 pintle path); ``ImpingingInjector.solve`` hard-binds ``smd_impinging_tn4222`` (``spray.smd.model``
6 is IGNORED — legacy lefebvre on doublets yielded bogus ~1 µm D32).
7 * Geometry-Cd (``cd_inf_from_orifice_diameter``): called only on the impinging path; pintle keeps
8 fixed ``Cd_inf`` semantics (this is WHY anchor A stayed bit-compatible with main).
9 * Layer-1 design vector: pintle 10 DOF (integer dim [6]); impinging 13 DOF (integer dim [4]).
10
11``validate_config_bindings`` flags physically-incoherent combos LOUDLY (log warnings at load time)
12instead of letting them run silently — the hardcoded-Cd bug class. It does not mutate the config:
13behavior changes only happen explicitly in Phase 3 with anchor verification.
14"""
15
16from __future__ import annotations
17
18from dataclasses import dataclass
19from typing import Any, List, Optional
20
21
22@dataclass(frozen=True)
24 injector_type: str
25 expected_smd_model: Optional[str] # None = solver hard-binds its own correlation (knob ignored)
26 smd_knob_ignored: bool # spray.smd.model has no effect for this injector
27 expected_use_geometry_cd: bool # drilled-orifice geometry Cd vs fixed Cd_inf
28 layer1_dof: Optional[int]
29 layer1_integer_dims: Optional[List[int]]
30
31
32INJECTOR_BINDINGS = {
33 "pintle": InjectorBindings(
34 injector_type="pintle",
35 expected_smd_model=None, smd_knob_ignored=True,
36 expected_use_geometry_cd=False,
37 layer1_dof=10, layer1_integer_dims=[6],
38 ),
39 "impinging": InjectorBindings(
40 injector_type="impinging",
41 expected_smd_model="ingebo", smd_knob_ignored=True,
42 expected_use_geometry_cd=True,
43 layer1_dof=13, layer1_integer_dims=[4],
44 ),
45 "coaxial": InjectorBindings(
46 injector_type="coaxial",
47 expected_smd_model=None, smd_knob_ignored=False,
48 expected_use_geometry_cd=False,
49 layer1_dof=None, layer1_integer_dims=None,
50 ),
51}
52
53
54def bindings_for(injector_type: str) -> InjectorBindings:
55 try:
56 return INJECTOR_BINDINGS[str(injector_type)]
57 except KeyError:
58 raise ValueError(
59 f"Unknown injector type {injector_type!r}; known: {sorted(INJECTOR_BINDINGS)}"
60 ) from None
61
62
63# Default geometry templates stamped in when the USER switches injector type in the UI (the
64# geometry structure differs per type and is incompatible across types, so we can't just toggle a
65# flag — we replace the whole geometry block with sane starting values the optimizer then refines).
66# Pintle values mirror main's default.yaml; impinging mirror this branch's default.yaml.
67INJECTOR_GEOMETRY_TEMPLATES = {
68 "pintle": {
69 "lox": {"n_orifices": 14, "d_orifice": 0.00272, "theta_orifice": 90.0,
70 "A_entry": 5.81e-06, "d_hydraulic": 0.00272},
71 "fuel": {"d_pintle_tip": 0.028194, "d_reservoir_inner": 0.0292862, "h_gap": 0.0005461,
72 "A_entry": 0.000624314, "d_hydraulic": 0.0010922},
73 },
74 # NOTE: this template is injector-ONLY and is stamped on an injector-type switch (e.g.
75 # pintle -> impinging) onto the live chamber. The injector and chamber must be a feasible
76 # PAIR (jet area vs throat demand), so we keep the robust larger-area symmetric seed here;
77 # the curated good-basin methalox doublet (asymmetric jets + matched chamber) lives in
78 # configs/default.yaml, which is what the methalox button actually seeds from.
79 "impinging": {
80 "oxidizer": {"n_elements": 20, "d_jet": 0.002, "impingement_angle": 50.0, "spacing": 0.006},
81 "fuel": {"n_elements": 20, "d_jet": 0.002, "impingement_angle": 60.0, "spacing": 0.006},
82 },
83}
84
85
86def default_geometry_for(injector_type: str) -> dict:
87 """Default injector.geometry block for a freshly-switched injector type."""
88 t = str(injector_type)
89 if t not in INJECTOR_GEOMETRY_TEMPLATES:
90 raise ValueError(f"No geometry template for injector type {t!r}; "
91 f"known: {sorted(INJECTOR_GEOMETRY_TEMPLATES)}")
92 import copy
93 return copy.deepcopy(INJECTOR_GEOMETRY_TEMPLATES[t])
94
95
96# Per-injector discharge-coefficient baselines — the "different Cd models we originally had for each"
97# (INJECTOR_PARITY_PLAN W2). Stamped onto discharge.{oxidizer,fuel} on an injector switch so the Cd
98# physics actually reverts per type, not just the use_geometry_cd flag.
99# * pintle = main's ethalox values: LOX orifice 0.40 / annular-gap fuel 0.65, fixed Cd_inf.
100# * impinging = matched drilled-hole 0.60 with geometry-Cd scaling (this branch).
101_GEOMETRY_CD_PARAMS = { # only meaningful when use_geometry_cd=True (impinging)
102 "d_ref_m": 0.002, "cd_small_hole_exponent": 0.20, "cd_large_hole_log_gain": 0.015,
103 "cd_inf_max": 0.62, "cd_inf_min_geom": 0.48, "d_min_m": 0.0004,
104}
105INJECTOR_DISCHARGE_BASELINES = {
106 "pintle": {
107 "oxidizer": {"Cd_inf": 0.40, "a_Re": 0.15, "Cd_min": 0.15, "use_geometry_cd": False},
108 "fuel": {"Cd_inf": 0.65, "a_Re": 0.20, "Cd_min": 0.20, "use_geometry_cd": False},
109 },
110 "impinging": {
111 "oxidizer": {"Cd_inf": 0.60, "a_Re": 0.18, "Cd_min": 0.35, "use_geometry_cd": True, **_GEOMETRY_CD_PARAMS},
112 "fuel": {"Cd_inf": 0.60, "a_Re": 0.18, "Cd_min": 0.35, "use_geometry_cd": True, **_GEOMETRY_CD_PARAMS},
113 },
114}
115
116
117def default_discharge_for(injector_type: str) -> dict:
118 """Per-stream discharge-Cd baseline for a freshly-switched injector type. Empty for types without
119 a registered baseline (caller then leaves discharge untouched)."""
120 import copy
121 return copy.deepcopy(INJECTOR_DISCHARGE_BASELINES.get(str(injector_type), {}))
122
123
124# Which Layer-1 frozen-parameter fields apply to each injector type — the authority the UI uses to
125# render ONLY the relevant freeze checkboxes (no pintle fields in doublet mode, vice-versa).
126# Names match FrozenParametersConfig fields AND the optimizer's frozen_mapping keys.
127_FROZEN_COMMON = ["A_throat_mm2", "Lstar_mm", "expansion_ratio", "D_chamber_outer_mm",
128 "P_O_start_psi", "P_F_start_psi"]
129_FROZEN_INJECTOR = {
130 "pintle": ["d_pintle_tip_mm", "h_gap_mm", "n_orifices", "d_orifice_mm"],
131 "impinging": ["n_doublets", "d_jet_O_mm", "d_jet_F_mm", "impingement_angle_O_deg",
132 "impingement_angle_F_deg", "spacing_O_mm", "spacing_F_mm"],
133 "coaxial": [],
134}
135
136
137def frozen_param_fields_for(injector_type: str) -> List[str]:
138 """Frozen-param field names valid for this injector type (chamber-common + injector-specific)."""
139 t = str(injector_type)
140 if t not in _FROZEN_INJECTOR:
141 raise ValueError(f"Unknown injector type {t!r}; known: {sorted(_FROZEN_INJECTOR)}")
142 return list(_FROZEN_COMMON) + list(_FROZEN_INJECTOR[t])
143
144
145def validate_config_bindings(config: Any) -> List[str]:
146 """Return human-readable warnings for combos that contradict the physics bindings.
147
148 Warn-only by design (Phase 2b): the loader logs these; nothing is mutated so golden anchors
149 cannot shift. Tightening to hard errors / auto-derivation is a Phase-3 decision.
150 """
151 warnings: List[str] = []
152 inj_type = str(getattr(getattr(config, "injector", None), "type", "") or "")
153 if inj_type not in INJECTOR_BINDINGS:
154 return [f"injector.type {inj_type!r} has no physics bindings registered"]
155 b = INJECTOR_BINDINGS[inj_type]
156
157 smd_model = str(getattr(getattr(getattr(config, "spray", None), "smd", None), "model", "") or "")
158 if b.smd_knob_ignored and smd_model:
159 # Documented expected model in YAML is fine (reconciled configs); warn on misleading values.
160 if b.expected_smd_model and smd_model == b.expected_smd_model:
161 pass
162 elif not b.expected_smd_model and smd_model != "lefebvre":
163 warnings.append(
164 f"spray.smd.model='{smd_model}' has NO EFFECT for injector.type='{inj_type}' "
165 f"(solver hard-binds its own correlation)"
166 )
167 elif b.expected_smd_model and smd_model != b.expected_smd_model:
168 warnings.append(
169 f"spray.smd.model='{smd_model}' is ignored for injector.type='{inj_type}' "
170 f"(solver hard-binds {b.expected_smd_model!r}); reconcile via load/switch"
171 )
172 if b.expected_smd_model and not b.smd_knob_ignored and smd_model and smd_model != b.expected_smd_model:
173 warnings.append(
174 f"injector.type='{inj_type}' expects spray.smd.model='{b.expected_smd_model}' but config "
175 f"says '{smd_model}' — atomization (and stability time-lag, τ∝SMD²) will use the "
176 f"unexpected correlation"
177 )
178
179 discharge = getattr(config, "discharge", None) or {}
180 for side in ("oxidizer", "fuel"):
181 d = discharge.get(side) if isinstance(discharge, dict) else getattr(discharge, side, None)
182 if d is None:
183 continue
184 geo = bool(getattr(d, "use_geometry_cd", False))
185 if geo != b.expected_use_geometry_cd:
186 warnings.append(
187 f"discharge.{side}.use_geometry_cd={geo} but injector.type='{inj_type}' expects "
188 f"{b.expected_use_geometry_cd} (geometry-Cd is calibrated for drilled impinging "
189 f"orifices; pintle uses fixed Cd_inf)"
190 )
191 return warnings
List[str] frozen_param_fields_for(str injector_type)
Definition dispatch.py:137
InjectorBindings bindings_for(str injector_type)
Definition dispatch.py:54
dict default_geometry_for(str injector_type)
Definition dispatch.py:86
List[str] validate_config_bindings(Any config)
Definition dispatch.py:145
dict default_discharge_for(str injector_type)
Definition dispatch.py:117