1use std::collections::HashMap;
7
8pub const WATER_DENSITY_KG_M3: f64 = 998.2;
14pub const AIR_DENSITY_KG_M3: f64 = 1.204;
16pub const WATER_VISCOSITY_PA_S: f64 = 1.002e-3;
18pub const AIR_VISCOSITY_PA_S: f64 = 1.825e-5;
20pub const GRAVITY_M_S2: f64 = 9.81;
22
23#[derive(Debug, Clone)]
29pub struct FluidProperties {
30 pub name: String,
32 pub density: f64,
34 pub dynamic_viscosity: f64,
36 pub kinematic_viscosity: f64,
38 pub specific_heat: f64,
40 pub thermal_conductivity: f64,
42}
43
44impl FluidProperties {
45 pub fn water() -> Self {
47 let density = WATER_DENSITY_KG_M3;
48 let dynamic_viscosity = WATER_VISCOSITY_PA_S;
49 Self {
50 name: "Water".to_string(),
51 density,
52 dynamic_viscosity,
53 kinematic_viscosity: dynamic_viscosity / density,
54 specific_heat: 4182.0,
55 thermal_conductivity: 0.598,
56 }
57 }
58
59 pub fn air() -> Self {
61 let density = AIR_DENSITY_KG_M3;
62 let dynamic_viscosity = AIR_VISCOSITY_PA_S;
63 Self {
64 name: "Air".to_string(),
65 density,
66 dynamic_viscosity,
67 kinematic_viscosity: dynamic_viscosity / density,
68 specific_heat: 1005.0,
69 thermal_conductivity: 0.0257,
70 }
71 }
72
73 pub fn oil(density: f64, viscosity: f64) -> Self {
75 Self {
76 name: "Oil".to_string(),
77 density,
78 dynamic_viscosity: viscosity,
79 kinematic_viscosity: viscosity / density,
80 specific_heat: 1900.0,
81 thermal_conductivity: 0.145,
82 }
83 }
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub enum FlowRegime {
93 Laminar,
95 Transitional,
97 Turbulent,
99}
100
101#[derive(Debug, Clone)]
107pub struct PipeFlowResult {
108 pub reynolds_number: f64,
110 pub regime: FlowRegime,
112 pub friction_factor: f64,
114 pub pressure_drop_pa: f64,
116 pub flow_velocity_m_s: f64,
118 pub head_loss_m: f64,
120}
121
122#[derive(Debug)]
128pub enum FluidError {
129 InvalidInput(String),
131 ConvergenceFailure(String),
133 PhysicallyImpossible(String),
135}
136
137impl std::fmt::Display for FluidError {
138 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
139 match self {
140 FluidError::InvalidInput(msg) => write!(f, "Invalid input: {}", msg),
141 FluidError::ConvergenceFailure(msg) => write!(f, "Convergence failure: {}", msg),
142 FluidError::PhysicallyImpossible(msg) => write!(f, "Physically impossible: {}", msg),
143 }
144 }
145}
146
147impl std::error::Error for FluidError {}
148
149pub struct FluidDynamics;
155
156impl FluidDynamics {
157 pub fn reynolds_number(density: f64, velocity: f64, length: f64, viscosity: f64) -> f64 {
168 density * velocity * length / viscosity
169 }
170
171 pub fn flow_regime(re: f64) -> FlowRegime {
173 if re < 2300.0 {
174 FlowRegime::Laminar
175 } else if re < 4000.0 {
176 FlowRegime::Transitional
177 } else {
178 FlowRegime::Turbulent
179 }
180 }
181
182 pub fn darcy_friction_factor(
196 re: f64,
197 roughness: f64,
198 diameter: f64,
199 ) -> Result<f64, FluidError> {
200 if re <= 0.0 {
201 return Err(FluidError::InvalidInput(
202 "Reynolds number must be positive".to_string(),
203 ));
204 }
205 if diameter <= 0.0 {
206 return Err(FluidError::InvalidInput(
207 "Pipe diameter must be positive".to_string(),
208 ));
209 }
210 if roughness < 0.0 {
211 return Err(FluidError::InvalidInput(
212 "Surface roughness must be non-negative".to_string(),
213 ));
214 }
215
216 if re < 2300.0 {
217 return Ok(64.0 / re);
219 }
220
221 let relative_roughness = roughness / diameter;
223 let arg = relative_roughness / 3.7 + 5.74 / re.powf(0.9);
224 if arg <= 0.0 {
225 return Err(FluidError::ConvergenceFailure(
226 "Swamee-Jain argument is non-positive".to_string(),
227 ));
228 }
229 let log_val = arg.log10();
230 Ok(0.25 / (log_val * log_val))
231 }
232
233 pub fn pressure_drop(
245 friction_factor: f64,
246 length: f64,
247 diameter: f64,
248 density: f64,
249 velocity: f64,
250 ) -> f64 {
251 friction_factor * (length / diameter) * (density * velocity * velocity / 2.0)
252 }
253
254 pub fn analyze_pipe_flow(
264 fluid: &FluidProperties,
265 diameter: f64,
266 length: f64,
267 velocity: f64,
268 roughness: f64,
269 ) -> Result<PipeFlowResult, FluidError> {
270 if fluid.density <= 0.0 {
271 return Err(FluidError::InvalidInput(
272 "Fluid density must be positive".to_string(),
273 ));
274 }
275 if diameter <= 0.0 {
276 return Err(FluidError::InvalidInput(
277 "Diameter must be positive".to_string(),
278 ));
279 }
280 if length <= 0.0 {
281 return Err(FluidError::InvalidInput(
282 "Pipe length must be positive".to_string(),
283 ));
284 }
285
286 let re = Self::reynolds_number(fluid.density, velocity, diameter, fluid.dynamic_viscosity);
287 let regime = Self::flow_regime(re);
288 let friction_factor = Self::darcy_friction_factor(re, roughness, diameter)?;
289 let pressure_drop_pa =
290 Self::pressure_drop(friction_factor, length, diameter, fluid.density, velocity);
291 let head_loss_m = pressure_drop_pa / (fluid.density * GRAVITY_M_S2);
292
293 Ok(PipeFlowResult {
294 reynolds_number: re,
295 regime,
296 friction_factor,
297 pressure_drop_pa,
298 flow_velocity_m_s: velocity,
299 head_loss_m,
300 })
301 }
302
303 pub fn bernoulli_p2(p1: f64, v1: f64, h1: f64, v2: f64, h2: f64, density: f64) -> f64 {
313 let dynamic_1 = 0.5 * density * v1 * v1;
314 let dynamic_2 = 0.5 * density * v2 * v2;
315 let potential_1 = density * GRAVITY_M_S2 * h1;
316 let potential_2 = density * GRAVITY_M_S2 * h2;
317 p1 + dynamic_1 + potential_1 - dynamic_2 - potential_2
318 }
319
320 pub fn continuity_velocity(v1: f64, d1: f64, d2: f64) -> Result<f64, FluidError> {
329 if d1 <= 0.0 {
330 return Err(FluidError::InvalidInput(
331 "Upstream diameter must be positive".to_string(),
332 ));
333 }
334 if d2 <= 0.0 {
335 return Err(FluidError::InvalidInput(
336 "Downstream diameter must be positive".to_string(),
337 ));
338 }
339 Ok(v1 * (d1 / d2) * (d1 / d2))
340 }
341
342 pub fn hydraulic_diameter(area: f64, perimeter: f64) -> Result<f64, FluidError> {
351 if area <= 0.0 {
352 return Err(FluidError::InvalidInput(
353 "Cross-sectional area must be positive".to_string(),
354 ));
355 }
356 if perimeter <= 0.0 {
357 return Err(FluidError::InvalidInput(
358 "Perimeter must be positive".to_string(),
359 ));
360 }
361 Ok(4.0 * area / perimeter)
362 }
363
364 pub fn nusselt_turbulent(re: f64, prandtl: f64) -> f64 {
371 0.023 * re.powf(0.8) * prandtl.powf(0.4)
372 }
373
374 pub fn prandtl_number(viscosity: f64, specific_heat: f64, thermal_conductivity: f64) -> f64 {
376 viscosity * specific_heat / thermal_conductivity
377 }
378
379 pub fn convective_htc(
384 nusselt: f64,
385 conductivity: f64,
386 diameter: f64,
387 ) -> Result<f64, FluidError> {
388 if diameter <= 0.0 {
389 return Err(FluidError::InvalidInput(
390 "Diameter must be positive".to_string(),
391 ));
392 }
393 Ok(nusselt * conductivity / diameter)
394 }
395
396 pub fn sphere_drag_force(cd: f64, density: f64, velocity: f64, radius: f64) -> f64 {
402 let area = std::f64::consts::PI * radius * radius;
403 cd * 0.5 * density * velocity * velocity * area
404 }
405
406 pub fn sphere_drag_coefficient(re: f64) -> f64 {
411 if re <= 0.0 {
412 return f64::INFINITY;
413 }
414 if re < 1.0 {
415 24.0 / re
417 } else if re < 1000.0 {
418 24.0 / re * (1.0 + 0.15 * re.powf(0.687))
420 } else {
421 0.44
423 }
424 }
425}
426
427#[derive(Debug, Default)]
433pub struct FluidRegistry {
434 fluids: HashMap<String, FluidProperties>,
435}
436
437impl FluidRegistry {
438 pub fn new() -> Self {
440 Self {
441 fluids: HashMap::new(),
442 }
443 }
444
445 pub fn register(&mut self, fluid: FluidProperties) {
447 self.fluids.insert(fluid.name.clone(), fluid);
448 }
449
450 pub fn get(&self, name: &str) -> Option<&FluidProperties> {
452 self.fluids.get(name)
453 }
454}
455
456#[cfg(test)]
461mod tests {
462 use super::*;
463
464 const EPS: f64 = 1e-6;
465
466 #[test]
469 fn test_water_properties() {
470 let w = FluidProperties::water();
471 assert_eq!(w.name, "Water");
472 assert!((w.density - 998.2).abs() < EPS);
473 assert!((w.dynamic_viscosity - 1.002e-3).abs() < 1e-10);
474 let expected_kv = 1.002e-3 / 998.2;
475 assert!((w.kinematic_viscosity - expected_kv).abs() < 1e-12);
476 }
477
478 #[test]
479 fn test_air_properties() {
480 let a = FluidProperties::air();
481 assert_eq!(a.name, "Air");
482 assert!((a.density - 1.204).abs() < EPS);
483 assert!((a.dynamic_viscosity - 1.825e-5).abs() < 1e-10);
484 }
485
486 #[test]
487 fn test_oil_properties() {
488 let o = FluidProperties::oil(870.0, 0.05);
489 assert_eq!(o.name, "Oil");
490 assert!((o.density - 870.0).abs() < EPS);
491 assert!((o.kinematic_viscosity - 0.05 / 870.0).abs() < 1e-10);
492 }
493
494 #[test]
497 fn test_reynolds_number_basic() {
498 let re = FluidDynamics::reynolds_number(998.2, 1.0, 0.05, 1.002e-3);
500 assert!((re - 49800.0).abs() < 50.0, "re = {}", re);
502 }
503
504 #[test]
505 fn test_reynolds_number_air() {
506 let re = FluidDynamics::reynolds_number(1.204, 5.0, 0.1, 1.825e-5);
507 let expected = 1.204 * 5.0 * 0.1 / 1.825e-5;
508 assert!((re - expected).abs() / expected < 1e-9);
509 }
510
511 #[test]
512 fn test_reynolds_number_formula() {
513 let re = FluidDynamics::reynolds_number(1000.0, 2.0, 0.02, 0.001);
514 assert!((re - 40_000.0).abs() < EPS);
515 }
516
517 #[test]
520 fn test_flow_regime_laminar_boundary() {
521 assert_eq!(FluidDynamics::flow_regime(2299.9), FlowRegime::Laminar);
522 }
523
524 #[test]
525 fn test_flow_regime_laminar_low() {
526 assert_eq!(FluidDynamics::flow_regime(100.0), FlowRegime::Laminar);
527 }
528
529 #[test]
530 fn test_flow_regime_transitional_lower() {
531 assert_eq!(FluidDynamics::flow_regime(2300.0), FlowRegime::Transitional);
532 }
533
534 #[test]
535 fn test_flow_regime_transitional_upper() {
536 assert_eq!(FluidDynamics::flow_regime(3999.9), FlowRegime::Transitional);
537 }
538
539 #[test]
540 fn test_flow_regime_turbulent_boundary() {
541 assert_eq!(FluidDynamics::flow_regime(4000.0), FlowRegime::Turbulent);
542 }
543
544 #[test]
545 fn test_flow_regime_turbulent_high() {
546 assert_eq!(FluidDynamics::flow_regime(100_000.0), FlowRegime::Turbulent);
547 }
548
549 #[test]
552 fn test_laminar_friction_factor_64_over_re() {
553 let re = 1000.0;
554 let f = FluidDynamics::darcy_friction_factor(re, 0.0, 0.05).expect("ok");
555 assert!((f - 64.0 / re).abs() < EPS, "f = {}", f);
556 }
557
558 #[test]
559 fn test_laminar_friction_factor_re_100() {
560 let f = FluidDynamics::darcy_friction_factor(100.0, 0.0, 0.05).expect("ok");
561 assert!((f - 0.64).abs() < EPS, "f = {}", f);
562 }
563
564 #[test]
565 fn test_turbulent_friction_swamee_jain() {
566 let re = 100_000.0;
568 let diameter = 0.1;
569 let roughness = 0.0; let f = FluidDynamics::darcy_friction_factor(re, roughness, diameter).expect("ok");
571 let arg = 5.74 / re.powf(0.9);
573 let expected = 0.25 / (arg.log10() * arg.log10());
574 assert!((f - expected).abs() / expected < 1e-9, "f = {}", f);
575 }
576
577 #[test]
578 fn test_turbulent_rough_pipe() {
579 let f = FluidDynamics::darcy_friction_factor(50_000.0, 4.6e-5, 0.05).expect("ok");
580 assert!(f > 0.01 && f < 0.1, "f out of reasonable range: {}", f);
582 }
583
584 #[test]
585 fn test_friction_factor_invalid_re() {
586 assert!(FluidDynamics::darcy_friction_factor(0.0, 0.0, 0.05).is_err());
587 assert!(FluidDynamics::darcy_friction_factor(-1.0, 0.0, 0.05).is_err());
588 }
589
590 #[test]
591 fn test_friction_factor_invalid_diameter() {
592 assert!(FluidDynamics::darcy_friction_factor(1000.0, 0.0, 0.0).is_err());
593 assert!(FluidDynamics::darcy_friction_factor(1000.0, 0.0, -0.1).is_err());
594 }
595
596 #[test]
597 fn test_friction_factor_negative_roughness() {
598 assert!(FluidDynamics::darcy_friction_factor(10_000.0, -0.001, 0.05).is_err());
599 }
600
601 #[test]
604 fn test_pressure_drop_formula() {
605 let dp = FluidDynamics::pressure_drop(0.02, 10.0, 0.05, 1000.0, 1.0);
607 let expected = 0.02 * (10.0 / 0.05) * (1000.0 * 1.0 / 2.0);
608 assert!((dp - expected).abs() < EPS, "dp = {}", dp);
609 }
610
611 #[test]
612 fn test_pressure_drop_zero_velocity() {
613 let dp = FluidDynamics::pressure_drop(0.03, 5.0, 0.1, 1000.0, 0.0);
614 assert!((dp).abs() < EPS);
615 }
616
617 #[test]
620 fn test_analyze_pipe_flow_water_laminar() {
621 let water = FluidProperties::water();
622 let result = FluidDynamics::analyze_pipe_flow(&water, 0.05, 10.0, 0.01, 0.0)
624 .expect("should succeed");
625 assert_eq!(result.regime, FlowRegime::Laminar);
626 assert!(result.reynolds_number < 2300.0);
627 let expected_f = 64.0 / result.reynolds_number;
628 assert!((result.friction_factor - expected_f).abs() < EPS);
629 }
630
631 #[test]
632 fn test_analyze_pipe_flow_water_turbulent() {
633 let water = FluidProperties::water();
634 let result = FluidDynamics::analyze_pipe_flow(&water, 0.05, 10.0, 2.0, 4.6e-5)
635 .expect("should succeed");
636 assert_eq!(result.regime, FlowRegime::Turbulent);
637 assert!(result.pressure_drop_pa > 0.0);
638 assert!(result.head_loss_m > 0.0);
639 let expected_hl = result.pressure_drop_pa / (water.density * GRAVITY_M_S2);
641 assert!((result.head_loss_m - expected_hl).abs() < 1e-6);
642 }
643
644 #[test]
645 fn test_analyze_pipe_flow_air() {
646 let air = FluidProperties::air();
647 let result =
648 FluidDynamics::analyze_pipe_flow(&air, 0.1, 5.0, 10.0, 0.0).expect("should succeed");
649 assert!(result.pressure_drop_pa > 0.0);
650 }
651
652 #[test]
653 fn test_analyze_pipe_flow_invalid_density() {
654 let mut bad_fluid = FluidProperties::water();
655 bad_fluid.density = -1.0;
656 assert!(FluidDynamics::analyze_pipe_flow(&bad_fluid, 0.05, 10.0, 1.0, 0.0).is_err());
657 }
658
659 #[test]
660 fn test_analyze_pipe_flow_invalid_diameter() {
661 let water = FluidProperties::water();
662 assert!(FluidDynamics::analyze_pipe_flow(&water, 0.0, 10.0, 1.0, 0.0).is_err());
663 }
664
665 #[test]
666 fn test_analyze_pipe_flow_invalid_length() {
667 let water = FluidProperties::water();
668 assert!(FluidDynamics::analyze_pipe_flow(&water, 0.05, 0.0, 1.0, 0.0).is_err());
669 }
670
671 #[test]
674 fn test_bernoulli_p2_same_velocity_height() {
675 let p2 = FluidDynamics::bernoulli_p2(101_325.0, 2.0, 0.0, 2.0, 0.0, 1000.0);
677 assert!((p2 - 101_325.0).abs() < EPS);
678 }
679
680 #[test]
681 fn test_bernoulli_p2_speed_increase() {
682 let p2 = FluidDynamics::bernoulli_p2(200_000.0, 1.0, 0.0, 3.0, 0.0, 1000.0);
684 assert!(p2 < 200_000.0);
685 }
686
687 #[test]
688 fn test_bernoulli_p2_height_increase() {
689 let p2 = FluidDynamics::bernoulli_p2(200_000.0, 1.0, 0.0, 1.0, 5.0, 1000.0);
691 let expected = 200_000.0 - 1000.0 * GRAVITY_M_S2 * 5.0;
692 assert!((p2 - expected).abs() < EPS);
693 }
694
695 #[test]
696 fn test_bernoulli_p2_formula() {
697 let rho = 998.2;
698 let p2 = FluidDynamics::bernoulli_p2(100_000.0, 2.0, 1.0, 4.0, 2.0, rho);
699 let expected = 100_000.0
700 + 0.5 * rho * (2.0_f64.powi(2) - 4.0_f64.powi(2))
701 + rho * GRAVITY_M_S2 * (1.0 - 2.0);
702 assert!((p2 - expected).abs() < 1e-3);
703 }
704
705 #[test]
708 fn test_continuity_velocity_same_diameter() {
709 let v2 = FluidDynamics::continuity_velocity(5.0, 0.1, 0.1).expect("ok");
710 assert!((v2 - 5.0).abs() < EPS);
711 }
712
713 #[test]
714 fn test_continuity_velocity_halved_diameter() {
715 let v2 = FluidDynamics::continuity_velocity(2.0, 0.1, 0.05).expect("ok");
717 assert!((v2 - 8.0).abs() < EPS, "v2 = {}", v2);
718 }
719
720 #[test]
721 fn test_continuity_velocity_area_ratio() {
722 let v1 = 3.0;
723 let d1 = 0.2;
724 let d2 = 0.1;
725 let v2 = FluidDynamics::continuity_velocity(v1, d1, d2).expect("ok");
726 assert!((v2 - v1 * (d1 / d2).powi(2)).abs() < EPS);
727 }
728
729 #[test]
730 fn test_continuity_velocity_zero_d1() {
731 assert!(FluidDynamics::continuity_velocity(1.0, 0.0, 0.05).is_err());
732 }
733
734 #[test]
735 fn test_continuity_velocity_zero_d2() {
736 assert!(FluidDynamics::continuity_velocity(1.0, 0.05, 0.0).is_err());
737 }
738
739 #[test]
742 fn test_hydraulic_diameter_circular() {
743 let r = 0.05_f64;
745 let area = std::f64::consts::PI * r * r;
746 let perimeter = 2.0 * std::f64::consts::PI * r;
747 let dh = FluidDynamics::hydraulic_diameter(area, perimeter).expect("ok");
748 assert!((dh - 2.0 * r).abs() < 1e-10, "dh = {}", dh);
749 }
750
751 #[test]
752 fn test_hydraulic_diameter_square() {
753 let a = 0.1_f64;
755 let dh = FluidDynamics::hydraulic_diameter(a * a, 4.0 * a).expect("ok");
756 assert!((dh - a).abs() < EPS, "dh = {}", dh);
757 }
758
759 #[test]
760 fn test_hydraulic_diameter_invalid_area() {
761 assert!(FluidDynamics::hydraulic_diameter(0.0, 0.5).is_err());
762 }
763
764 #[test]
765 fn test_hydraulic_diameter_invalid_perimeter() {
766 assert!(FluidDynamics::hydraulic_diameter(0.01, 0.0).is_err());
767 }
768
769 #[test]
772 fn test_nusselt_turbulent_formula() {
773 let re = 10_000.0;
774 let pr = 7.0;
775 let nu = FluidDynamics::nusselt_turbulent(re, pr);
776 let expected = 0.023 * re.powf(0.8) * pr.powf(0.4);
777 assert!((nu - expected).abs() / expected < 1e-9);
778 }
779
780 #[test]
781 fn test_nusselt_turbulent_water() {
782 let nu = FluidDynamics::nusselt_turbulent(50_000.0, 6.99);
784 assert!(nu > 100.0, "nu = {}", nu);
785 }
786
787 #[test]
790 fn test_prandtl_number_water() {
791 let pr = FluidDynamics::prandtl_number(1.002e-3, 4182.0, 0.598);
792 assert!((pr - 7.0).abs() < 0.1, "Pr = {}", pr);
794 }
795
796 #[test]
797 fn test_prandtl_number_air() {
798 let pr = FluidDynamics::prandtl_number(1.825e-5, 1005.0, 0.0257);
799 assert!((pr - 0.71).abs() < 0.01, "Pr = {}", pr);
801 }
802
803 #[test]
804 fn test_prandtl_number_formula() {
805 let mu = 0.002;
806 let cp = 2000.0;
807 let k = 0.5;
808 let pr = FluidDynamics::prandtl_number(mu, cp, k);
809 assert!((pr - mu * cp / k).abs() < EPS);
810 }
811
812 #[test]
815 fn test_convective_htc_basic() {
816 let h = FluidDynamics::convective_htc(200.0, 0.598, 0.05).expect("ok");
817 let expected = 200.0 * 0.598 / 0.05;
818 assert!((h - expected).abs() < EPS);
819 }
820
821 #[test]
822 fn test_convective_htc_invalid_diameter() {
823 assert!(FluidDynamics::convective_htc(100.0, 0.5, 0.0).is_err());
824 assert!(FluidDynamics::convective_htc(100.0, 0.5, -0.01).is_err());
825 }
826
827 #[test]
830 fn test_sphere_drag_force_formula() {
831 let cd = 0.44;
832 let rho = 1.204;
833 let v = 10.0;
834 let r = 0.05;
835 let f = FluidDynamics::sphere_drag_force(cd, rho, v, r);
836 let area = std::f64::consts::PI * r * r;
837 let expected = cd * 0.5 * rho * v * v * area;
838 assert!((f - expected).abs() / expected < 1e-9);
839 }
840
841 #[test]
842 fn test_sphere_drag_force_zero_velocity() {
843 let f = FluidDynamics::sphere_drag_force(0.44, 1.204, 0.0, 0.05);
844 assert!(f.abs() < EPS);
845 }
846
847 #[test]
848 fn test_sphere_drag_coefficient_stokes() {
849 let re = 0.5;
850 let cd = FluidDynamics::sphere_drag_coefficient(re);
851 let expected = 24.0 / re;
852 assert!((cd - expected).abs() < EPS, "cd = {}", cd);
853 }
854
855 #[test]
856 fn test_sphere_drag_coefficient_newton_region() {
857 let cd = FluidDynamics::sphere_drag_coefficient(100_000.0);
858 assert!((cd - 0.44).abs() < EPS);
859 }
860
861 #[test]
862 fn test_sphere_drag_coefficient_intermediate() {
863 let re = 100.0;
864 let cd = FluidDynamics::sphere_drag_coefficient(re);
865 assert!(cd > 0.44 && cd < 24.0 / re + 1.0);
867 }
868
869 #[test]
870 fn test_sphere_drag_coefficient_zero_re() {
871 let cd = FluidDynamics::sphere_drag_coefficient(0.0);
872 assert!(cd.is_infinite());
873 }
874
875 #[test]
878 fn test_fluid_registry() {
879 let mut reg = FluidRegistry::new();
880 reg.register(FluidProperties::water());
881 reg.register(FluidProperties::air());
882 assert!(reg.get("Water").is_some());
883 assert!(reg.get("Air").is_some());
884 assert!(reg.get("Ethanol").is_none());
885 }
886
887 #[test]
890 fn test_fluid_error_display() {
891 let e = FluidError::InvalidInput("bad value".to_string());
892 assert!(e.to_string().contains("bad value"));
893 let e2 = FluidError::ConvergenceFailure("too slow".to_string());
894 assert!(e2.to_string().contains("too slow"));
895 let e3 = FluidError::PhysicallyImpossible("negative density".to_string());
896 assert!(e3.to_string().contains("negative density"));
897 }
898
899 #[test]
902 fn test_pressure_drop_scaling_with_velocity() {
903 let dp1 = FluidDynamics::pressure_drop(0.025, 10.0, 0.05, 1000.0, 2.0);
905 let dp2 = FluidDynamics::pressure_drop(0.025, 10.0, 0.05, 1000.0, 4.0);
906 assert!((dp2 / dp1 - 4.0).abs() < 1e-9, "ratio = {}", dp2 / dp1);
907 }
908
909 #[test]
910 fn test_bernoulli_venturi_tube() {
911 let rho = 1000.0;
913 let v1 = 1.0;
914 let v2 = FluidDynamics::continuity_velocity(v1, 0.1, 0.05).expect("ok");
915 assert!((v2 - 4.0 * v1).abs() < EPS);
916 let p2 = FluidDynamics::bernoulli_p2(200_000.0, v1, 0.0, v2, 0.0, rho);
917 assert!(p2 < 200_000.0);
919 }
920
921 #[test]
922 fn test_head_loss_nondimensional_consistency() {
923 let water = FluidProperties::water();
924 let result =
925 FluidDynamics::analyze_pipe_flow(&water, 0.025, 100.0, 1.5, 4.6e-5).expect("ok");
926 let hl_check = result.pressure_drop_pa / (water.density * GRAVITY_M_S2);
928 assert!((result.head_loss_m - hl_check).abs() < 1e-6);
929 }
930}