STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
test_feed_friction_fittings.py
Go to the documentation of this file.
1"""Feed-line loss from an itemised run: Colebrook friction at the actual Re, plus fittings.
2
3 P_tank - P_manifold = (K_base + sum K_fittings + K1 phi(P)) q_line + K_exit q_exit
4 K_base = K0 (roughness_m None: the measured override)
5 = K_entrance + f(Re, e/D) L / D (roughness_m set and mu given)
6
7f is the Darcy factor from Colebrook-White. References: fluids.friction.Colebrook (the exact
8Lambert-W solution) and Moody (1944) chart reads. Everything else is hand arithmetic.
9
10Both new inputs are opt-in: a drawing that declares neither must give exactly the old number.
11"""
12from __future__ import annotations
13
14import math
15from pathlib import Path
16
17import pytest
18from pydantic import ValidationError
19
20from engine.pipeline.config_schemas import FeedSystemConfig
21from engine.pipeline.feed_loss import colebrook_darcy, darcy_friction_factor, delta_p_feed
22
23ROOT = Path(__file__).resolve().parents[1]
24PSI_TO_PA = 6894.76
25D_TUBE = 0.010922 # 1/2" x 0.035" tube bore
26A_TUBE = math.pi / 4.0 * D_TUBE ** 2
27EPS_DRAWN_SS = 1.5e-6 # drawn stainless tube, Crane TP-410 A-23
28
29
30def _q(mdot, rho, A):
31 v = mdot / (rho * A)
32 return 0.5 * rho * v * v
33
34
35# Moody (1944) chart reads, +-1 %: smooth Re 1e5 -> 0.0180; e/D 1e-4 at Re 1e6 -> 0.0134;
36# e/D 1e-3 at Re 1e4 -> 0.0324.
37MOODY = [(1.0e5, 0.0, 0.0180), (1.0e6, 1.0e-4, 0.0134), (1.0e4, 1.0e-3, 0.0324)]
38
39
40@pytest.mark.parametrize("Re,eD,f_chart", MOODY)
42 assert colebrook_darcy(Re, eD) == pytest.approx(f_chart, rel=0.01)
43 assert darcy_friction_factor(Re, eD) == pytest.approx(f_chart, rel=0.01)
44
45
46@pytest.mark.parametrize("Re,eD,_", MOODY)
48 """The no-fluids fallback solves the same equation to machine precision."""
49 fr = pytest.importorskip("fluids.friction")
50 assert colebrook_darcy(Re, eD) == pytest.approx(fr.Colebrook(Re, eD), rel=1e-12)
51 assert darcy_friction_factor(Re, eD) == pytest.approx(fr.Colebrook(Re, eD), rel=1e-12)
52
53
55 assert colebrook_darcy(1000.0, 1e-3) == pytest.approx(0.064, rel=1e-15)
56 assert darcy_friction_factor(1000.0, 1e-3) == pytest.approx(0.064, rel=1e-15)
57
58
59def _fuel(**kw):
60 base = dict(line_size="1/2_TUBE_035", K0=2.019, K1=0.0, length=0.9144)
61 base.update(kw)
62 return FeedSystemConfig(**base)
63
64
66 fit = [{"name": "1/2 in full-bore ball valve", "K": 0.081, "source": "Crane TP-410, 3 f_T"},
67 {"name": "90 deg tube bend r/d 3", "K": 0.324, "source": "Crane TP-410, 12 f_T"}]
68 cfg = _fuel(fittings=fit)
69 dp = delta_p_feed(1.118, 789.0, cfg, 4.0e6)
70 assert dp == pytest.approx((2.019 + 0.081 + 0.324 + 1.0) * _q(1.118, 789.0, A_TUBE), rel=1e-12)
71 # and they add to the pressure-dependent term, not replace it
72 cfg = _fuel(fittings=fit, K1=1e-4, phi_type="sqrtP")
73 dp = delta_p_feed(1.118, 789.0, cfg, 4.0e6)
74 K = 2.019 + 0.405 + 1e-4 * math.sqrt(4.0e6)
75 assert dp == pytest.approx((K + 1.0) * _q(1.118, 789.0, A_TUBE), rel=1e-12)
76
77
79 with pytest.raises(ValidationError):
80 _fuel(fittings=[{"name": "valve", "K": 2.0, "source": ""}])
81 with pytest.raises(ValidationError):
82 _fuel(fittings=[{"name": "valve", "K": 2.0}])
83
84
86 mdot, rho, mu = 1.118, 789.0, 1.2e-3
87 fit = [{"name": "ball valve", "K": 0.081, "source": "Crane TP-410, 3 f_T"}]
88 cfg = _fuel(roughness_m=EPS_DRAWN_SS, fittings=fit)
89 v = mdot / (rho * A_TUBE)
90 Re = rho * v * D_TUBE / mu
91 fr = pytest.importorskip("fluids.friction")
92 f = fr.Colebrook(Re, EPS_DRAWN_SS / D_TUBE)
93 K = 0.5 + f * 0.9144 / D_TUBE + 0.081
94 assert delta_p_feed(mdot, rho, cfg, 4.0e6, mu=mu) == pytest.approx((K + 1.0) * _q(mdot, rho, A_TUBE), rel=1e-12)
95 # f is evaluated at THIS call's Re: halving the flow raises f, so dp falls by less than 4x.
96 f2 = fr.Colebrook(Re / 2, EPS_DRAWN_SS / D_TUBE)
97 K2 = 0.5 + f2 * 0.9144 / D_TUBE + 0.081
98 assert f2 > f
99 assert delta_p_feed(mdot / 2, rho, cfg, 4.0e6, mu=mu) == pytest.approx((K2 + 1.0) * _q(mdot / 2, rho, A_TUBE), rel=1e-12)
100 # K_entrance is a field
101 cfg = _fuel(roughness_m=EPS_DRAWN_SS, K_entrance=0.04)
102 K3 = 0.04 + f * 0.9144 / D_TUBE
103 assert delta_p_feed(mdot, rho, cfg, 4.0e6, mu=mu) == pytest.approx((K3 + 1.0) * _q(mdot, rho, A_TUBE), rel=1e-12)
104
105
107 cfg = _fuel(roughness_m=EPS_DRAWN_SS)
108 assert delta_p_feed(1.118, 789.0, cfg, 4.0e6) == pytest.approx(3.019 * _q(1.118, 789.0, A_TUBE), rel=1e-12)
109
110
112 with pytest.raises(ValidationError):
113 FeedSystemConfig(line_size="1/2_TUBE_035", K0=2.019, K1=0.0, roughness_m=EPS_DRAWN_SS)
114
115
117 """ethalox_6500N declares neither fittings nor roughness_m: old formula to 1e-12, with or
118 without a viscosity passed."""
119 from engine.pipeline.io import load_config
120 config = load_config(str(ROOT / "configs/ethalox_6500N.yaml"))
121 for side, mdot in (("oxidizer", 1.68), ("fuel", 1.118)):
122 cfg = config.feed_system[side]
123 assert cfg.fittings == [] and cfg.roughness_m is None
124 rho, mu = config.fluids[side].density, config.fluids[side].viscosity
125 A = cfg.A_hydraulic
126 Ax = math.pi * (cfg.d_exit / 2) ** 2 if cfg.d_exit else A
127 for P in (3.0e6, 4.0e6):
128 want = cfg.K0 * _q(mdot, rho, A) + cfg.K_exit * _q(mdot, rho, Ax)
129 assert delta_p_feed(mdot, rho, cfg, P, mu=mu) == pytest.approx(want, rel=1e-12)
130 assert delta_p_feed(mdot, rho, cfg, P, mu=mu) == delta_p_feed(mdot, rho, cfg, P)
131
132
133# ---- accelerator -------------------------------------------------------------------------
134
135# An impinging config the kernel takes (no ring-network manifold, no regen).
136KERNEL_CFG = "configs/impinging_lox_ch4_8000N.yaml"
137
138
140 from engine import accel
141 if not accel.available():
142 pytest.skip("numba unavailable")
143
144
145def _with_feed(config, **kw):
146 fs = {s: FeedSystemConfig(**{**config.feed_system[s].model_dump(), **kw.get(s, {})})
147 for s in ("oxidizer", "fuel")}
148 return config.model_copy(update={"feed_system": fs})
149
150
152 """params._feed folds the fittings into K0; the injector solve must still be Python's."""
154 from engine import accel
155 from engine.core.injectors import get_injector_model
156 from engine.pipeline.io import load_config
157 config = _with_feed(
158 load_config(str(ROOT / KERNEL_CFG)),
159 oxidizer={"fittings": [{"name": "LOX main solenoid", "K": 3.0, "source": "test value"}]},
160 fuel={"fittings": [{"name": "fuel main solenoid", "K": 2.5, "source": "test value"},
161 {"name": "tee, run", "K": 0.4, "source": "test value"}]},
162 )
163 assert accel.can_handle(config)
164 model = get_injector_model(config)
165 P = 584.27 * PSI_TO_PA
166 for Pc in (2.0e6, 2.6e6):
167 got = accel.solve(config, P, P, Pc)
168 assert got is not None
169 mO, mF, want = model.solve(P, P, Pc)
170 for name, g, w in (("mdot_O", got[0], mO), ("mdot_F", got[1], mF),
171 ("dpf_O", got[2]["delta_p_feed_O"], want["delta_p_feed_O"]),
172 ("dpf_F", got[2]["delta_p_feed_F"], want["delta_p_feed_F"])):
173 assert g == pytest.approx(w, rel=1e-9), name
174
175
178 from engine import accel
179 from engine.pipeline.io import load_config
180 config = load_config(str(ROOT / KERNEL_CFG))
181 assert accel.can_handle(config)
182 rough = _with_feed(config, fuel={"roughness_m": EPS_DRAWN_SS, "length": 0.5})
183 assert not accel.can_handle(rough)
184 res, outcome = accel.solve_ex(rough, 4.0e6, 4.0e6, 2.6e6)
185 assert res is None and outcome == accel.Outcome.NOT_HANDLED