1"""Bends and flex hose: geometry that changes the answer.
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
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.
15from __future__
import annotations
23M = Provenance.MANUFACTURER
27@pytest.fixture(scope="module")
29 return conditions_from_fluid(
Fluid(
"LOX"), p=3.0e6, T=90.0)
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"),
37 params.update(overrides)
38 return build_component(ComponentInstance.build(
"BD-01",
"bend", params))
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"),
46 base.update(params
or {})
47 return build_component(
48 ComponentInstance.build(
"FH-01",
"flex_hose", base, **kwargs)
56 """A tight bend and a long-radius one are not the same component.
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.
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"))
65 assert tight.diagnostics(0.5, lox)[
"r_over_D"] == pytest.approx(1.03, rel=1e-2)
66 assert tight.pressure_drop(0.5, lox) > easy.pressure_drop(0.5, lox)
70 """A real and slightly counter-intuitive result, worth pinning.
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.
79 bend_radius=
Param(rD * BORE_MM,
"mm", M,
"swept")
83 for rD
in (1.0, 3.0, 10.0)
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"
92 """It still solves. The violation is a statement, not a refusal."""
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"),
98 assert over_bent.pressure_drop(0.5, lox) > 0.0
100 violations = over_bent.check()
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"
109 bend_radius=
Param(40.0,
"mm", M,
"as built"),
110 min_bend_radius=
Param(28.6,
"mm", M,
"3x OD"),
112 assert fine.check() == []
116 """Silence about a limit is not a pass -- it is an absence of information."""
124 """Curved flow is genuinely more lossy, and the whole run is curved.
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.
131 gentle =
_hose({
"installed_bend_radius":
Param(150.0,
"mm", M,
"routed")})
132 tight =
_hose({
"installed_bend_radius":
Param(60.0,
"mm", M,
"routed")})
135 straight.pressure_drop(0.5, lox)
136 < gentle.pressure_drop(0.5, lox)
137 < tight.pressure_drop(0.5, lox)
142 """The convolutions are in the flow path, not just in the braid."""
144 convoluted =
_hose(options={
"construction":
"convoluted"})
147 convoluted.diagnostics(0.5, lox)[
"friction_factor"]
148 / smooth.diagnostics(0.5, lox)[
"friction_factor"]
150 assert ratio == pytest.approx(4.0, rel=1e-9),
"the default convolution factor"
154 """On a short hose the crimped ends dominate, and that should be visible.
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.
160 short =
_hose({
"length":
Param(0.15,
"m", M,
"short jumper")})
161 d = short.diagnostics(0.5, lox)
163 assert d[
"dp_end_fittings"] > d[
"dp_hose"],
"ends dominate a short hose"
164 assert short.pressure_drop(0.5, lox) == pytest.approx(
165 d[
"dp_hose"] + d[
"dp_end_fittings"]
170 """Inside static, outside dynamic -- the case worth surfacing.
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.
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"),
183 violations = hose.check()
184 assert len(violations) == 1
185 assert violations[0].severity ==
"warning"
186 assert violations[0].limit ==
"min_bend_radius_dynamic"
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"),
197 violations = hose.check()
198 assert [v.severity
for v
in violations] == [
"error"]
199 assert "collapses" in violations[0].detail
203 hose =
_hose({
"min_bend_radius":
Param(100.0,
"mm", M,
"datasheet")})
204 assert hose.check() == []
208 """Roughness, convolution factor and end-fitting K are all placeholders.
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.
214 instance = ComponentInstance.build(
218 "bore":
Param(9.5,
"mm", M,
"hose"),
219 "length":
Param(0.6,
"m", M,
"cut length"),
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()
None test_no_limit_declared_means_no_claim()
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)