STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
test_injector_face_real_estate.py
Go to the documentation of this file.
1"""The injector face has finite real estate, and the optimizer must respect it.
2
3Until these constraints existed, WHERE the doublet ring pair sat radially was an exactly
4flat direction in the Layer-1 objective: ``_layer1_derive_fuel_spacing`` solves the fuel
5pitch so the standoff hits its target, and it does so by fixing the ring GAP, so ``dr`` is
6independent of ``s_O`` and the pair slides radially at zero cost. ``s_O`` fell onto its own
7lower bound (0.003 m).
8
9Measured on configs/ethalox_8kN_FINAL.yaml before the fix:
10
11 D_pitch_O 26.90 mm (28 holes at a 3.018 mm pitch -- a 1.44 mm web)
12 D_pitch_F 88.01 mm
13 impingement 41.79 mm circle, inside a 127.00 mm bore and NARROWER THAN THE
14 49.57 mm THROAT -- 10.8 % of the chamber area was fed directly
15 centre 24.83 mm clear, against 27.15 mm needed for a 3/8-18 NPT igniter boss
16
17None of that violated a single constraint, because none of those constraints existed.
18"""
19import math
20import numpy as np
21import pytest
22
24 _impinging_ring_geometry_squared,
25 _impinging_hard_geometry_blocks_eval,
26)
27
28# The shipped FINAL design, in SI.
29FINAL = dict(
30 n_elements=28.0,
31 spacing_O_m=0.0030182972100858507,
32 spacing_F_m=0.009875029755263455,
33 d_jet_O_m=0.0015821116699998301,
34 d_jet_F_m=0.001375132476863662,
35 D_chamber_inner_m=0.127,
36 angle_O_deg=40.0,
37 angle_F_deg=69.0,
38)
39IGNITER_BOSS_M = 0.0280 # 3/8-18 NPT: 17.15 mm thread crest + ~5 mm wall each side
40
41
42def _dp(n, s):
43 return n * s / math.pi
44
45
46# ---------------------------------------------------------------------------------------
47# Centre clearance
48# ---------------------------------------------------------------------------------------
49
51 """Documents the defect: the FINAL face cannot take a 3/8 NPT boss."""
52 half_major = 0.5 * FINAL["d_jet_O_m"] / math.cos(math.radians(FINAL["angle_O_deg"]))
53 inner_edge = 0.5 * _dp(FINAL["n_elements"], FINAL["spacing_O_m"]) - half_major
54 assert 2 * inner_edge < IGNITER_BOSS_M, "fixture stale: FINAL now clears the boss"
55
56
58 free = _impinging_ring_geometry_squared(**FINAL, center_clear_dia_m=0.0)
59 held = _impinging_ring_geometry_squared(**FINAL, center_clear_dia_m=IGNITER_BOSS_M)
60 assert held > free, "reserving the centre circle must cost the shipped layout something"
61
62
64 """Same design, LOX ring opened to a 60 mm pitch circle: the term must go to zero."""
65 moved = dict(FINAL)
66 moved["spacing_O_m"] = math.pi * 0.060 / FINAL["n_elements"]
67 moved["spacing_F_m"] = moved["spacing_O_m"] + (
68 FINAL["spacing_F_m"] - FINAL["spacing_O_m"]) # same ring gap => same standoff
69 base = _impinging_ring_geometry_squared(**moved, center_clear_dia_m=0.0)
70 held = _impinging_ring_geometry_squared(**moved, center_clear_dia_m=IGNITER_BOSS_M)
71 assert held == pytest.approx(base), "a ring well clear of the boss must pay nothing"
72
73
75 kw = dict(
76 d_jet_O=FINAL["d_jet_O_m"], d_jet_F=FINAL["d_jet_F_m"],
77 sp_O=FINAL["spacing_O_m"], sp_F=FINAL["spacing_F_m"],
78 D_chamber_inner=FINAL["D_chamber_inner_m"],
79 D_throat_check=0.0496, A_chamber_check=0.012668, A_throat_check=0.00193,
80 n_elements=FINAL["n_elements"],
81 angle_O_deg=FINAL["angle_O_deg"], angle_F_deg=FINAL["angle_F_deg"],
82 )
83 assert _impinging_hard_geometry_blocks_eval(**kw) is False, "FINAL was accepted before"
84 assert _impinging_hard_geometry_blocks_eval(**kw, center_clear_dia_m=IGNITER_BOSS_M) is True
85
86
87# ---------------------------------------------------------------------------------------
88# The elliptical face trace
89# ---------------------------------------------------------------------------------------
90
92 """A hole inclined theta from the axis prints as an ellipse of major axis d/cos(theta).
93
94 At 69 deg that is 2.79x the drill diameter. Using d would under-state the radial reach
95 of every orifice, which is exactly the number the clearance terms are made of.
96 """
97 d, bore = 0.001375, 0.127
98 n, sp = 28.0, math.pi * 0.030 / 28.0 # 30 mm pitch circle, fuel INBOARD
99 kw = dict(n_elements=n, spacing_O_m=math.pi * 0.090 / n, spacing_F_m=sp,
100 d_jet_O_m=0.0015821, d_jet_F_m=d, D_chamber_inner_m=bore,
101 angle_O_deg=40.0, ring_order_fuel_outboard=False)
102 # Clear circle sized to sit exactly between the round diameter and the ellipse.
103 edge_round = 0.5 * 0.030 - 0.5 * d
104 edge_ellipse = 0.5 * 0.030 - 0.5 * d / math.cos(math.radians(69.0))
105 clear = 2 * 0.5 * (edge_round + edge_ellipse)
106 assert edge_ellipse < 0.5 * clear < edge_round
107 # Isolate the clearance term: theta_F also moves tan_sum, hence L_imp, hence the standoff
108 # term, which is ~1e5 larger. Difference the same angle against its own zero-clearance run.
109 def _clearance_only(theta_F_deg):
110 with_c = _impinging_ring_geometry_squared(
111 **kw, angle_F_deg=theta_F_deg, center_clear_dia_m=clear)
112 without = _impinging_ring_geometry_squared(
113 **kw, angle_F_deg=theta_F_deg, center_clear_dia_m=0.0)
114 return with_c - without
115
116 steep, shallow = _clearance_only(69.0), _clearance_only(5.0)
117 assert shallow == pytest.approx(0.0, abs=1e-15), (
118 "a nearly-axial hole prints ~round, so its edge clears the circle and it pays nothing"
119 )
120 assert steep > 0.0, (
121 "a 69 deg hole reaches 2.79x further radially than its drill diameter; the clearance "
122 "term must see that"
123 )
124
125
126# ---------------------------------------------------------------------------------------
127# Web
128# ---------------------------------------------------------------------------------------
129
131 """spacing >= d_jet alone permits a web of exactly zero."""
132 web = FINAL["spacing_O_m"] - FINAL["d_jet_O_m"]
133 assert web == pytest.approx(0.001436, abs=1e-5), "fixture stale"
134 assert _impinging_ring_geometry_squared(**FINAL, min_web_m=0.001) == pytest.approx(
135 _impinging_ring_geometry_squared(**FINAL, min_web_m=0.0)), "1.0 mm floor is met"
136 assert _impinging_ring_geometry_squared(**FINAL, min_web_m=0.002) > \
137 _impinging_ring_geometry_squared(**FINAL, min_web_m=0.0), "2.0 mm floor is not"
138
139
141 kw = dict(
142 d_jet_O=FINAL["d_jet_O_m"], d_jet_F=FINAL["d_jet_F_m"],
143 sp_O=FINAL["spacing_O_m"], sp_F=FINAL["spacing_F_m"],
144 D_chamber_inner=FINAL["D_chamber_inner_m"],
145 D_throat_check=0.0496, A_chamber_check=0.012668, A_throat_check=0.00193,
146 n_elements=FINAL["n_elements"],
147 angle_O_deg=FINAL["angle_O_deg"], angle_F_deg=FINAL["angle_F_deg"],
148 )
149 assert _impinging_hard_geometry_blocks_eval(**kw, min_web_m=0.001) is False
150 assert _impinging_hard_geometry_blocks_eval(**kw, min_web_m=0.002) is True
151
152
153# ---------------------------------------------------------------------------------------
154# Wall clearance
155# ---------------------------------------------------------------------------------------
156
158 crowd = dict(FINAL)
159 crowd["spacing_F_m"] = math.pi * 0.120 / FINAL["n_elements"] # 120 mm ring in a 127 bore
160 assert _impinging_ring_geometry_squared(**crowd, wall_clearance_m=0.008) > \
161 _impinging_ring_geometry_squared(**crowd, wall_clearance_m=0.0)
162 # 88 mm ring in a 127 bore has 17.6 mm of land -- 8 mm must be free
163 assert _impinging_ring_geometry_squared(**FINAL, wall_clearance_m=0.008) == pytest.approx(
164 _impinging_ring_geometry_squared(**FINAL, wall_clearance_m=0.0))
165
166
167# ---------------------------------------------------------------------------------------
168# Defaults must not move
169# ---------------------------------------------------------------------------------------
170
172 """CLAUDE.md: new physics is opt-in and defaults to the previous behaviour, exactly."""
173 base = _impinging_ring_geometry_squared(**FINAL)
174 assert _impinging_ring_geometry_squared(
175 **FINAL, center_clear_dia_m=0.0, min_web_m=0.0, wall_clearance_m=0.0
176 ) == pytest.approx(base)
177 kw = dict(
178 d_jet_O=FINAL["d_jet_O_m"], d_jet_F=FINAL["d_jet_F_m"],
179 sp_O=FINAL["spacing_O_m"], sp_F=FINAL["spacing_F_m"],
180 D_chamber_inner=FINAL["D_chamber_inner_m"],
181 D_throat_check=0.0496, A_chamber_check=0.012668, A_throat_check=0.00193,
182 n_elements=FINAL["n_elements"],
183 )
184 assert _impinging_hard_geometry_blocks_eval(**kw) is False
185
186
187# ---------------------------------------------------------------------------------------
188# Machining: face incidence
189# ---------------------------------------------------------------------------------------
190
192 """theta is from the AXIS, so the drill meets the face at (90 - theta).
193
194 The shipped FINAL put the fuel jet at 69 deg -- 21 deg of incidence. A twist drill
195 entering a flat that shallow walks off the spot.
196 """
197 kw = dict(
198 d_jet_O=FINAL["d_jet_O_m"], d_jet_F=FINAL["d_jet_F_m"],
199 sp_O=FINAL["spacing_O_m"], sp_F=FINAL["spacing_F_m"],
200 D_chamber_inner=FINAL["D_chamber_inner_m"],
201 D_throat_check=0.0496, A_chamber_check=0.012668, A_throat_check=0.00193,
202 n_elements=FINAL["n_elements"],
203 angle_O_deg=FINAL["angle_O_deg"], angle_F_deg=FINAL["angle_F_deg"],
204 )
205 assert _impinging_hard_geometry_blocks_eval(**kw) is False, "inert when unset"
206 assert _impinging_hard_geometry_blocks_eval(**kw, min_face_incidence_deg=20.0) is False, \
207 "21 deg of incidence clears a 20 deg floor"
208 assert _impinging_hard_geometry_blocks_eval(**kw, min_face_incidence_deg=40.0) is True, \
209 "21 deg of incidence must not clear a 40 deg floor"
210
211
213 """40 / 49 deg -> 50 / 41 deg of incidence, both above a 40 deg floor."""
214 assert _impinging_hard_geometry_blocks_eval(
215 d_jet_O=0.0016380, d_jet_F=0.0014330,
216 sp_O=0.0090639037305711, sp_F=0.0119080988215582,
217 D_chamber_inner=0.127, D_throat_check=0.0488,
218 A_chamber_check=0.012668, A_throat_check=0.00187,
219 n_elements=27.0, angle_O_deg=40.0, angle_F_deg=49.0,
220 center_clear_dia_m=0.0381, min_web_m=0.002, wall_clearance_m=0.008,
221 min_face_incidence_deg=40.0,
222 ) is False
223
224
225# ---------------------------------------------------------------------------------------
226# The tilt allowance must follow the geometry, not sit still while it moves
227# ---------------------------------------------------------------------------------------
228
230 _resultant_tilt_breakeven_deg,
231 _resolve_tilt_allowance_deg,
232)
233
234SHIP = dict(n_elements=27.0, spacing_O_m=0.0090639037305711,
235 spacing_F_m=0.0119080988215582, angle_O_deg=40.0, angle_F_deg=49.0,
236 D_chamber_inner_m=0.127, L_chamber_m=0.15340)
237
238
240 be = _resultant_tilt_breakeven_deg(**SHIP)
241 r_imp = 0.5 * 0.08821 # from the emitted design
242 assert math.degrees(math.atan2(0.0635 - r_imp, SHIP["L_chamber_m"])) == pytest.approx(be, abs=0.05)
243 # and a fan at exactly that angle lands exactly one chamber length downstream
244 assert (0.0635 - r_imp) / math.tan(math.radians(be)) == pytest.approx(SHIP["L_chamber_m"], rel=2e-3)
245
246
248 """A ring pair further out has less room, so it must be allowed less tilt."""
249 near = _resultant_tilt_breakeven_deg(**SHIP)
250 out = dict(SHIP)
251 out["spacing_O_m"] *= 1.25 # push both rings outward
252 out["spacing_F_m"] *= 1.25
253 far = _resultant_tilt_breakeven_deg(**out)
254 assert far < near, "a ring closer to the liner must earn a smaller allowance"
255 a_near = _resolve_tilt_allowance_deg(from_reach=True, constant_deg=6.0,
256 breakeven_deg=near, margin=1.5)
257 a_far = _resolve_tilt_allowance_deg(from_reach=True, constant_deg=6.0,
258 breakeven_deg=far, margin=1.5)
259 assert a_far < a_near < near, "the margin must cut the allowance below break-even"
260
261
263 """margin = 1.5 => the fan reaches the liner at 1.5 chamber lengths, not 1.0."""
264 be = _resultant_tilt_breakeven_deg(**SHIP)
265 allowed = _resolve_tilt_allowance_deg(from_reach=True, constant_deg=0.0,
266 breakeven_deg=be, margin=1.5)
267 r_imp = 0.5 * 0.08821
268 reach = (0.0635 - r_imp) / math.tan(math.radians(allowed))
269 assert reach == pytest.approx(1.5 * SHIP["L_chamber_m"], rel=5e-3)
270
271
273 """CLAUDE.md: defaults to the previous behaviour, exactly."""
274 assert _resolve_tilt_allowance_deg(
275 from_reach=False, constant_deg=6.0, breakeven_deg=7.21, margin=1.5) == 6.0
276 # degenerate geometry must not silently forbid every candidate
277 for bad in (float("nan"), 0.0, -3.0):
278 assert _resolve_tilt_allowance_deg(
279 from_reach=True, constant_deg=6.0, breakeven_deg=bad, margin=1.5) == 6.0
280
281
282# ---------------------------------------------------------------------------------------
283# Where the propellant actually lands
284# ---------------------------------------------------------------------------------------
285
286EQUAL_AREA = 1.0 / math.sqrt(2.0) # splits the chamber cross-section in half
287
288
289def _r_imp(kw):
290 n = kw["n_elements"]
291 dpo, dpf = n * kw["spacing_O_m"] / math.pi, n * kw["spacing_F_m"] / math.pi
292 tan_sum = math.tan(math.radians(kw["angle_O_deg"])) + math.tan(math.radians(kw["angle_F_deg"]))
293 L = 0.5 * abs(dpo - dpf) / tan_sum
294 th_in = kw["angle_O_deg"] if dpo <= dpf else kw["angle_F_deg"]
295 return 0.5 * min(dpo, dpf) + L * math.tan(math.radians(th_in))
296
297
299 """FINAL put every element on a circle at 0.33 of the bore radius."""
300 assert _r_imp(FINAL) / (0.5 * FINAL["D_chamber_inner_m"]) == pytest.approx(0.329, abs=0.005)
301
302
304 base = _impinging_ring_geometry_squared(**FINAL)
305 assert _impinging_ring_geometry_squared(**FINAL, spray_radius_frac=0.0) == pytest.approx(base)
306
307
309 """0.33 of the radius is 10.8 % of the area -- it must cost something against 0.707."""
310 free = _impinging_ring_geometry_squared(**FINAL)
311 held = _impinging_ring_geometry_squared(**FINAL, spray_radius_frac=EQUAL_AREA)
312 assert held > free
313
314
316 """A ring pair at the equal-area radius must pay exactly nothing."""
317 kw = dict(FINAL)
318 # slide BOTH rings out together: same gap, same standoff, same L/d -- only the radius moves
319 target_r = EQUAL_AREA * 0.5 * FINAL["D_chamber_inner_m"]
320 shift = target_r - _r_imp(FINAL)
321 d_spacing = 2.0 * shift * math.pi / FINAL["n_elements"]
322 kw["spacing_O_m"] = FINAL["spacing_O_m"] + d_spacing
323 kw["spacing_F_m"] = FINAL["spacing_F_m"] + d_spacing
324 assert _r_imp(kw) / (0.5 * kw["D_chamber_inner_m"]) == pytest.approx(EQUAL_AREA, abs=1e-6)
325 base = _impinging_ring_geometry_squared(**kw)
326 held = _impinging_ring_geometry_squared(**kw, spray_radius_frac=EQUAL_AREA)
327 assert held == pytest.approx(base), "on target must be free"
328
329
331 """Just outside the band costs; just inside does not."""
332 def at(frac_target, tol):
333 return _impinging_ring_geometry_squared(
334 **FINAL, spray_radius_frac=frac_target, spray_radius_tol=tol)
335 base = _impinging_ring_geometry_squared(**FINAL)
336 actual = _r_imp(FINAL) / (0.5 * FINAL["D_chamber_inner_m"]) # 0.329
337 assert at(actual + 0.05, 0.08) == pytest.approx(base), "inside the band is free"
338 assert at(actual + 0.20, 0.08) > base, "outside the band is not"
339
340
342 """The soft term cost 1.5 points against an objective of ~2690, so every seed bought a
343 spray circle outside its own declared band. Hard, like ring fit."""
344 kw = dict(
345 d_jet_O=FINAL["d_jet_O_m"], d_jet_F=FINAL["d_jet_F_m"],
346 sp_O=FINAL["spacing_O_m"], sp_F=FINAL["spacing_F_m"],
347 D_chamber_inner=FINAL["D_chamber_inner_m"],
348 D_throat_check=0.0496, A_chamber_check=0.012668, A_throat_check=0.00193,
349 n_elements=FINAL["n_elements"],
350 angle_O_deg=FINAL["angle_O_deg"], angle_F_deg=FINAL["angle_F_deg"],
351 )
352 assert _impinging_hard_geometry_blocks_eval(**kw) is False, "inert when unset"
353 # FINAL sits at 0.329 of the bore radius; the equal-area target is 0.707
354 assert _impinging_hard_geometry_blocks_eval(
355 **kw, spray_radius_frac=EQUAL_AREA, spray_radius_tol=0.08) is True
356 # a band wide enough to contain it must let it through
357 assert _impinging_hard_geometry_blocks_eval(
358 **kw, spray_radius_frac=EQUAL_AREA, spray_radius_tol=0.40) is False
359
360
362 """A limit only one path applies is not a limit.
363
364 The serial loop's _impinging_hard_geometry_blocks_eval never ran in the parallel CMA
365 workers, where essentially every candidate is scored. Two of three seeds converged
366 outside their own declared spray-radius band and reported ALL GATES PASS.
367 """
368 import inspect
369 from engine.optimizer.layers import layer1_static_optimization as L1
370 src = inspect.getsource(L1)
371 worker = src[src.index("def _compute_objective_value"):]
372 worker = worker[:worker.index("\ndef ", 10)]
373 assert "_impinging_face_infeasibility_terms(" in worker, (
374 "_compute_objective_value does not apply the face limits; the parallel workers "
375 "would score a violating candidate as feasible"
376 )
377 for key in ("layer1_injector_spray_radius_frac", "layer1_injector_center_clear_dia_m",
378 "layer1_injector_wall_clearance_m", "layer1_injector_min_web_m"):
379 assert key in worker, f"worker path never reads {key}"
380
381
383 """A binary block puts the whole violating region on one flat 1e6 plateau.
384
385 Measured: with `infeasibility_score += 1.0`, 2 of 3 seeds never found the feasible set
386 at all (converged O/F 2.09 and 25.6 against a 1.65 target). The term must carry a
387 gradient pointing back toward the band.
388 """
390 _impinging_face_infeasibility_terms,
391 )
392 base = dict(
393 n_elements=FINAL["n_elements"], spacing_O_m=FINAL["spacing_O_m"],
394 spacing_F_m=FINAL["spacing_F_m"], d_jet_O_m=FINAL["d_jet_O_m"],
395 d_jet_F_m=FINAL["d_jet_F_m"], D_chamber_inner_m=FINAL["D_chamber_inner_m"],
396 angle_O_deg=FINAL["angle_O_deg"], angle_F_deg=FINAL["angle_F_deg"],
397 )
398 assert _impinging_face_infeasibility_terms(**base) == 0.0, "inert when nothing is declared"
399
400 # Walk the ring pair outward toward the target. Declare ONLY the spray band, so the
401 # monotonicity claim is about that term and is not confounded by the wall-clearance term
402 # taking over once the outer ring runs out of chamber (which it correctly does).
403 scores = []
404 for mult in (1.0, 1.4, 1.8, 2.2):
405 kw = dict(base)
406 kw["spacing_O_m"] = FINAL["spacing_O_m"] * mult
407 kw["spacing_F_m"] = FINAL["spacing_F_m"] + (kw["spacing_O_m"] - FINAL["spacing_O_m"])
408 scores.append(_impinging_face_infeasibility_terms(
409 **kw, spray_radius_frac=EQUAL_AREA, spray_radius_tol=0.08))
410 assert scores[0] > 0.0, "the shipped bug must register as infeasible"
411 for a, b in zip(scores, scores[1:]):
412 assert b < a, f"no gradient: {scores} -- CMA cannot descend a flat plateau"
413
414
416 """Pushing the rings out to hit the spray target must not smuggle them past the wall.
417
418 At a fixed ring GAP, sliding the pair out far enough to reach the equal-area radius puts
419 the fuel ring at 136 mm on a 127 mm bore. The spray term is then satisfied and the wall
420 term is not, and the total must rise -- the sum is a constraint set, not a score to game.
421 """
423 _impinging_face_infeasibility_terms,
424 )
425 kw = dict(
426 n_elements=FINAL["n_elements"], d_jet_O_m=FINAL["d_jet_O_m"],
427 d_jet_F_m=FINAL["d_jet_F_m"], D_chamber_inner_m=FINAL["D_chamber_inner_m"],
428 angle_O_deg=FINAL["angle_O_deg"], angle_F_deg=FINAL["angle_F_deg"],
429 )
430 kw["spacing_O_m"] = FINAL["spacing_O_m"] * 2.8
431 kw["spacing_F_m"] = FINAL["spacing_F_m"] + (kw["spacing_O_m"] - FINAL["spacing_O_m"])
432 assert _r_imp(dict(kw, spacing_O_m=kw["spacing_O_m"], spacing_F_m=kw["spacing_F_m"])) \
433 / (0.5 * kw["D_chamber_inner_m"]) == pytest.approx(EQUAL_AREA, abs=0.01)
434 spray_only = _impinging_face_infeasibility_terms(
435 **kw, spray_radius_frac=EQUAL_AREA, spray_radius_tol=0.08)
436 with_wall = _impinging_face_infeasibility_terms(
437 **kw, spray_radius_frac=EQUAL_AREA, spray_radius_tol=0.08, wall_clearance_m=0.008)
438 # On the spray target, but the fuel trace reaches r 70.13 mm in a 63.50 mm bore. That is
439 # infeasible with no wall land declared at all -- the orifice is drilled into the liner --
440 # so the bore itself is checked at a clearance of 0: (70.13 - 63.50)/127 = 0.0522, squared.
441 assert spray_only == pytest.approx(0.002730, rel=1e-3), (
442 "a trace across the bore must count even when no wall land is declared")
443 # fuel ring at 136.43 mm + a 1.92 mm elliptical half-trace = 70.13 mm of radius, 8 mm of
444 # land wanted, against a 63.50 mm bore radius: (70.13 + 8 - 63.50)/127 = 0.1152, squared.
445 assert with_wall == pytest.approx(0.01328, rel=1e-3), (
446 "the fuel ring is 14.6 mm outside where the wall land allows, and that must be what "
447 "the total reports once the spray term is satisfied")
448 assert with_wall > spray_only
449
450
453 _impinging_face_infeasibility_terms, _layer1_centre_clear_m,
454 )
455 from engine.pipeline.io import load_config
456 import yaml as _yaml
457 c = _yaml.safe_load(open("configs/ethalox_8kN_SHIP.yaml"))
458 g = c["injector"]["geometry"]
459 rq = c["design_requirements"]
460 assert _impinging_face_infeasibility_terms(
461 n_elements=float(g["oxidizer"]["n_elements"]),
462 spacing_O_m=g["oxidizer"]["spacing"], spacing_F_m=g["fuel"]["spacing"],
463 d_jet_O_m=g["oxidizer"]["d_jet"], d_jet_F_m=g["fuel"]["d_jet"],
464 D_chamber_inner_m=c["chamber_geometry"]["chamber_diameter"],
465 angle_O_deg=g["oxidizer"]["impingement_angle"],
466 angle_F_deg=g["fuel"]["impingement_angle"],
467 # The limits the YAML declares, not a copy of them: a copy stays green when someone
468 # tightens the config and the design no longer meets it.
469 center_clear_dia_m=_layer1_centre_clear_m(load_config("configs/ethalox_8kN_SHIP.yaml"), rq),
470 min_web_m=rq["layer1_injector_min_web_m"],
471 wall_clearance_m=rq["layer1_injector_wall_clearance_m"],
472 spray_radius_frac=rq["layer1_injector_spray_radius_frac"],
473 spray_radius_tol=rq["layer1_injector_spray_radius_tol"],
474 min_face_incidence_deg=rq["layer1_injector_min_face_incidence_deg"],
475 ) == 0.0, "the shipped design must violate none of its own declared face limits"
#define min(a, b)
Definition Arduino.h:42
T & at(const key_type &key)
Definition json.hpp:19110