STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
test_default_doublet_cd.py
Go to the documentation of this file.
1"""A blank doublet design flows its holes as the drilled passages the layout draws.
2
3The layout drills every doublet hole as an L/d-4 passage (SP-8089 3.1.2.2: "an orifice L/d of
4at least 4"), so a design started from configs/default.yaml (what a new session loads) or
5configs/canonical/impinging.yaml (what an injector switch loads) must use that passage's Cd. A
6sharp-entry drilled hole reattaches inside the bore: Lichtarowicz, Duggins & Markland (1965),
7Cd_u = 0.827 - 0.0085 L/d for 2 <= L/d <= 10, i.e. 0.793 at L/d 4. Both blank configs used to
8fall through to the thin-plate diameter model, 0.60 x (d / 2 mm)^0.2 = 0.566 at 1.5 mm: holes
9sized on it come out ~1.4x too large in area and run at half the design injector dp.
10
11That small-hole exponent also had the wrong sign. ISO 5167 and Sutton & Biblarz Table 8-2 both
12have small sharp holes flowing slightly more, not less; the thin-plate path is now the flat
130.60 asymptote below d_ref.
14
15Expected values are the published fit and an energy balance built on ``fluids``, not the module.
16"""
17from __future__ import annotations
18
19import math
20from pathlib import Path
21
22import pytest
23
24from engine.core.discharge import cd_inf_from_inlet_geometry, cd_inf_from_orifice_diameter
25from engine.pipeline.config_schemas import DischargeConfig
26
27ROOT = Path(__file__).resolve().parents[1]
28BLANKS = ["configs/default.yaml", "configs/canonical/impinging.yaml"]
29
30
31@pytest.fixture(autouse=True, scope="module")
33 """The authoritative Python path; engine/accel mirrors it and test_numba_ab_parity holds the
34 two together."""
35 from engine import accel
36 real = accel.enabled, accel.require
37 accel.enabled = lambda: False
38 accel.require = lambda: False
39 try:
40 yield
41 finally:
42 accel.enabled, accel.require = real
43
44
45def _lichtarowicz(ld: float) -> float:
46 return 0.827 - 0.0085 * ld
47
48
49def _short_tube_band(ld: float, Re: float = 3.0e5):
50 """Energy balance through a short tube, Cd = (1 + K_entrance + f L/d)^-1/2, with the sharp
51 entrance K from Rennels (0.57) and Idelchik (0.5) and a smooth-wall Colebrook f."""
52 from fluids.fittings import entrance_sharp
53 from fluids.friction import friction_factor
54 f = friction_factor(Re=Re, eD=0.0)
55 cds = [1.0 / math.sqrt(1.0 + entrance_sharp(method=m) + f * ld) for m in ("Rennels", "Idelchik")]
56 return min(cds), max(cds)
57
58
59@pytest.mark.parametrize("path", BLANKS)
60@pytest.mark.parametrize("side", ["oxidizer", "fuel"])
62 from engine.pipeline.io import load_config
63 from engine.pipeline.config_switch import switch_config
64 cfg = switch_config(load_config(str(ROOT / path)).model_dump(mode="json"), propellant_preset="ethalox")
65 dc = DischargeConfig(**cfg["discharge"][side])
66 cd = cd_inf_from_orifice_diameter(1.5e-3, dc)
67 lo, hi = _short_tube_band(4.0)
68 assert lo <= cd <= hi, f"{path} {side}: Cd {cd:.4f} outside the short-tube energy balance {lo:.4f}-{hi:.4f}"
69 assert cd == pytest.approx(_lichtarowicz(4.0), abs=2e-3), (
70 f"{path} {side}: Cd {cd:.4f}; a sharp L/d-4 hole is {_lichtarowicz(4.0):.4f} (Lichtarowicz 1965)")
71
72
74 """One number drives the drawing and the Cd: the layout's passage L/d is the discharge L/d."""
75 import yaml
76 from engine.core.injectors.layout import ORIFICE_LD_MIN
77 for path in BLANKS:
78 dis = yaml.safe_load((ROOT / path).read_text())["discharge"]
79 for side in ("oxidizer", "fuel"):
80 assert dis[side]["inlet_geometry"] == "sharp"
81 assert dis[side]["orifice_l_over_d"] == ORIFICE_LD_MIN
82
83
84@pytest.mark.parametrize("ld", [2.0, 4.0, 5.0, 8.0, 10.0])
86 """No plateau over L/d 2-5: the default is Lichtarowicz, not the piecewise curve."""
87 dc = DischargeConfig(Cd_inf=0.6, a_Re=0.18, inlet_geometry="sharp", orifice_l_over_d=ld)
88 assert cd_inf_from_inlet_geometry(dc) == pytest.approx(_lichtarowicz(ld), abs=1e-9)
89
90
92 from engine.pipeline import assumptions
93 dc = DischargeConfig(Cd_inf=0.6, a_Re=0.18, inlet_geometry="sharp", orifice_l_over_d=13.0)
94 with assumptions.scope() as used:
95 cd_inf_from_inlet_geometry(dc)
96 assert any("orifice_l_over_d" in k for k in used), used
97
98
99@pytest.mark.parametrize("d_mm", [0.8, 1.0, 1.5, 1.9])
101 """Below d_ref the thin-plate path is the 0.60 sharp-orifice asymptote (ISO 5167 C ~ 0.60-0.62),
102 not 0.60 x (d/2 mm)^0.2."""
103 dc = DischargeConfig(Cd_inf=0.60, a_Re=0.18)
104 assert 0.60 - 1e-12 <= cd_inf_from_orifice_diameter(d_mm * 1e-3, dc) <= 0.62
105
106
108 """An impinging config that still declares no inlet is told its Cd is a thin plate."""
109 from engine.pipeline import assumptions
110 from engine.pipeline.config_schemas import PintleEngineConfig
111 from engine.core.injectors.impinging import ImpingingInjector
112 from engine.pipeline.io import load_config
113 base = load_config(str(ROOT / "configs/ethalox_6500N.yaml")).model_dump()
114 base["discharge"] = {s: {**base["discharge"][s], "inlet_geometry": None, "orifice_l_over_d": None}
115 for s in ("oxidizer", "fuel")}
116 cfg = PintleEngineConfig.model_validate(base)
117 PSI = 6894.757
118 with assumptions.scope() as used:
119 ImpingingInjector(cfg).solve(584.27 * PSI, 584.27 * PSI, 430.0 * PSI)
120 assert "discharge.oxidizer.inlet_geometry" in used and "discharge.fuel.inlet_geometry" in used, used
#define min(a, b)
Definition Arduino.h:42
#define max(a, b)
Definition Arduino.h:43
_short_tube_band(float ld, float Re=3.0e5)
test_blank_doublet_gets_the_drilled_hole_cd(path, side)