1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4pub 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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[derive(Debug, Clone, Copy, PartialEq)]
395pub struct Fluid {
396 pub density: f64,
397 pub dynamic_viscosity: Option<f64>,
398}
399
400impl Fluid {
401 #[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 #[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 #[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 #[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 #[must_use]
456 pub fn dynamic_pressure(&self, velocity: f64) -> Option<f64> {
457 dynamic_pressure(self.density, velocity)
458 }
459
460 #[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#[derive(Debug, Clone, Copy, PartialEq)]
476pub struct PipeFlow {
477 pub area: f64,
478 pub velocity: f64,
479}
480
481impl PipeFlow {
482 #[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 #[must_use]
504 pub fn volumetric_flow_rate(&self) -> Option<f64> {
505 volumetric_flow_rate(self.area, self.velocity)
506 }
507
508 #[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}