1"""Friction factors and fitting losses, as registries rather than choices.
3Two registries, both extensible from outside this package, both wrapping
4``fluids`` rather than reimplementing it.
6**Friction factors.** Colebrook is implicit and has to be iterated; a dozen
7explicit approximations to it exist, and they disagree in the third significant
8figure. Which one a project uses is a modelling decision, not a fact, so it is
9an *option* on the component -- ``pipe.options.friction`` -- and changing it is
12**Fitting losses.** Crane TP-410's resistance coefficients, plus the Hooper 2K
13and Darby 3K methods, all of which ``fluids`` implements and validates against
14the source books. Every fitting here is a thin adapter that unpacks a uniform
15context into the arguments one of those functions wants -- deliberately thin, so
16that what this package contributes is the plumbing and never the physics.
18Nothing in this module computes a loss coefficient from first principles. If a
19number here disagrees with Crane, it is this module's adapter that is wrong.
22from __future__
import annotations
25from dataclasses
import dataclass, field
26from typing
import Callable, Mapping
28import fluids.fittings
as ft
29from fluids.friction
import ft_Crane, friction_factor, friction_factor_methods
41FRICTION_METHODS: tuple[str, ...] = tuple(
42 sorted(friction_factor_methods(Re=1.0e5, eD=1.0e-4))
45DEFAULT_FRICTION_METHOD =
"Clamond"
52"""Laminar-turbulent transition, from fluids.friction.LAMINAR_TRANSITION_PIPE."""
56 """Darcy friction factor from a named correlation.
60 roughness_ratio: Absolute roughness over bore, e/D.
61 method: One of :data:`FRICTION_METHODS`.
63 Zero and negative Reynolds numbers return zero rather than raising: a
64 solver's first iterate legitimately passes through zero flow, and a
65 component with no flow through it has no frictional loss.
67 Below the transition the answer is 64/Re regardless of which turbulent
68 correlation was named -- ``fluids`` applies that itself, and it matters
69 because a solve sweeping down toward zero flow passes through the laminar
70 regime whether or not the design point is anywhere near it.
74 if method
not in FRICTION_METHODS:
76 f
"unknown friction method {method!r}; available: "
77 f
"{', '.join(FRICTION_METHODS)}"
79 return float(friction_factor(Re=Re, eD=roughness_ratio, Method=method))
82def reynolds(mdot: float, bore: float, rho: float, mu: float) -> float:
83 """Reynolds number from mass flow. ``Re = 4 m / (pi D mu)``.
85 Written in terms of mass flow rather than velocity because that is what a
86 network solver carries, and going via velocity would divide by density and
87 then multiply by it again.
89 if bore <= 0.0
or mu <= 0.0:
91 return abs(4.0 * mdot / (math.pi * bore * mu))
94def velocity(mdot: float, bore: float, rho: float) -> float:
95 """Bulk velocity [m/s] through a circular bore."""
96 if bore <= 0.0
or rho <= 0.0:
98 return mdot / (rho * math.pi * bore * bore / 4.0)
101@dataclass(frozen=True, slots=True)
103 """What every fitting correlation is given.
105 A uniform shape, so the registry can hold functions whose underlying
106 ``fluids`` signatures differ wildly -- a rounded bend wants an angle and a
107 bend radius, a contraction wants two diameters, an exit wants nothing.
111 """Bore of the run the fitting sits in [m]."""
116 """Friction factor of the attached pipe, which several Crane correlations
119 params: Mapping[str, float] = field(default_factory=dict)
120 """Fitting-specific geometry: ``angle`` [deg, as ``fluids`` takes it],
121 ``bore2`` [m], ``bend_diameters`` [-]."""
123 def get(self, name: str, default: float) -> float:
124 return float(self.
params.
get(name, default))
128FittingK = Callable[[FittingContext], float]
130_FITTINGS: dict[str, FittingK] = {}
134 """Register a fitting correlation. Replacing a name is allowed."""
139 if name
not in _FITTINGS:
141 f
"unknown fitting {name!r}; registered: {', '.join(registered_fittings())}"
143 return _FITTINGS[name]
147 return sorted(_FITTINGS)
151 """Resistance coefficient of one fitting, referred to ``ctx.bore``."""
160def _bend(ctx: FittingContext, angle: float) -> float:
161 def rennels(Re: float |
None, fd: float |
None =
None) -> float:
165 angle=ctx.get(
"angle", angle),
167 roughness=ctx.roughness,
168 bend_diameters=ctx.get(
"bend_diameters", 5.0),
174 return rennels(ctx.Re
or None)
182 return rennels(
None, fd=ctx.fd)
187 ft.contraction_sharp(
188 Di1=ctx.bore, Di2=ctx.get(
"bore2", ctx.bore * 0.5), Re=ctx.Re
or None
196 Di1=ctx.bore, Di2=ctx.get(
"bore2", ctx.bore * 2.0), Re=ctx.Re
or None
202 """A Crane coefficient expressed as a multiple of the pipe friction factor.
204 Crane TP-410 gives many fittings as ``K = n . f_T``, where f_T is the
205 fully-turbulent friction factor *of the pipe size* -- ``fluids.fittings.
206 ft_Crane(D)``, a constant of the bore, not the line's friction factor at its
207 own Reynolds number. Scaling ``ctx.fd`` instead put a 30 f_T elbow anywhere
208 from 0.55 to 1.09 against Crane's 0.885 in 9.5 mm tube, and laminar flow
209 (64/Re) made it larger still.
212 def K(ctx: FittingContext) -> float:
213 return constant * float(ft_Crane(ctx.bore))
236 lambda ctx: float(ft.K_ball_valve_Crane(ctx.bore, ctx.bore, 0.0)),
240 lambda ctx: float(ft.K_gate_valve_Crane(ctx.bore, ctx.bore, 0.0)),
243 "globe_valve",
lambda ctx: float(ft.K_globe_valve_Crane(ctx.bore, ctx.bore))
246 "swing_check",
lambda ctx: float(ft.K_swing_check_valve_Crane(ctx.bore))
float get(self, str name, float default)
float _expansion(FittingContext ctx)
float _bend(FittingContext ctx, float angle)
float velocity(float mdot, float bore, float rho)
float reynolds(float mdot, float bore, float rho, float mu)
float _contraction(FittingContext ctx)
float darcy_friction_factor(float Re, float roughness_ratio, str method)
None register_fitting(str name, FittingK fn)
list[str] registered_fittings()
FittingK _crane(float constant)
FittingK get_fitting(str name)
float fitting_K(str name, FittingContext ctx)