STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
test_injector_plate_profile.py
Go to the documentation of this file.
1"""The injector plug's half-section built from its parameters (layout.plate_profile): the drill-spot
2channel, back-face grooves, the rim gland, the centre port and the cover plate's feed ports.
3
4The acceptance test is the stand's own plate: built from the numbers in the config it must land
5on the designer's revolve sketch (configs/cad/...dxf) to within a thou, everywhere.
6"""
7import copy
8import json
9import math
10from pathlib import Path
11
12import pytest
13import yaml
14
15ROOT = Path(__file__).resolve().parents[1]
16CFG = ROOT / "tests/fixtures/ethalox_6500N_doublet_cad_2026-09-28.yaml"
17IN = 0.0254
18
19
20def _cfg():
21 return yaml.safe_load(CFG.read_text())
22
23
24def _lay(cfg, drawings=False):
25 from engine.core.injectors.layout import layout_from_config
26 return layout_from_config(cfg, drawings=drawings)
27
28
29def _codes(out):
30 return {w["code"]: w["level"] for w in out["warnings"]}
31
32
33# ---- the drill-spot channel ------------------------------------------------------------------
34
35@pytest.mark.parametrize("inner", [True, False])
37 from engine.core.injectors.layout import channel_for_ring
38 th, d, t, w, spot = (35.0 if inner else 48.0), 1.6e-3, 19.05e-3, 7.62e-3, 2.54e-3
39 ch = channel_for_ring(r_exit=0.03, z_exit=-0.7e-3, d=d, theta_deg=th, is_inner=inner,
40 plate_thickness=t, passage_ld=5.0, width=w, floor="spot", spot_length=spot)
41 near, far = ch["spot"]
42 along = (far[0] - near[0], far[1] - near[1])
43 k = -1.0 if inner else 1.0
44 into = (k * math.sin(math.radians(th)), -math.cos(math.radians(th))) # the passage, going in
45 assert math.hypot(*along) == pytest.approx(spot, rel=1e-12)
46 assert along[0] * into[0] + along[1] * into[1] == pytest.approx(0.0, abs=1e-15)
47 assert 0.5 * (near[0] + far[0]) == pytest.approx(ch["end"][0], rel=1e-12)
48 assert ch["length"] == pytest.approx(5.0 * d, rel=1e-12)
49 # Flat floor w wide from the facet's face-side end, away from the exit side.
50 floor = [p for p in ch["section"] if abs(p[1] - near[1]) < 1e-15]
51 assert max(p[0] for p in floor) - min(p[0] for p in floor) == pytest.approx(w, rel=1e-12)
52 # Area by hand: the floor's rectangle plus the trapezoid under the facet.
53 a = w * (t + near[1]) + abs(far[0] - near[0]) * (t + 0.5 * (near[1] + far[1]))
54 assert ch["flow_area"] == pytest.approx(a, rel=1e-12)
55 assert ch["breakthrough"].startswith("square")
56
57
58# ---- the stand's plate, against its drawing ---------------------------------------------------
59
61 out = _lay(_cfg())
62 assert out["drawing"]["mode"] == "check"
63 assert out["drawing"]["deviation"]["max"] < 0.01e-3
64 assert _codes(out)["drawing_profile"] == "info"
65 for k in ("O", "F"):
66 p = out["drawing"]["passages"][k]
67 assert p["exit_offset"] < 0.005e-3 and abs(p["length_offset"]) < 0.005e-3
68
69
71 cfg = _cfg()
72 cfg["injector"]["plate"]["channel_spot_length"] += 0.1e-3
73 out = _lay(cfg)
74 assert _codes(out)["drawing_profile"] == "bad"
75 dev = out["drawing"]["deviation"]
76 assert 0.02e-3 < dev["max"] < 0.2e-3
77 # ... at a channel, not somewhere else on the plate
78 r = dev["where_a"][0]
79 chans = [out["passages"][k]["channel"] for k in ("O", "F")]
80 assert any(c["r_lo"] - 1e-4 <= r <= c["r_hi"] + 1e-4 for c in chans)
81
82
84 from engine.pipeline.io import load_config
85 from engine.core.injectors.impinging import _ring_manifold_for
86 cfg = load_config(str(CFG))
87 ch = _lay(cfg)["passages"]["O"]["channel"]
88 net = _ring_manifold_for(cfg, "oxidizer", 1140.0, 1.8e-4, 24, cfg.injector.geometry.oxidizer.d_jet)
89 assert ch["flow_area"] == pytest.approx(0.18357 * IN ** 2, rel=2e-3) # by hand, see test_plate_dxf
90 assert net.A_ch == ch["flow_area"] and net.D_h == ch["hydraulic_diameter"]
91
92
93# ---- back-face features ------------------------------------------------------------------------
94
96 cfg = _cfg()
97 lox = _lay(cfg)["passages"]["O"]["channel"]
98 cfg["injector"]["plate"]["back_grooves"].append(
99 {"r_inner": lox["r_hi"] - 1e-3, "r_outer": lox["r_hi"] + 2e-3, "depth": 1e-3})
100 out = _lay(cfg)
101 assert _codes(out)["back_groove"] == "bad"
102 assert len(out["profile"]["grooves"]) == 6
103 assert any("LOX channel" in m for m in out["profile"]["skipped"])
104
105
107 out = _lay(_cfg())
108 assert _codes(out)["igniter_groove"] == "bad" # 3/8 NPT OD 17.1 mm vs groove at r 7.65
109 cfg = _cfg()
110 cfg["injector"]["igniter"] = {"thread": "1/8 NPT"} # OD 10.3 mm clears it
111 assert "igniter_groove" not in _codes(_lay(cfg))
112
113
115 out = _lay(_cfg())
116 assert _codes(out)["port_over_groove_O"] == "warn"
117 ports = out["ports"]
118 assert ports["per_ring"] == 2 and ports["rings"]["F"]["angles_deg"] == [90.0, 270.0]
119 cfg = _cfg()
120 cfg["injector"]["plate"]["port_bore"] = 0.0127
121 assert "port_over_groove_O" not in _codes(_lay(cfg))
122
123
125 """The grooves thin the section: 19.05 mm of plate, 19.05 - 1.956 mm under the outer groove
126 (where the face is flat), and under groove 3 the face groove comes off too (1.458 mm)."""
127 from engine.core.injectors.plate_dxf import loop_section
128 out = _lay(_cfg())
129 g3 = out["profile"]["grooves"][2]
130 r3 = 0.5 * (g3["r_inner"] + g3["r_outer"])
131 assert loop_section(out["profile"]["loop"], r3) == pytest.approx(0.673 * IN - 0.0574 * IN, rel=2e-3)
132 g = out["profile"]["grooves"][4]
133 r = 0.5 * (g["r_inner"] + g["r_outer"])
134 assert loop_section(out["profile"]["loop"], r) == pytest.approx(0.75 * IN - 0.077 * IN, rel=1e-9)
135
136
138 """Opt-in: a plate that declares no grooves, gland or ports has the section the bending
139 check used before (face surface down to the channel floor)."""
140 from engine.core.injectors.layout import face_z_at
141 from engine.core.injectors.plate_dxf import loop_section
142 cfg = yaml.safe_load((ROOT / "configs/ethalox_6500N.yaml").read_text())
143 out = _lay(cfg)
144 assert not out["profile"]["grooves"] and out["ports"] is None
145 t = out["inputs"]["plate_thickness"]
146 for k in ("O", "F"):
147 ch = out["passages"][k]["channel"]
148 for r in (ch["r_lo"] + 1e-4, ch["r_center"], ch["r_hi"] - 1e-4):
149 old = face_z_at(out["face"]["profile"], r) - max(-t, ch["end"][1] + ch["floor_slope"] * (r - ch["r_center"]))
150 assert loop_section(out["profile"]["loop"], r) == pytest.approx(min(t, old), rel=1e-9)
151
152
153# ---- views --------------------------------------------------------------------------------------
154
156 D = _lay(_cfg(), drawings=True)["drawings"]
157 polys = [p for p in D["revolve"] if p["layer"] == "PLATE"]
158 assert len(polys) == 1 and polys[0]["closed"]
159 ys = [q[1] for q in polys[0]["pts"]]
160 # As the designer's sketch: face at y = 0, the groove cut up into the plate, back at y = t.
161 assert min(ys) == pytest.approx(0.0, abs=1e-12) and max(ys) == pytest.approx(0.75 * IN, rel=1e-6)
162 face = [q for q in polys[0]["pts"] if 0 < q[1] < 0.1 * IN]
163 assert max(q[1] for q in face) == pytest.approx(0.0574 * IN, rel=2e-3)
164
165
167 D = _lay(_cfg(), drawings=True)["drawings"]
168 ids = {p.get("id") for p in D["ports"] if p.get("id")}
169 assert ids == {"PO0", "PO1", "PF0", "PF1"}
170
171
172FIXTURE = ROOT / "frontend/src/components/__fixtures__/layout_doublet_6500N.json"
173
174
176 """Regenerate: python3 scripts/injector_layout.py --no-flows tests/fixtures/ethalox_6500N_doublet_cad_2026-09-28.yaml > <fixture>"""
177 want = json.loads(json.dumps(_lay(_cfg(), drawings=True)))
178 got = json.loads(FIXTURE.read_text())
179 assert got.keys() == want.keys()
180 assert got["drawings"].keys() == want["drawings"].keys()
181 flat = lambda L: [v for q in L for v in q] # noqa: E731
182 assert flat(got["profile"]["loop"]) == pytest.approx(flat(want["profile"]["loop"]), abs=1e-9)
183 assert got["ports"] == want["ports"]
184
185
187 """The injector solve reads channel sections dozens of times per operating point; it takes the
188 layout without the plate-bending check and drawing comparison (80 % of a solve before). The
189 channels must be the same ones the full layout draws."""
190 full = _lay(_cfg())
191 from engine.core.injectors.layout import layout_from_config
192 lean = layout_from_config(_cfg(), drawings=False, checks=False)
193 assert "plate_bending" not in lean and "drawing" not in lean
194 for k in ("O", "F"):
195 a, b = full["passages"][k]["channel"], lean["passages"][k]["channel"]
196 for f in ("flow_area", "hydraulic_diameter", "r_center", "r_lo", "r_hi"):
197 assert b[f] == pytest.approx(a[f], rel=1e-12), (k, f)
198 # geometry mode: the drawing's channels reach the lean path too
199 cfg = _cfg()
200 cfg["injector"]["plate"]["profile_dxf_mode"] = "geometry"
201 assert layout_from_config(cfg, drawings=False, checks=False)["passages"]["O"]["channel"]["floor"] == "drawing"
#define min(a, b)
Definition Arduino.h:42
#define max(a, b)
Definition Arduino.h:43