Skip to main content

use_fluid/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4//! Small fluid mechanics helpers.
5
6pub mod prelude;
7
8fn finite(value: f64) -> Option<f64> {
9    value.is_finite().then_some(value)
10}
11
12fn nonnegative(value: f64) -> bool {
13    value.is_finite() && value >= 0.0
14}
15
16fn positive(value: f64) -> bool {
17    value.is_finite() && value > 0.0
18}
19
20/// Computes buoyant force from fluid density, displaced volume, and gravitational acceleration.
21///
22/// Formula: `F_b = ρ * V * g`
23///
24/// Returns `None` when `fluid_density` or `displaced_volume` is negative, when
25/// `gravitational_acceleration` is not finite, or when the computed result is not finite.
26///
27/// # Examples
28///
29/// ```rust
30/// use use_fluid::buoyant_force;
31///
32/// let force = buoyant_force(1000.0, 0.01, 9.80665).unwrap();
33///
34/// assert!((force - 98.0665).abs() < 1.0e-10);
35/// ```
36#[must_use]
37pub fn buoyant_force(
38    fluid_density: f64,
39    displaced_volume: f64,
40    gravitational_acceleration: f64,
41) -> Option<f64> {
42    if !nonnegative(fluid_density)
43        || !nonnegative(displaced_volume)
44        || !gravitational_acceleration.is_finite()
45    {
46        return None;
47    }
48
49    finite(fluid_density * displaced_volume * gravitational_acceleration)
50}
51
52/// Computes displaced volume from buoyant force, fluid density, and gravitational acceleration.
53///
54/// Formula: `V = F_b / (ρ * g)`
55///
56/// Returns `None` when `buoyant_force` is negative, when `fluid_density` is less than or equal
57/// to zero, when `gravitational_acceleration` is zero or not finite, when the computed volume is
58/// negative, or when the computed volume is not finite.
59#[must_use]
60pub fn displaced_volume_from_buoyant_force(
61    buoyant_force: f64,
62    fluid_density: f64,
63    gravitational_acceleration: f64,
64) -> Option<f64> {
65    if !nonnegative(buoyant_force)
66        || !positive(fluid_density)
67        || !gravitational_acceleration.is_finite()
68        || gravitational_acceleration == 0.0
69    {
70        return None;
71    }
72
73    let volume = buoyant_force / (fluid_density * gravitational_acceleration);
74    if volume < 0.0 {
75        return None;
76    }
77
78    finite(volume)
79}
80
81/// Computes hydrostatic pressure from fluid density, gravitational acceleration, and depth.
82///
83/// Formula: `P = ρ * g * h`
84///
85/// Returns `None` when `fluid_density` or `depth` is negative, when
86/// `gravitational_acceleration` is not finite, or when the computed result is not finite.
87///
88/// # Examples
89///
90/// ```rust
91/// use use_fluid::hydrostatic_pressure;
92///
93/// let pressure = hydrostatic_pressure(1000.0, 9.80665, 10.0).unwrap();
94///
95/// assert!((pressure - 98_066.5).abs() < 1.0e-9);
96/// ```
97#[must_use]
98pub fn hydrostatic_pressure(
99    fluid_density: f64,
100    gravitational_acceleration: f64,
101    depth: f64,
102) -> Option<f64> {
103    if !nonnegative(fluid_density) || !nonnegative(depth) || !gravitational_acceleration.is_finite()
104    {
105        return None;
106    }
107
108    finite(fluid_density * gravitational_acceleration * depth)
109}
110
111/// Computes absolute pressure from surface pressure and the hydrostatic contribution.
112///
113/// Formula: `P_abs = P_surface + ρ * g * h`
114///
115/// Returns `None` when `surface_pressure`, `fluid_density`, or `depth` is negative, when
116/// `gravitational_acceleration` is not finite, or when the computed result is not finite.
117#[must_use]
118pub fn absolute_pressure(
119    surface_pressure: f64,
120    fluid_density: f64,
121    gravitational_acceleration: f64,
122    depth: f64,
123) -> Option<f64> {
124    if !nonnegative(surface_pressure)
125        || !nonnegative(fluid_density)
126        || !nonnegative(depth)
127        || !gravitational_acceleration.is_finite()
128    {
129        return None;
130    }
131
132    let hydrostatic_gradient = fluid_density * gravitational_acceleration;
133
134    finite(hydrostatic_gradient.mul_add(depth, surface_pressure))
135}
136
137/// Computes volumetric flow rate from area and flow velocity.
138///
139/// Formula: `Q = A * v`
140///
141/// Returns `None` when `area` is negative, when either input is not finite, or when the
142/// computed result is not finite.
143///
144/// # Examples
145///
146/// ```rust
147/// use use_fluid::volumetric_flow_rate;
148///
149/// assert_eq!(volumetric_flow_rate(2.0, 3.0), Some(6.0));
150/// assert_eq!(volumetric_flow_rate(2.0, -3.0), Some(-6.0));
151/// ```
152#[must_use]
153pub fn volumetric_flow_rate(area: f64, velocity: f64) -> Option<f64> {
154    if !nonnegative(area) || !velocity.is_finite() {
155        return None;
156    }
157
158    finite(area * velocity)
159}
160
161/// Computes velocity from volumetric flow rate and area.
162///
163/// Formula: `v = Q / A`
164///
165/// Returns `None` when `area` is less than or equal to zero, when `flow_rate` is not finite, or
166/// when the computed result is not finite.
167#[must_use]
168pub fn velocity_from_flow_rate(flow_rate: f64, area: f64) -> Option<f64> {
169    if !flow_rate.is_finite() || !positive(area) {
170        return None;
171    }
172
173    finite(flow_rate / area)
174}
175
176/// Computes mass flow rate from density and volumetric flow rate.
177///
178/// Formula: `ṁ = ρ * Q`
179///
180/// Returns `None` when `density` is negative, when either input is not finite, or when the
181/// computed result is not finite.
182#[must_use]
183pub fn mass_flow_rate(density: f64, volumetric_flow_rate: f64) -> Option<f64> {
184    if !nonnegative(density) || !volumetric_flow_rate.is_finite() {
185        return None;
186    }
187
188    finite(density * volumetric_flow_rate)
189}
190
191/// Computes downstream velocity from continuity for incompressible flow.
192///
193/// Formula: `A1 * v1 = A2 * v2`, so `v2 = A1 * v1 / A2`
194///
195/// Returns `None` when `area_a` is negative, when `area_b` is less than or equal to zero, when
196/// any input is not finite, or when the computed result is not finite.
197#[must_use]
198pub fn continuity_velocity(area_a: f64, velocity_a: f64, area_b: f64) -> Option<f64> {
199    if !nonnegative(area_a) || !velocity_a.is_finite() || !positive(area_b) {
200        return None;
201    }
202
203    finite(area_a * velocity_a / area_b)
204}
205
206/// Computes downstream area from continuity for incompressible flow.
207///
208/// Formula: `A2 = A1 * v1 / v2`
209///
210/// Returns `None` when `area_a` is negative, when `velocity_b` is zero, when any input is not
211/// finite, when the computed area is negative, or when the computed area is not finite.
212#[must_use]
213pub fn continuity_area(area_a: f64, velocity_a: f64, velocity_b: f64) -> Option<f64> {
214    if !nonnegative(area_a)
215        || !velocity_a.is_finite()
216        || !velocity_b.is_finite()
217        || velocity_b == 0.0
218    {
219        return None;
220    }
221
222    let area = area_a * velocity_a / velocity_b;
223    if area < 0.0 {
224        return None;
225    }
226
227    finite(area)
228}
229
230/// Computes downstream pressure from the Bernoulli relation between two points.
231///
232/// Formula: `P2 = P1 + 0.5ρ(v1² - v2²) + ρg(h1 - h2)`
233///
234/// Returns `None` when `reference_pressure` or `density` is negative, when any input is not
235/// finite, or when the computed result is not finite.
236///
237/// # Examples
238///
239/// ```rust
240/// use use_fluid::bernoulli_pressure;
241///
242/// let pressure = bernoulli_pressure(100000.0, 1000.0, 4.0, 2.0, 9.80665, 10.0, 5.0).unwrap();
243///
244/// assert!(pressure.is_finite());
245/// ```
246#[must_use]
247pub fn bernoulli_pressure(
248    reference_pressure: f64,
249    density: f64,
250    reference_velocity: f64,
251    velocity: f64,
252    gravitational_acceleration: f64,
253    reference_height: f64,
254    height: f64,
255) -> Option<f64> {
256    if !nonnegative(reference_pressure)
257        || !nonnegative(density)
258        || !reference_velocity.is_finite()
259        || !velocity.is_finite()
260        || !gravitational_acceleration.is_finite()
261        || !reference_height.is_finite()
262        || !height.is_finite()
263    {
264        return None;
265    }
266
267    let velocity_delta = velocity.mul_add(-velocity, reference_velocity * reference_velocity);
268    let pressure_without_height = (0.5 * density).mul_add(velocity_delta, reference_pressure);
269    let hydrostatic_gradient = density * gravitational_acceleration;
270
271    finite(hydrostatic_gradient.mul_add(reference_height - height, pressure_without_height))
272}
273
274/// Computes dynamic pressure from density and flow velocity.
275///
276/// Formula: `q = 0.5 * ρ * v²`
277///
278/// Returns `None` when `density` is negative, when any input is not finite, or when the
279/// computed result is not finite.
280#[must_use]
281pub fn dynamic_pressure(density: f64, velocity: f64) -> Option<f64> {
282    if !nonnegative(density) || !velocity.is_finite() {
283        return None;
284    }
285
286    let pressure = 0.5 * density * velocity.powi(2);
287    if pressure < 0.0 {
288        return None;
289    }
290
291    finite(pressure)
292}
293
294/// Computes Reynolds number from density, velocity, characteristic length, and dynamic viscosity.
295///
296/// Formula: `Re = ρ * v * L / μ`
297///
298/// Returns `None` when `density` is negative, when `characteristic_length` is negative, when
299/// `dynamic_viscosity` is less than or equal to zero, when any input is not finite, or when the
300/// computed result is not finite.
301///
302/// # Examples
303///
304/// ```rust
305/// use use_fluid::reynolds_number;
306///
307/// let reynolds = reynolds_number(1000.0, 2.0, 0.1, 0.001).unwrap();
308///
309/// assert!((reynolds - 200_000.0).abs() < 1.0e-9);
310/// ```
311#[must_use]
312pub fn reynolds_number(
313    density: f64,
314    velocity: f64,
315    characteristic_length: f64,
316    dynamic_viscosity: f64,
317) -> Option<f64> {
318    if !nonnegative(density)
319        || !velocity.is_finite()
320        || !nonnegative(characteristic_length)
321        || !positive(dynamic_viscosity)
322    {
323        return None;
324    }
325
326    finite(density * velocity.abs() * characteristic_length / dynamic_viscosity)
327}
328
329/// Computes kinematic viscosity from dynamic viscosity and density.
330///
331/// Formula: `ν = μ / ρ`
332///
333/// Returns `None` when `dynamic_viscosity` is negative, when `density` is less than or equal to
334/// zero, when any input is not finite, or when the computed result is not finite.
335#[must_use]
336pub fn kinematic_viscosity(dynamic_viscosity: f64, density: f64) -> Option<f64> {
337    if !nonnegative(dynamic_viscosity) || !positive(density) {
338        return None;
339    }
340
341    finite(dynamic_viscosity / density)
342}
343
344/// Computes dynamic viscosity from kinematic viscosity and density.
345///
346/// Formula: `μ = ν * ρ`
347///
348/// Returns `None` when `kinematic_viscosity` or `density` is negative, when any input is not
349/// finite, or when the computed result is not finite.
350#[must_use]
351pub fn dynamic_viscosity(kinematic_viscosity: f64, density: f64) -> Option<f64> {
352    if !nonnegative(kinematic_viscosity) || !nonnegative(density) {
353        return None;
354    }
355
356    finite(kinematic_viscosity * density)
357}
358
359/// Computes drag force from density, velocity, drag coefficient, and area.
360///
361/// Formula: `F_d = 0.5 * ρ * v² * C_d * A`
362///
363/// Returns `None` when `density`, `drag_coefficient`, or `area` is negative, when any input is
364/// not finite, or when the computed result is not finite.
365///
366/// # Examples
367///
368/// ```rust
369/// use use_fluid::drag_force;
370///
371/// let force = drag_force(1.225, 10.0, 0.47, 1.0).unwrap();
372///
373/// assert!((force - 28.7875).abs() < 1.0e-9);
374/// ```
375#[must_use]
376pub fn drag_force(density: f64, velocity: f64, drag_coefficient: f64, area: f64) -> Option<f64> {
377    if !nonnegative(density)
378        || !velocity.is_finite()
379        || !nonnegative(drag_coefficient)
380        || !nonnegative(area)
381    {
382        return None;
383    }
384
385    let force = 0.5 * density * velocity.powi(2) * drag_coefficient * area;
386    if force < 0.0 {
387        return None;
388    }
389
390    finite(force)
391}
392
393/// A simple fluid model with density and optional dynamic viscosity.
394#[derive(Debug, Clone, Copy, PartialEq)]
395pub struct Fluid {
396    pub density: f64,
397    pub dynamic_viscosity: Option<f64>,
398}
399
400impl Fluid {
401    /// Creates a fluid from density when the density is non-negative and finite.
402    #[must_use]
403    pub fn new(density: f64) -> Option<Self> {
404        if !nonnegative(density) {
405            return None;
406        }
407
408        Some(Self {
409            density,
410            dynamic_viscosity: None,
411        })
412    }
413
414    /// Creates a fluid from density and dynamic viscosity when both values are non-negative and finite.
415    #[must_use]
416    pub fn with_dynamic_viscosity(density: f64, dynamic_viscosity: f64) -> Option<Self> {
417        if !nonnegative(density) || !nonnegative(dynamic_viscosity) {
418            return None;
419        }
420
421        Some(Self {
422            density,
423            dynamic_viscosity: Some(dynamic_viscosity),
424        })
425    }
426
427    /// Computes buoyant force for a displaced volume in this fluid.
428    ///
429    /// # Examples
430    ///
431    /// ```rust
432    /// use use_fluid::Fluid;
433    ///
434    /// let water = Fluid::new(1000.0).unwrap();
435    /// let force = water.buoyant_force(0.01, 9.80665).unwrap();
436    ///
437    /// assert!((force - 98.0665).abs() < 1.0e-10);
438    /// ```
439    #[must_use]
440    pub fn buoyant_force(
441        &self,
442        displaced_volume: f64,
443        gravitational_acceleration: f64,
444    ) -> Option<f64> {
445        buoyant_force(self.density, displaced_volume, gravitational_acceleration)
446    }
447
448    /// Computes hydrostatic pressure at a depth in this fluid.
449    #[must_use]
450    pub fn hydrostatic_pressure(&self, gravitational_acceleration: f64, depth: f64) -> Option<f64> {
451        hydrostatic_pressure(self.density, gravitational_acceleration, depth)
452    }
453
454    /// Computes dynamic pressure for this fluid at a given velocity.
455    #[must_use]
456    pub fn dynamic_pressure(&self, velocity: f64) -> Option<f64> {
457        dynamic_pressure(self.density, velocity)
458    }
459
460    /// Computes Reynolds number for this fluid when dynamic viscosity is available.
461    #[must_use]
462    pub fn reynolds_number(&self, velocity: f64, characteristic_length: f64) -> Option<f64> {
463        let dynamic_viscosity = self.dynamic_viscosity?;
464
465        reynolds_number(
466            self.density,
467            velocity,
468            characteristic_length,
469            dynamic_viscosity,
470        )
471    }
472}
473
474/// A simple cross-sectional pipe flow with area and scalar velocity.
475#[derive(Debug, Clone, Copy, PartialEq)]
476pub struct PipeFlow {
477    pub area: f64,
478    pub velocity: f64,
479}
480
481impl PipeFlow {
482    /// Creates a pipe flow when `area` is non-negative and both values are finite.
483    #[must_use]
484    pub fn new(area: f64, velocity: f64) -> Option<Self> {
485        if !nonnegative(area) || !velocity.is_finite() {
486            return None;
487        }
488
489        Some(Self { area, velocity })
490    }
491
492    /// Computes volumetric flow rate for this pipe flow.
493    ///
494    /// # Examples
495    ///
496    /// ```rust
497    /// use use_fluid::PipeFlow;
498    ///
499    /// let flow = PipeFlow::new(2.0, 3.0).unwrap();
500    ///
501    /// assert_eq!(flow.volumetric_flow_rate(), Some(6.0));
502    /// ```
503    #[must_use]
504    pub fn volumetric_flow_rate(&self) -> Option<f64> {
505        volumetric_flow_rate(self.area, self.velocity)
506    }
507
508    /// Computes mass flow rate for this pipe flow with a provided density.
509    #[must_use]
510    pub fn mass_flow_rate(&self, density: f64) -> Option<f64> {
511        mass_flow_rate(density, self.volumetric_flow_rate()?)
512    }
513}
514
515#[cfg(test)]
516#[allow(clippy::float_cmp)]
517mod tests {
518    use super::{
519        Fluid, PipeFlow, absolute_pressure, bernoulli_pressure, buoyant_force, continuity_area,
520        continuity_velocity, displaced_volume_from_buoyant_force, drag_force, dynamic_pressure,
521        dynamic_viscosity, hydrostatic_pressure, kinematic_viscosity, mass_flow_rate,
522        reynolds_number, velocity_from_flow_rate, volumetric_flow_rate,
523    };
524
525    fn approx_eq(left: f64, right: f64, tolerance: f64) {
526        let delta = (left - right).abs();
527
528        assert!(
529            delta <= tolerance,
530            "left={left} right={right} delta={delta} tolerance={tolerance}"
531        );
532    }
533
534    #[test]
535    fn buoyancy_helpers_cover_valid_and_invalid_inputs() {
536        approx_eq(
537            buoyant_force(1000.0, 0.01, 9.80665).unwrap(),
538            98.0665,
539            1.0e-10,
540        );
541        assert_eq!(buoyant_force(-1000.0, 0.01, 9.80665), None);
542        assert_eq!(buoyant_force(1000.0, -0.01, 9.80665), None);
543
544        approx_eq(
545            displaced_volume_from_buoyant_force(98.0665, 1000.0, 9.80665).unwrap(),
546            0.01,
547            1.0e-12,
548        );
549        assert_eq!(
550            displaced_volume_from_buoyant_force(98.0665, 0.0, 9.80665),
551            None
552        );
553    }
554
555    #[test]
556    fn hydrostatic_helpers_cover_valid_and_invalid_inputs() {
557        approx_eq(
558            hydrostatic_pressure(1000.0, 9.80665, 10.0).unwrap(),
559            98_066.5,
560            1.0e-9,
561        );
562        assert_eq!(hydrostatic_pressure(1000.0, 9.80665, -1.0), None);
563
564        approx_eq(
565            absolute_pressure(101_325.0, 1000.0, 9.80665, 10.0).unwrap(),
566            199_391.5,
567            1.0e-9,
568        );
569        assert_eq!(absolute_pressure(-1.0, 1000.0, 9.80665, 10.0), None);
570    }
571
572    #[test]
573    fn flow_rate_helpers_cover_valid_and_invalid_inputs() {
574        assert_eq!(volumetric_flow_rate(2.0, 3.0), Some(6.0));
575        assert_eq!(volumetric_flow_rate(2.0, -3.0), Some(-6.0));
576        assert_eq!(volumetric_flow_rate(-2.0, 3.0), None);
577
578        assert_eq!(velocity_from_flow_rate(6.0, 2.0), Some(3.0));
579        assert_eq!(velocity_from_flow_rate(6.0, 0.0), None);
580
581        assert_eq!(mass_flow_rate(1000.0, 0.5), Some(500.0));
582        assert_eq!(mass_flow_rate(-1000.0, 0.5), None);
583    }
584
585    #[test]
586    fn continuity_helpers_cover_valid_and_invalid_inputs() {
587        assert_eq!(continuity_velocity(2.0, 3.0, 1.0), Some(6.0));
588        assert_eq!(continuity_velocity(2.0, 3.0, 0.0), None);
589
590        assert_eq!(continuity_area(2.0, 3.0, 6.0), Some(1.0));
591        assert_eq!(continuity_area(2.0, 3.0, 0.0), None);
592    }
593
594    #[test]
595    fn bernoulli_and_dynamic_pressure_cover_common_cases() {
596        assert_eq!(dynamic_pressure(1000.0, 3.0), Some(4500.0));
597        assert_eq!(dynamic_pressure(-1000.0, 3.0), None);
598
599        let pressure = bernoulli_pressure(100_000.0, 1000.0, 4.0, 2.0, 9.80665, 10.0, 5.0);
600        assert!(pressure.is_some_and(f64::is_finite));
601    }
602
603    #[test]
604    fn viscosity_helpers_cover_reynolds_and_conversions() {
605        approx_eq(
606            reynolds_number(1000.0, 2.0, 0.1, 0.001).unwrap(),
607            200_000.0,
608            1.0e-9,
609        );
610        assert_eq!(reynolds_number(1000.0, 2.0, 0.1, 0.0), None);
611
612        approx_eq(
613            kinematic_viscosity(0.001, 1000.0).unwrap(),
614            0.000_001,
615            1.0e-15,
616        );
617        assert_eq!(kinematic_viscosity(0.001, 0.0), None);
618
619        approx_eq(
620            dynamic_viscosity(0.000_001, 1000.0).unwrap(),
621            0.001,
622            1.0e-15,
623        );
624        assert_eq!(dynamic_viscosity(-0.000_001, 1000.0), None);
625    }
626
627    #[test]
628    fn drag_force_handles_standard_cases() {
629        approx_eq(drag_force(1.225, 10.0, 0.47, 1.0).unwrap(), 28.7875, 1.0e-9);
630        approx_eq(
631            drag_force(1.225, -10.0, 0.47, 1.0).unwrap(),
632            28.7875,
633            1.0e-9,
634        );
635        assert_eq!(drag_force(1.225, 10.0, -0.47, 1.0), None);
636    }
637
638    #[test]
639    fn fluid_methods_delegate_to_public_functions() {
640        approx_eq(
641            Fluid::new(1000.0)
642                .unwrap()
643                .hydrostatic_pressure(9.80665, 10.0)
644                .unwrap(),
645            98_066.5,
646            1.0e-9,
647        );
648        approx_eq(
649            Fluid::with_dynamic_viscosity(1000.0, 0.001)
650                .unwrap()
651                .reynolds_number(2.0, 0.1)
652                .unwrap(),
653            200_000.0,
654            1.0e-9,
655        );
656        assert_eq!(Fluid::new(1000.0).unwrap().reynolds_number(2.0, 0.1), None);
657        assert_eq!(Fluid::new(-1000.0), None);
658    }
659
660    #[test]
661    fn pipe_flow_methods_delegate_to_public_functions() {
662        assert_eq!(
663            PipeFlow::new(2.0, 3.0).unwrap().volumetric_flow_rate(),
664            Some(6.0)
665        );
666        assert_eq!(
667            PipeFlow::new(2.0, 3.0).unwrap().mass_flow_rate(1000.0),
668            Some(6000.0)
669        );
670        assert_eq!(PipeFlow::new(-2.0, 3.0), None);
671    }
672}