STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
feed_line_K.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""Compute EngineDesign's lumped feed ``K0`` from an actual component list.
3
4EngineDesign's feed model is a single lumped coefficient per side:
5
6 dp = K0 * 0.5 * rho * v^2, v = mdot / (rho * A_hydraulic)
7
8``docs/integration/line-loss-plan.md`` rates that "estimated K" -- rung 4 of 5, a
9guess. Until the P&ID path lands, this script is rung 3: itemise the run, take each
10K from a correlation rather than memory, and refer every one to a single reference
11bore so the sum is a valid K0 for that A_hydraulic.
12
13Reference-area conversion. dp is one number, so for two bores
14 K_ref = K_local * (A_ref / A_local)^2
15because v ~ 1/A. Losses at a WIDE section shrink when expressed against the high
16velocity of a NARROW reference bore. Getting this backwards is the classic error.
17
18Friction factors come from ``fluids`` (Colebrook); fitting K's are Crane TP-410 /
19Idelchik forms, each labelled at its call site. Run:
20
21 python3 scripts/feed_line_K.py
22"""
23from __future__ import annotations
24
25import math
26from fluids.friction import friction_factor
27
28IN = 0.0254
29PSI = 6894.757
30ROUGHNESS_M = 1.5e-6 # drawn stainless tube, Crane TP-410 Table A-23
31
32def area(d: float) -> float:
33 return math.pi / 4.0 * d * d
34
35def tube_id(od_in: float, wall_in: float = 0.035) -> float:
36 return (od_in - 2.0 * wall_in) * IN
37
38class Run:
39 """One feed run, itemised. All K's accumulate against ``d_ref``."""
40
41 def __init__(self, name: str, d_ref: float, mdot: float, rho: float, mu: float):
42 self.name, self.d_ref, self.mdot, self.rho, self.mu = name, d_ref, mdot, rho, mu
43 self.A_ref = area(d_ref)
44 self.v_ref = mdot / (rho * self.A_ref)
45 self.rows: list[tuple[str, float, str]] = []
46
47 def _refer(self, K_local: float, d_local: float) -> float:
48 return K_local * (self.A_ref / area(d_local)) ** 2
49
50 def add(self, label: str, K_local: float, d_local: float, note: str) -> None:
51 self.rows.append((label, self._refer(K_local, d_local), note))
52
53 def entrance(self, d: float) -> None:
54 # Sharp-edged entrance from a vessel, Crane TP-410 A-29.
55 self.add("tank exit -> line (sharp entrance)", 0.5, d, "Crane A-29, K=0.5")
56
57 def ball_valve_full_bore(self, d: float, f_T: float) -> None:
58 # Crane TP-410: full-bore ball valve, fully open, K = 3 f_T.
59 self.add(f"full-bore ball valve {d/IN:.3f}\" bore", 3.0 * f_T, d,
60 f"Crane TP-410, K=3*f_T, f_T={f_T}")
61
62 def contraction(self, d_big: float, d_small: float) -> None:
63 # Sudden contraction, referenced to the SMALL (downstream) velocity.
64 beta2 = (d_small / d_big) ** 2
65 self.add(f"contraction {d_big/IN:.3f}\" -> {d_small/IN:.3f}\"",
66 0.5 * (1.0 - beta2), d_small, "K=0.5(1-beta^2), at small-bore v")
67
68 def expansion(self, d_small: float, d_big: float) -> None:
69 # Sudden expansion, referenced to the SMALL (upstream) velocity.
70 beta2 = (d_small / d_big) ** 2
71 self.add(f"expansion {d_small/IN:.3f}\" -> {d_big/IN:.3f}\"",
72 (1.0 - beta2) ** 2, d_small, "K=(1-beta^2)^2, at small-bore v")
73
74 def straight(self, d: float, L: float) -> None:
75 v = self.mdot / (self.rho * area(d))
76 Re = self.rho * v * d / self.mu
77 f = friction_factor(Re=Re, eD=ROUGHNESS_M / d)
78 self.add(f"straight tube {d/IN:.3f}\" ID x {L/IN:.1f}\"", f * L / d, d,
79 f"Darcy f*L/D, Re={Re:.3e}, f={f:.4f}")
80
81 def report(self) -> float:
82 K0 = sum(k for _, k, _ in self.rows)
83 q = 0.5 * self.rho * self.v_ref ** 2
84 print(f"\n{self.name}")
85 print(f" reference bore {self.d_ref*1000:.3f} mm mdot {self.mdot:.3f} kg/s"
86 f" v_ref {self.v_ref:.1f} m/s q {q/PSI:.1f} psi")
87 print(f" {'component':44s} {'K@ref':>8s} source")
88 print(" " + "-" * 92)
89 for label, k, note in self.rows:
90 print(f" {label:44s} {k:8.4f} {note}")
91 print(" " + "-" * 92)
92 print(f" {'K0 (use this for A_hydraulic at the reference bore)':44s} {K0:8.4f}")
93 print(f" => dp_line = {K0 * q / PSI:.1f} psi")
94 return K0
95
96
97def main() -> None:
98 # Design point the runs must carry.
99 F, Isp, OF, Pc_psi = 8000.0, 237.0, 1.65, 430.0
100 mdot = F / (Isp * 9.80665)
101 mdot_O, mdot_F = mdot * OF / (1 + OF), mdot / (1 + OF)
102
103 d_half_npt = 0.5 * IN # 1/2" NPT through-bore and full-bore ball valve bore
104 d_half_tube = tube_id(0.500) # 1/2" Swagelok tube, 0.035" wall -> 0.430" ID
105 d_38_tube = tube_id(0.375) # 3/8" Swagelok tube, 0.035" wall -> 0.305" ID
106
107 print("=" * 96)
108 print("CalSTAR ethalox flight feed runs -- K0 from the as-built component list (operator, 2026-09-13)")
109 print(f"design point: F {F:.0f} N, O/F {OF}, Pc {Pc_psi:.0f} psia"
110 f" -> mdot_O {mdot_O:.3f}, mdot_F {mdot_F:.3f} kg/s")
111 print("=" * 96)
112
113 # --- LOX: 1/2" NPT -> 1/2" full-flow ball valve -> 1/2" NPT-to-Swage -> 4" of 1/2"
114 # tube -> 1/2" Swage-to-NPT -> injector. No bends.
115 lox = Run("OXIDISER (reference: 1/2\" tube ID)", d_half_tube, mdot_O, 1140.0, 1.8e-4)
116 lox.entrance(d_half_npt)
117 lox.ball_valve_full_bore(d_half_npt, f_T=0.027)
118 lox.contraction(d_half_npt, d_half_tube)
119 lox.straight(d_half_tube, 4.0 * IN)
120 lox.expansion(d_half_tube, d_half_npt)
121 K_O = lox.report()
122
123 # --- FUEL: 1/2" NPT full-flow ball valve -> 1/2"-to-3/8" Swage -> ~3 ft of 3/8"
124 # tube (straight) -> 3/8" Swage-to-1/2" NPT -> injector.
125 fu = Run("FUEL (reference: 3/8\" tube ID)", d_38_tube, mdot_F, 789.0, 1.2e-3)
126 fu.entrance(d_half_npt)
127 fu.ball_valve_full_bore(d_half_npt, f_T=0.027)
128 fu.contraction(d_half_npt, d_38_tube)
129 fu.straight(d_38_tube, 36.0 * IN)
130 fu.expansion(d_38_tube, d_half_npt)
131 K_F = fu.report()
132
133 # --- Does the design point close on a 600 psi tank?
134 print("\n" + "=" * 96)
135 print("PRESSURE BUDGET at a 600 psi tank")
136 print("=" * 96)
137 for tag, run, K in (("LOX", lox, K_O), ("FUEL", fu, K_F)):
138 dp_line = K * 0.5 * run.rho * run.v_ref ** 2 / PSI
139 for frac in (0.20, 0.25):
140 need = Pc_psi + dp_line + frac * Pc_psi
141 mark = "OK" if need <= 600 else f"SHORT {need-600:.0f} psi"
142 print(f" {tag:4s} dP/Pc {frac:.2f}: {Pc_psi:.0f} Pc + {dp_line:5.1f} line"
143 f" + {frac*Pc_psi:5.1f} inj = {need:5.1f} psi {mark}")
144
145 # --- What a 1/2" fuel run would buy, since the 3/8" one is the binding side.
146 print("\n" + "=" * 96)
147 print("IF THE FUEL RUN WERE 1/2\" TUBE INSTEAD (same layout, 3 ft straight)")
148 print("=" * 96)
149 alt = Run("FUEL alt (reference: 1/2\" tube ID)", d_half_tube, mdot_F, 789.0, 1.2e-3)
150 alt.entrance(d_half_npt)
151 alt.ball_valve_full_bore(d_half_npt, f_T=0.027)
152 alt.contraction(d_half_npt, d_half_tube)
153 alt.straight(d_half_tube, 36.0 * IN)
154 alt.expansion(d_half_tube, d_half_npt)
155 K_alt = alt.report()
156 dp_alt = K_alt * 0.5 * alt.rho * alt.v_ref ** 2 / PSI
157 for frac in (0.20, 0.25, 0.30):
158 need = Pc_psi + dp_alt + frac * Pc_psi
159 mark = "OK" if need <= 600 else f"SHORT {need-600:.0f} psi"
160 print(f" FUEL dP/Pc {frac:.2f}: {Pc_psi:.0f} + {dp_alt:5.1f} + {frac*Pc_psi:5.1f}"
161 f" = {need:5.1f} psi {mark}")
162
163
164if __name__ == "__main__":
165 main()
None ball_valve_full_bore(self, float d, float f_T)
None entrance(self, float d)
None add(self, str label, float K_local, float d_local, str note)
None expansion(self, float d_small, float d_big)
float _refer(self, float K_local, float d_local)
float report(self)
None contraction(self, float d_big, float d_small)
__init__(self, str name, float d_ref, float mdot, float rho, float mu)
None straight(self, float d, float L)
float tube_id(float od_in, float wall_in=0.035)
None main()
float area(float d)