STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
test_plate_dxf.py
Go to the documentation of this file.
1"""The injector plate read from its revolve sketch (engine/core/injectors/plate_dxf.py).
2
3The synthetic plate is built from a chosen answer -- exits, passage lengths, facets normal to
4the passages, a flat-bottomed groove -- and the reader must recover that answer from the lines
5alone. The shipped drawing is checked against numbers worked by hand from its coordinates.
6"""
7import math
8
9import pytest
10
11ezdxf = pytest.importorskip("ezdxf")
12
13IN = 0.0254
14DRAWING = "configs/cad/ethalox_6500N_injector_revolve_2026-09-28.dxf"
15
16
17def _s(a):
18 return math.sin(math.radians(a))
19
20
21def _c(a):
22 return math.cos(math.radians(a))
23
24
25# The answer, inches: face at y = 0, back at y = T.
26T, R = 0.75, 3.0
27TH_O, TH_F = 35.0, 48.0
28E_O_R, F_O = 1.30, 0.05 # LOX exit radius, half-flank
29E_D = F_O * _s(TH_O) # exits at mid-flank, flank starts on the face
30F_F = F_O * _s(TH_O) / _s(TH_F) # fuel half-flank: same groove depth
31E_F_R = 1.72
32L_O, L_F = 0.33, 0.36 # passage lengths
33G = 0.04 # half-facet
34
35
37 P_O = (E_O_R - L_O * _s(TH_O), E_D + L_O * _c(TH_O)) # LOX goes inward to its channel
38 P_F = (E_F_R + L_F * _s(TH_F), E_D + L_F * _c(TH_F)) # fuel outward
39 fo = [(P_O[0] - G * _c(TH_O), P_O[1] - G * _s(TH_O)), (P_O[0] + G * _c(TH_O), P_O[1] + G * _s(TH_O))]
40 ff = [(P_F[0] - G * _c(TH_F), P_F[1] + G * _s(TH_F)), (P_F[0] + G * _c(TH_F), P_F[1] - G * _s(TH_F))]
41 r1, r2 = fo[0][0] - 0.3, ff[1][0] + 0.3
42 loop = [
43 (0.0, 0.0),
44 (E_O_R - F_O * _c(TH_O), 0.0), (E_O_R + F_O * _c(TH_O), 2 * E_D), # LOX flank
45 (E_F_R - F_F * _c(TH_F), 2 * E_D), (E_F_R + F_F * _c(TH_F), 0.0), # fuel flank
46 (R, 0.0), (R, T),
47 (r2, T), (r2, ff[1][1]), ff[1], ff[0], (ff[0][0], T), # fuel channel
48 (fo[1][0], T), fo[1], fo[0], (r1, fo[0][1]), (r1, T), # LOX channel
49 (0.0, T),
50 ]
51 return loop, P_O, P_F, fo, ff, r1, r2
52
53
54def _write(path, loop):
55 doc = ezdxf.new()
56 doc.header["$INSUNITS"] = 1
57 msp = doc.modelspace()
58 for a, b in zip(loop, loop[1:] + loop[:1]):
59 msp.add_line(a, b)
60 doc.saveas(path)
61
62
63@pytest.fixture
64def synthetic(tmp_path):
65 loop, *rest = _geometry()
66 p = tmp_path / "plate.dxf"
67 _write(str(p), loop)
68 return str(p), rest
69
70
72 from engine.core.injectors.plate_dxf import read_plate_profile
73 path, (P_O, P_F, fo, ff, r1, r2) = synthetic
74 prof = read_plate_profile(path)
75 assert prof["thickness"] == pytest.approx(T * IN, rel=1e-9)
76 assert prof["plate_radius"] == pytest.approx(R * IN, rel=1e-9)
77 assert not prof["axis_hole"]
78 lox, fuel = prof["channels"]
79 # Area between the back face and the floor: flat floor, then the facet (trapezoid).
80 a_lox = (fo[0][0] - r1) * (T - fo[0][1]) + (fo[1][0] - fo[0][0]) * (T - P_O[1])
81 a_fuel = (r2 - ff[1][0]) * (T - ff[1][1]) + (ff[1][0] - ff[0][0]) * (T - P_F[1])
82 assert lox["flow_area"] == pytest.approx(a_lox * IN ** 2, rel=1e-9)
83 assert fuel["flow_area"] == pytest.approx(a_fuel * IN ** 2, rel=1e-9)
84 assert lox["depth"] == pytest.approx((T - fo[0][1]) * IN, rel=1e-9)
85 assert lox["ligament_min"] == pytest.approx(fo[0][1] * IN, rel=1e-9)
86 assert prof["face_groove"]["depth"] == pytest.approx(2 * E_D * IN, rel=1e-9)
87
88
90 from engine.core.injectors.plate_dxf import drilled_passages, read_plate_profile
91 path, _ = synthetic
92 h = drilled_passages(read_plate_profile(path), {"O": TH_O, "F": TH_F})
93 assert h["O"]["exit_r"] == pytest.approx(E_O_R * IN, rel=1e-9)
94 assert h["F"]["exit_r"] == pytest.approx(E_F_R * IN, rel=1e-9)
95 assert h["O"]["exit_depth"] == pytest.approx(E_D * IN, rel=1e-9)
96 assert h["O"]["length"] == pytest.approx(L_O * IN, rel=1e-9)
97 assert h["F"]["length"] == pytest.approx(L_F * IN, rel=1e-9)
98 assert h["O"]["entry_off_square_deg"] == pytest.approx(0.0, abs=1e-6)
99 # Mid-flank: equal slant to each flank edge.
100 a, b = h["F"]["flank_slant"]
101 assert a == pytest.approx(b, rel=1e-9) and a == pytest.approx(F_F * IN, rel=1e-9)
102
103
105 from engine.core.injectors.plate_dxf import drilled_passages, read_plate_profile
106 path, _ = synthetic
107 assert drilled_passages(read_plate_profile(path), {"O": 30.0}) == {}
108
109
111 from engine.core.injectors.plate_dxf import read_plate_profile, section_thickness
112 path, (P_O, P_F, fo, ff, r1, r2) = synthetic
113 prof = read_plate_profile(path)
114 r_floor = 0.5 * (r1 + fo[0][0])
115 assert section_thickness(prof, r_floor * IN) == pytest.approx(fo[0][1] * IN, rel=1e-9)
116 r_groove = 0.5 * (E_O_R + E_F_R)
117 assert section_thickness(prof, r_groove * IN) == pytest.approx((T - 2 * E_D) * IN, rel=1e-9)
118 assert section_thickness(prof, 0.2 * IN) == pytest.approx(T * IN, rel=1e-9)
119
120
122 from engine.core.injectors.plate_dxf import read_plate_profile
123 loop, *_ = _geometry()
124 doc = ezdxf.new()
125 doc.header["$INSUNITS"] = 1
126 for a, b in zip(loop, loop[1:]): # the closing line left out
127 doc.modelspace().add_line(a, b)
128 p = tmp_path / "open.dxf"
129 doc.saveas(str(p))
130 with pytest.raises(ValueError, match="close"):
131 read_plate_profile(str(p))
132
133
135 """By hand from the sketch's coordinates (inches). LOX channel: 0.300 wide floor 0.4868 deep,
136 then a facet 0.0819 across rising 0.0573: 0.300 x 0.4868 + 0.0819 x (0.4868 - 0.0573/2)
137 = 0.18357 in^2. LOX exit at the flank midpoint, r = (1.209 + 1.291)/2 = 1.250 in; the passage
138 to the facet midpoint (1.06575, 0.29185) from (1.250, 0.0287) is 0.3213 in = 8.16 mm."""
139 from engine.core.injectors.plate_dxf import drilled_passages, load_plate_profile
140 prof = load_plate_profile(DRAWING)
141 assert prof["thickness"] == pytest.approx(0.75 * IN, rel=1e-6)
142 lox, fuel = prof["channels"]
143 assert lox["flow_area"] == pytest.approx(0.18357 * IN ** 2, rel=2e-3)
144 assert lox["r_lo"] == pytest.approx(0.7248 * IN, rel=1e-4)
145 assert fuel["r_hi"] == pytest.approx(2.2322 * IN, rel=1e-4)
146 h = drilled_passages(prof, {"O": 35.0, "F": 48.0})
147 assert h["O"]["exit_r"] == pytest.approx(1.250 * IN, rel=1e-4)
148 assert h["O"]["length"] == pytest.approx(0.3213 * IN, rel=1e-3)
149 assert h["O"]["channel"] == 0 and h["F"]["channel"] == 1
150
151
152def _cfg():
153 """The stand's config with its drawing as the geometry (the config itself only checks it)."""
154 from engine.pipeline.io import load_config
155 cfg = load_config("tests/fixtures/ethalox_6500N_doublet_cad_2026-09-28.yaml")
156 cfg.injector.plate.profile_dxf_mode = "geometry"
157 return cfg
158
159
161 from engine.core.injectors.layout import layout_from_config
162 cfg = _cfg()
163 lay = layout_from_config(cfg, drawings=False)
164 assert lay["drawing"]["thickness"] == pytest.approx(0.75 * IN, rel=1e-6)
165 assert lay["passages"]["O"]["plate_l_over_d"] == pytest.approx(8.16e-3 / cfg.injector.geometry.oxidizer.d_jet, rel=2e-3)
166 assert not [w for w in lay["warnings"] if w["code"].startswith("drawing_") and w["level"] == "bad"]
167 # the igniter is the config's; the drawing has no centre port, and says so
168 assert [w for w in lay["warnings"] if w["code"] == "drawing_igniter" and w["level"] == "warn"]
169 # Move the LOX ring 0.5 mm: the drawing and the config now describe different plates.
170 cfg.injector.geometry.oxidizer.spacing *= (31.75 + 0.5) / 31.75
171 lay = layout_from_config(cfg, drawings=False)
172 assert [w for w in lay["warnings"] if w["code"] == "drawing_ring_O" and w["level"] == "bad"]
173
174
176 from engine.core.injectors.impinging import _ring_manifold_for
177 cfg = _cfg()
178 net = _ring_manifold_for(cfg, "oxidizer", 1140.0, 1.8e-4, 24, cfg.injector.geometry.oxidizer.d_jet)
179 lox = __import__("engine.core.injectors.plate_dxf", fromlist=["x"]).load_plate_profile(DRAWING)["channels"][0]
180 assert net.A_ch == pytest.approx(lox["flow_area"], rel=1e-12)
181 assert net.D_h == pytest.approx(lox["hydraulic_diameter"], rel=1e-12)
182 assert net.s == pytest.approx(2 * math.pi * lox["r_centroid"] / 24, rel=1e-12) # hole pitch along the channel
synthetic(tmp_path)
test_section_thickness_is_the_metal_on_the_axial_line(synthetic)
test_layout_takes_the_drawing_and_flags_what_the_config_contradicts()
test_a_jet_angle_the_drawing_does_not_carry_locates_nothing(synthetic)
test_the_shipped_drawing_against_hand_numbers()
test_ring_manifold_uses_the_drawn_section()
_write(path, loop)
test_passages_run_from_facet_to_flank_at_the_jet_angle(synthetic)
test_the_plate_and_its_channels_come_back_from_the_lines(synthetic)
test_an_open_profile_is_refused(tmp_path)