STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
test_comps_geometry.py
Go to the documentation of this file.
1"""Bends and flex hose: geometry that changes the answer.
2
3Bend radius is not a cosmetic property. At r/D = 1 a bend carries about three
4times the friction factor of a straight run and at r/D = 3 still nearly twice,
5so a model that ignores how tightly a line is actually routed under-predicts its
6loss substantially.
7
8It is also a hardware limit. Tube bent inside its minimum radius thins and
9ovalises; hose bent inside its minimum collapses a smooth-bore liner or fatigues
10a convoluted one. Those limits are *reported*, never enforced -- a line bent too
11tight is buildable, somebody will build it, and the model should still solve so
12that the cost is visible.
13"""
14
15from __future__ import annotations
16
17import pytest
18
19from feedtwin.comps import build_component, conditions_from_fluid
20from feedtwin.model import ComponentInstance, Param, Provenance
21from feedtwin.props import Fluid
22
23M = Provenance.MANUFACTURER
24BORE_MM = 7.75
25
26
27@pytest.fixture(scope="module")
28def lox() -> object:
29 return conditions_from_fluid(Fluid("LOX"), p=3.0e6, T=90.0)
30
31
32def _bend(**overrides: Param) -> object:
33 params: dict[str, Param] = {
34 "bore": Param(BORE_MM, "mm", M, "3/8 x 0.035 tube"),
35 "bend_radius": Param(23.0, "mm", M, "as bent"),
36 }
37 params.update(overrides)
38 return build_component(ComponentInstance.build("BD-01", "bend", params))
39
40
41def _hose(params: dict[str, Param] | None = None, **kwargs: object) -> object:
42 base: dict[str, Param] = {
43 "bore": Param(9.5, "mm", M, "3/8 in. hose"),
44 "length": Param(0.6, "m", M, "cut length"),
45 }
46 base.update(params or {})
47 return build_component(
48 ComponentInstance.build("FH-01", "flex_hose", base, **kwargs) # type: ignore[arg-type]
49 )
50
51
52# ------------------------------------------------------------------- geometry
53
54
56 """A tight bend and a long-radius one are not the same component.
57
58 The whole reason bend radius is a first-class parameter rather than an
59 elbow class: a tube bent on whatever former was to hand is not a
60 long-radius elbow, and the difference is visible in the loss.
61 """
62 tight = _bend(bend_radius=Param(8.0, "mm", M, "hand bender"))
63 easy = _bend(bend_radius=Param(23.0, "mm", M, "r/D = 3"))
64
65 assert tight.diagnostics(0.5, lox)["r_over_D"] == pytest.approx(1.03, rel=1e-2) # type: ignore[attr-defined]
66 assert tight.pressure_drop(0.5, lox) > easy.pressure_drop(0.5, lox) # type: ignore[attr-defined]
67
68
70 """A real and slightly counter-intuitive result, worth pinning.
71
72 Very tight bends lose to the turn itself; very long-radius ones lose to the
73 extra arc length they add. The optimum sits around r/D = 2 to 4. That this
74 falls out of the correlation rather than being imposed is a good sign that
75 the geometry is reaching it correctly.
76 """
77 losses = {
78 rD: _bend(
79 bend_radius=Param(rD * BORE_MM, "mm", M, "swept")
80 ).pressure_drop( # type: ignore[attr-defined]
81 0.5, lox
82 )
83 for rD in (1.0, 3.0, 10.0)
84 }
85 assert losses[3.0] < losses[1.0], "a very tight bend should cost more"
86 assert losses[3.0] < losses[10.0], "a very long bend should cost more"
87
88
90 lox: object,
91) -> None:
92 """It still solves. The violation is a statement, not a refusal."""
93 over_bent = _bend(
94 bend_radius=Param(15.0, "mm", M, "as built"),
95 outer_diameter=Param(9.525, "mm", M, "3/8 in. OD"),
96 min_bend_radius=Param(28.6, "mm", M, "3x OD, shop rule"),
97 )
98 assert over_bent.pressure_drop(0.5, lox) > 0.0 # type: ignore[attr-defined]
99
100 violations = over_bent.check() # type: ignore[attr-defined]
101 assert len(violations) == 1
102 assert violations[0].limit == "min_bend_radius"
103 assert violations[0].severity == "error"
104 assert "1.57x OD" in violations[0].detail, "quoted against OD, as benders are"
105
106
108 fine = _bend(
109 bend_radius=Param(40.0, "mm", M, "as built"),
110 min_bend_radius=Param(28.6, "mm", M, "3x OD"),
111 )
112 assert fine.check() == [] # type: ignore[attr-defined]
113
114
116 """Silence about a limit is not a pass -- it is an absence of information."""
117 assert _bend().check() == [] # type: ignore[attr-defined]
118
119
120# ------------------------------------------------------------------ flex hose
121
122
124 """Curved flow is genuinely more lossy, and the whole run is curved.
125
126 Unlike a discrete bend -- where the curvature effect is already inside the
127 fitting correlation -- a hose bent over its length is curved everywhere, so
128 the friction factor itself changes.
129 """
130 straight = _hose()
131 gentle = _hose({"installed_bend_radius": Param(150.0, "mm", M, "routed")})
132 tight = _hose({"installed_bend_radius": Param(60.0, "mm", M, "routed")})
133
134 assert (
135 straight.pressure_drop(0.5, lox) # type: ignore[attr-defined]
136 < gentle.pressure_drop(0.5, lox) # type: ignore[attr-defined]
137 < tight.pressure_drop(0.5, lox) # type: ignore[attr-defined]
138 )
139
140
142 """The convolutions are in the flow path, not just in the braid."""
143 smooth = _hose()
144 convoluted = _hose(options={"construction": "convoluted"})
145
146 ratio = (
147 convoluted.diagnostics(0.5, lox)["friction_factor"]
148 / smooth.diagnostics(0.5, lox)["friction_factor"] # type: ignore[attr-defined]
149 )
150 assert ratio == pytest.approx(4.0, rel=1e-9), "the default convolution factor"
151
152
154 """On a short hose the crimped ends dominate, and that should be visible.
155
156 Rolling them into a single number would hide the case where most of a
157 hose's loss is its terminations -- which is the case that tells you a
158 shorter hose will not help.
159 """
160 short = _hose({"length": Param(0.15, "m", M, "short jumper")})
161 d = short.diagnostics(0.5, lox) # type: ignore[attr-defined]
162
163 assert d["dp_end_fittings"] > d["dp_hose"], "ends dominate a short hose"
164 assert short.pressure_drop(0.5, lox) == pytest.approx( # type: ignore[attr-defined]
165 d["dp_hose"] + d["dp_end_fittings"]
166 )
167
168
170 """Inside static, outside dynamic -- the case worth surfacing.
171
172 Whether a run actually flexes in service is a fact about the installation
173 that the model does not know, so this is a warning rather than an error.
174 Silently passing it would lose the one piece of information that matters.
175 """
176 hose = _hose(
177 {
178 "installed_bend_radius": Param(120.0, "mm", M, "routed"),
179 "min_bend_radius": Param(100.0, "mm", M, "datasheet, static"),
180 "min_bend_radius_dynamic": Param(200.0, "mm", M, "datasheet, dynamic"),
181 }
182 )
183 violations = hose.check() # type: ignore[attr-defined]
184 assert len(violations) == 1
185 assert violations[0].severity == "warning"
186 assert violations[0].limit == "min_bend_radius_dynamic"
187
188
190 hose = _hose(
191 {
192 "installed_bend_radius": Param(80.0, "mm", M, "routed"),
193 "min_bend_radius": Param(100.0, "mm", M, "datasheet, static"),
194 "min_bend_radius_dynamic": Param(200.0, "mm", M, "datasheet, dynamic"),
195 }
196 )
197 violations = hose.check() # type: ignore[attr-defined]
198 assert [v.severity for v in violations] == ["error"]
199 assert "collapses" in violations[0].detail
200
201
203 hose = _hose({"min_bend_radius": Param(100.0, "mm", M, "datasheet")})
204 assert hose.check() == [] # type: ignore[attr-defined]
205
206
208 """Roughness, convolution factor and end-fitting K are all placeholders.
209
210 Published flex-hose loss data is scarce and construction varies between
211 manufacturers, so these defaults are more suspect than most. A run report
212 should say so, which it can only do if they are tagged.
213 """
214 instance = ComponentInstance.build(
215 "FH-01",
216 "flex_hose",
217 {
218 "bore": Param(9.5, "mm", M, "hose"),
219 "length": Param(0.6, "m", M, "cut length"),
220 },
221 )
222 assumed = set(instance.assumptions())
223 assert {"roughness", "convolution_factor", "end_fitting_K"} <= assumed
None test_a_bend_inside_its_minimum_radius_is_reported_not_enforced(object lox)
None test_bend_resistance_depends_on_how_tight_the_bend_is(object lox)
None test_end_fittings_are_reported_separately(object lox)
None test_convoluted_hose_is_far_lossier_than_smooth_bore(object lox)
object _bend(**Param overrides)
None test_a_straight_hose_has_no_bend_to_check()
object _hose(dict[str, Param]|None params=None, **object kwargs)
None test_hose_defaults_are_flagged_as_assumptions()
None test_a_bend_within_its_limit_reports_nothing()
None test_static_and_dynamic_bend_limits_are_distinguished()
None test_routing_a_hose_tighter_costs_pressure(object lox)
None test_breaking_the_static_limit_is_an_error()
None test_bend_loss_has_a_minimum_near_three_diameters(object lox)
static void check(bool ok, const std::string &what)