use serde::{Deserialize, Serialize};
use crate::error::{PravashError, Result};
#[inline]
#[must_use]
pub fn buoyancy_force(fluid_density: f64, gravity: f64, displaced_volume: f64) -> f64 {
fluid_density * gravity * displaced_volume
}
#[inline]
#[must_use]
pub fn drag_force(
fluid_density: f64,
velocity: f64,
drag_coefficient: f64,
cross_section_area: f64,
) -> f64 {
0.5 * fluid_density * velocity * velocity * drag_coefficient * cross_section_area
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct DragCoefficient;
impl DragCoefficient {
pub const SPHERE: f64 = 0.47;
pub const CUBE: f64 = 1.05;
pub const CYLINDER: f64 = 0.82;
pub const STREAMLINED: f64 = 0.04;
pub const FLAT_PLATE: f64 = 1.28;
}
#[inline]
pub fn terminal_velocity(
mass: f64,
gravity: f64,
fluid_density: f64,
drag_coefficient: f64,
area: f64,
) -> Result<f64> {
if !gravity.is_finite() {
return Err(PravashError::InvalidParameter {
reason: "terminal_velocity: gravity must be finite".into(),
});
}
let denom = fluid_density * drag_coefficient * area;
if denom <= 0.0 {
return Err(PravashError::InvalidParameter {
reason: "terminal_velocity denominator (ρ·Cd·A) must be positive".into(),
});
}
let arg = 2.0 * mass * gravity / denom;
if arg < 0.0 {
return Err(PravashError::InvalidParameter {
reason: "terminal_velocity: m·g must be non-negative".into(),
});
}
Ok(arg.sqrt())
}
#[inline]
pub fn reynolds_number(
fluid_density: f64,
velocity: f64,
length: f64,
viscosity: f64,
) -> Result<f64> {
if viscosity <= 0.0 {
return Err(PravashError::InvalidViscosity { viscosity });
}
if !fluid_density.is_finite() {
return Err(PravashError::InvalidParameter {
reason: "reynolds_number: fluid_density must be finite".into(),
});
}
if !velocity.is_finite() {
return Err(PravashError::InvalidParameter {
reason: "reynolds_number: velocity must be finite".into(),
});
}
if !length.is_finite() {
return Err(PravashError::InvalidParameter {
reason: "reynolds_number: length must be finite".into(),
});
}
Ok(fluid_density * velocity * length / viscosity)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum FlowRegime {
Laminar,
Transitional,
Turbulent,
}
#[inline]
#[must_use]
pub fn classify_flow(re: f64) -> FlowRegime {
if re < 2300.0 {
FlowRegime::Laminar
} else if re < 4000.0 {
FlowRegime::Transitional
} else {
FlowRegime::Turbulent
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_buoyancy_water() {
let f = buoyancy_force(1000.0, 9.81, 1.0);
assert!((f - 9810.0).abs() < 0.1);
}
#[test]
fn test_buoyancy_zero_volume() {
assert!(buoyancy_force(1000.0, 9.81, 0.0).abs() < f64::EPSILON);
}
#[test]
fn test_drag_force() {
let f = drag_force(1.225, 10.0, DragCoefficient::SPHERE, 0.01);
assert!(f > 0.0);
}
#[test]
fn test_drag_zero_velocity() {
assert!(drag_force(1.225, 0.0, DragCoefficient::SPHERE, 0.01).abs() < f64::EPSILON);
}
#[test]
fn test_terminal_velocity() {
let vt = terminal_velocity(1.0, 9.81, 1.225, DragCoefficient::SPHERE, 0.01).unwrap();
assert!(vt > 0.0);
assert!(vt.is_finite());
}
#[test]
fn test_terminal_velocity_zero_denom() {
assert!(terminal_velocity(1.0, 9.81, 0.0, DragCoefficient::SPHERE, 0.01).is_err());
}
#[test]
fn test_terminal_velocity_negative_gravity() {
assert!(terminal_velocity(1.0, -9.81, 1.225, DragCoefficient::SPHERE, 0.01).is_err());
}
#[test]
fn test_reynolds_number() {
let re = reynolds_number(1000.0, 1.0, 0.1, 0.001).unwrap();
assert!((re - 100000.0).abs() < 1.0);
}
#[test]
fn test_reynolds_number_zero_viscosity() {
assert!(reynolds_number(1000.0, 1.0, 0.1, 0.0).is_err());
}
#[test]
fn test_flow_regime_laminar() {
assert_eq!(classify_flow(1000.0), FlowRegime::Laminar);
}
#[test]
fn test_flow_regime_transitional() {
assert_eq!(classify_flow(3000.0), FlowRegime::Transitional);
}
#[test]
fn test_flow_regime_turbulent() {
assert_eq!(classify_flow(10000.0), FlowRegime::Turbulent);
}
#[test]
fn test_drag_coefficients() {
const { assert!(DragCoefficient::STREAMLINED < DragCoefficient::SPHERE) };
const { assert!(DragCoefficient::SPHERE < DragCoefficient::FLAT_PLATE) };
}
}