STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
test_orifice_ld_from_plate.py
Go to the documentation of this file.
1"""Orifice L/d from the plate as built, and the Cd it gives (docs/injector_face_and_manifold_plan.md, phase 6).
2
3The discharge block used to take L/d as a typed number while the plate thickness, hole angle
4and counterbore that decide it lived elsewhere. On SHIP the declared 4.0 is right only
5because a 4 mm counterbore is drilled; skip it and the small drill runs L/d 9.7 (LOX) and
613.2 (fuel), Cd falls to 0.745 / 0.715, and the engine makes 3.1 % less thrust at O/F 1.70.
7"""
8from __future__ import annotations
9
10import copy
11import math
12from contextlib import contextmanager
13from pathlib import Path
14
15import pytest
16
17from engine.core.discharge import (
18 LICHTAROWICZ_A, LICHTAROWICZ_B, cd_inf_from_inlet_geometry, cd_length_factor, cd_with_approach,
19)
20from engine.core.injectors.layout import effective_discharge, realized_land
21
22ROOT = Path(__file__).resolve().parents[1]
23PSI = 6894.757
24SHIP = ROOT / "configs/ethalox_8kN_SHIP.yaml"
25
26
27def _ship(src=None, model=None, counterbore="keep"):
28 from engine.pipeline.io import load_config
29 from engine.pipeline.config_schemas import InjectorPlateConfig
30 c = load_config(str(SHIP))
31 # The ship is drilled through a flat plate into a plenum (an undeclared plate now draws the
32 # stand's contoured, channelled plug).
33 c.injector.plate = InjectorPlateConfig(face="flat", back="plenum")
34 for side in ("oxidizer", "fuel"):
35 if src:
36 c.discharge[side].l_over_d_source = src
37 if model:
38 c.discharge[side].length_model = model
39 if counterbore != "keep":
40 c.design_requirements.layer1_injector_counterbore_dia_m = counterbore
41 return c
42
43
44# ---- the default moves nothing ---------------------------------------------------------------
45
47 c = _ship()
48 assert effective_discharge(c, "oxidizer") is c.discharge["oxidizer"]
49
50
52 # 'piecewise' is now a named legacy option; the default length model is Lichtarowicz.
53 assert cd_length_factor(4.0, "piecewise") == 1.0
54 assert cd_length_factor(10.0, "piecewise") == pytest.approx(0.94)
55 assert cd_length_factor(0.0) == 1.0 # non-positive -> no correction, as before
56 assert cd_with_approach(0.8, None) == 0.8
57
58
59# ---- Lichtarowicz ------------------------------------------------------------------------------
60
61@pytest.mark.parametrize("ld", [2.0, 4.0, 7.0, 10.0])
63 """Sharp inlet (table 0.80) x factor must equal Cd_u = 0.827 - 0.0085 L/d exactly."""
64 assert 0.80 * cd_length_factor(ld, "lichtarowicz") == pytest.approx(0.827 - 0.0085 * ld, rel=1e-12)
65
66
68 assert 0.80 * cd_length_factor(1e-9, "lichtarowicz") == pytest.approx(0.61, abs=1e-6)
69 # continuous at L/d 2
70 assert cd_length_factor(2.0 - 1e-9, "lichtarowicz") == pytest.approx(
71 cd_length_factor(2.0, "lichtarowicz"), abs=1e-8)
72
73
74# ---- counterbore velocity of approach -------------------------------------------------------
75
77 """Plenum -> counterbore (entrance K) -> orifice; solve the drops for a mass flow, compare."""
78 cd, beta, K = 0.80, 0.45, 0.5
79 rho, A, dp_total = 1000.0, 1e-6, 1e6
80 # unknown v (orifice velocity); counterbore velocity beta^2 v
81 # dp_orifice (static, tap in the counterbore) = rho v^2 (1 - beta^4) / (2 cd^2)
82 # dp_entrance = rho (beta^2 v)^2 (1 + K) / 2
83 v = math.sqrt(2 * dp_total / (rho * ((1 - beta ** 4) / cd ** 2 + beta ** 4 * (1 + K))))
84 mdot = rho * v * A
85 cd_eff = mdot / (A * math.sqrt(2 * rho * dp_total))
86 assert cd_with_approach(cd, beta, K) == pytest.approx(cd_eff, rel=1e-12)
87
88
89# ---- the land the plate gives ----------------------------------------------------------------
90
92 rl = realized_land(d=0.0017094, theta_deg=40.0, plate_thickness=0.0127,
93 counterbore_dia=0.0, declared_land_ld=4.0)
94 assert rl["land_ld"] == pytest.approx(0.0127 / math.cos(math.radians(40)) / 0.0017094)
95 assert rl["beta"] is None
96
97
99 rl = realized_land(d=0.0017094, theta_deg=40.0, plate_thickness=0.0127,
100 counterbore_dia=0.004, declared_land_ld=4.0)
101 assert rl["land_ld"] == pytest.approx(4.0)
102 assert rl["beta"] == pytest.approx(0.0017094 / 0.004)
103
104
106 rl = realized_land(d=0.001, theta_deg=0.0, plate_thickness=0.003,
107 counterbore_dia=0.004, declared_land_ld=10.0)
108 assert rl["land_ld"] == pytest.approx(3.0) and rl["beta"] is None
109
110
112 c = _ship(src="plate", model="lichtarowicz", counterbore=None)
113 eO, eF = effective_discharge(c, "oxidizer"), effective_discharge(c, "fuel")
114 assert eO.orifice_l_over_d == pytest.approx(9.70, abs=0.01)
115 assert eF.orifice_l_over_d == pytest.approx(13.15, abs=0.01)
116 assert cd_inf_from_inlet_geometry(eF) < cd_inf_from_inlet_geometry(eO) < 0.80
117
118
119# ---- both solver paths agree -----------------------------------------------------------------
120
121@contextmanager
123 from engine import accel
124 real_enabled, real_require = accel.enabled, accel.require
125 accel.enabled = lambda: False
126 accel.require = lambda: False
127 try:
128 yield
129 finally:
130 accel.enabled, accel.require = real_enabled, real_require
131
132
133@pytest.mark.parametrize("counterbore", ["keep", None], ids=["counterbored", "bare"])
135 from engine import accel
136 from engine.core.runner import PintleEngineRunner
137 if not accel.available():
138 pytest.skip("numba unavailable")
139 c = _ship(src="plate", model="lichtarowicz", counterbore=counterbore)
140 runner = PintleEngineRunner(c)
141 P = (c.lox_tank.initial_pressure_psi * PSI, c.fuel_tank.initial_pressure_psi * PSI)
142 from engine.core.injectors import get_injector_model
143 with _python_only():
144 ref = runner.evaluate(*P, P_ambient=101325.0, silent=True)
145 # The plate's L/d has to arrive in the injector kernel (accel.solve, which the default path
146 # runs on every residual) and through it in the chamber kernel Layer 1 runs.
147 full = accel.evaluate(c, runner.cea_cache, *P, 101325.0)
148 assert full is not None
149 assert full["F"] == pytest.approx(ref["F"], rel=1e-6)
150 got = accel.solve(c, *P, ref["Pc"])
151 assert got is not None
152 mO, mF, _ = get_injector_model(c).solve(*P, ref["Pc"])
153 assert got[0] == pytest.approx(mO, rel=1e-9)
154 assert got[1] == pytest.approx(mF, rel=1e-9)
155 assert mO == pytest.approx(ref["mdot_O"], rel=1e-6)
156 # Bare, the drill runs L/d 9.7/13.2 against the declared 4.0: a different engine, or the
157 # option did nothing. Counterbored, the plate gives the declared 4.0 (see the module
158 # docstring) and the Cd model is the default Lichtarowicz, so the two must agree instead.
159 base = PintleEngineRunner(_ship()).evaluate(*P, P_ambient=101325.0, silent=True)
160 if counterbore is None:
161 assert abs(ref["F"] - base["F"]) / base["F"] > 1e-3
162 else:
163 assert ref["F"] == pytest.approx(base["F"], rel=1e-3)
164
165
166# ---- cavitation margin (Nurick 1976), reporting only ------------------------------------------
167
168from engine.core.discharge import cavitation_margin, contraction_coefficient # noqa: E402
169
170
172 assert contraction_coefficient(0.0) == pytest.approx(0.62)
173 assert contraction_coefficient(0.1) == pytest.approx((1 / 0.62 ** 2 - 1.14) ** -0.5)
174 assert contraction_coefficient(1.0) == pytest.approx(0.98) # capped, well rounded
175
176
178 m = cavitation_margin(P_in=4.0e6, Pc=3.0e6, Pv=1.0e5, Cd=0.80)
179 assert m["K"] == pytest.approx((4.0e6 - 1.0e5) / 1.0e6)
180 assert m["K_crit"] == pytest.approx((0.80 / 0.62) ** 2)
181 assert m["margin"] == pytest.approx(m["K"] / m["K_crit"])
182
183
185 from engine.core.runner import PintleEngineRunner
186 c = _ship()
187 P = (c.lox_tank.initial_pressure_psi * PSI, c.fuel_tank.initial_pressure_psi * PSI)
188 cav = PintleEngineRunner(c).evaluate(*P, silent=True)["injector_cavitation"]
189 assert cav["O"]["margin"] > 2.0 and cav["F"]["margin"] > 2.0
190 # LOX sitting saturated at 500 psia -- the self-pressurisation case feed-twin models --
191 # leaves the vena contracta at vapour pressure.
192 c.fluids["oxidizer"].vapor_pressure = 500 * PSI
193 cav = PintleEngineRunner(c).evaluate(*P, silent=True)["injector_cavitation"]
194 # Cd is now held at Cc sqrt(K) (Nurick): the hole runs AT the cavitation line, margin 1.
195 assert cav["O"]["margin"] == pytest.approx(1.0, abs=1e-6)
196
197
199 """Cavitation used to be reported only; a vena contracta at vapour pressure now caps Cd at
200 Cc sqrt(K) (Nurick 1976), so saturated LOX flows less and the engine makes less thrust."""
201 from engine.core.runner import PintleEngineRunner
202 c = _ship()
203 P = (c.lox_tank.initial_pressure_psi * PSI, c.fuel_tank.initial_pressure_psi * PSI)
204 base = PintleEngineRunner(c).evaluate(*P, silent=True)
205 c.fluids["oxidizer"].vapor_pressure = 500 * PSI
206 warm = PintleEngineRunner(c).evaluate(*P, silent=True)
207 assert warm["mdot_O"] < 0.9 * base["mdot_O"]
208 assert warm["F"] < base["F"]
_ship(src=None, model=None, counterbore="keep")