1use std::f64::consts::PI;
19
20#[inline]
25fn add3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
26 [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
27}
28
29#[inline]
30fn sub3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
31 [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
32}
33
34#[inline]
35fn scale3(a: [f64; 3], s: f64) -> [f64; 3] {
36 [a[0] * s, a[1] * s, a[2] * s]
37}
38
39#[inline]
40fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 {
41 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
42}
43
44#[inline]
45fn cross3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
46 [
47 a[1] * b[2] - a[2] * b[1],
48 a[2] * b[0] - a[0] * b[2],
49 a[0] * b[1] - a[1] * b[0],
50 ]
51}
52
53#[inline]
54fn len3(a: [f64; 3]) -> f64 {
55 dot3(a, a).sqrt()
56}
57
58#[inline]
59fn normalize3(a: [f64; 3]) -> [f64; 3] {
60 let l = len3(a);
61 if l < 1e-14 {
62 [0.0, 0.0, 1.0]
63 } else {
64 scale3(a, 1.0 / l)
65 }
66}
67
68#[inline]
69fn neg3(a: [f64; 3]) -> [f64; 3] {
70 [-a[0], -a[1], -a[2]]
71}
72
73#[derive(Debug, Clone, Copy)]
79pub struct FluidProperties {
80 pub density: f64,
82 pub viscosity: f64,
84 pub kinematic_viscosity: f64,
86 pub surface_tension: f64,
88 pub speed_of_sound: f64,
90}
91
92impl FluidProperties {
93 pub fn new(density: f64, viscosity: f64, surface_tension: f64, speed_of_sound: f64) -> Self {
101 Self {
102 density,
103 viscosity,
104 kinematic_viscosity: viscosity / density,
105 surface_tension,
106 speed_of_sound,
107 }
108 }
109
110 pub fn water() -> Self {
112 Self::new(998.2, 1.002e-3, 0.0728, 1482.0)
113 }
114
115 pub fn air() -> Self {
117 Self::new(1.204, 1.81e-5, 0.0, 343.0)
118 }
119}
120
121#[derive(Debug, Clone)]
127pub struct RigidBodyFluidState {
128 pub position: [f64; 3],
130 pub velocity: [f64; 3],
132 pub angular_velocity: [f64; 3],
134 pub mass: f64,
136 pub inertia: [f64; 3],
138 pub radius: f64,
140 pub cross_section_area: f64,
142}
143
144impl RigidBodyFluidState {
145 pub fn new(position: [f64; 3], velocity: [f64; 3], mass: f64, radius: f64) -> Self {
147 let i = 0.4 * mass * radius * radius; Self {
149 position,
150 velocity,
151 angular_velocity: [0.0; 3],
152 mass,
153 inertia: [i, i, i],
154 radius,
155 cross_section_area: PI * radius * radius,
156 }
157 }
158}
159
160pub fn reynolds_number(velocity: f64, length: f64, kinematic_viscosity: f64) -> f64 {
171 if kinematic_viscosity < 1e-30 {
172 return f64::INFINITY;
173 }
174 velocity.abs() * length / kinematic_viscosity
175}
176
177#[derive(Debug, Clone, Copy)]
183pub struct BuoyancyResult {
184 pub force: [f64; 3],
186 pub submerged_volume: f64,
188 pub centre_of_buoyancy: [f64; 3],
190}
191
192pub fn buoyancy_sphere(
204 body: &RigidBodyFluidState,
205 fluid: &FluidProperties,
206 free_surface_y: f64,
207 gravity: f64,
208) -> BuoyancyResult {
209 let r = body.radius;
210 let cy = body.position[1];
211 let depth = free_surface_y - cy;
213
214 let (vol_sub, cob_y) = if depth >= r {
215 let vol = 4.0 / 3.0 * PI * r * r * r;
217 (vol, cy) } else if depth <= -r {
219 (0.0, cy)
221 } else {
222 let h = (r + depth).max(0.0).min(2.0 * r);
225 let vol = PI * h * h * (r - h / 3.0);
226 let cob_y_local = if vol > 1e-30 {
228 let cob_from_top = 3.0 * (2.0 * r - h).powi(2) / (4.0 * (3.0 * r - h).max(1e-15));
230 cy - r + h - cob_from_top
231 } else {
232 cy - r
233 };
234 (vol, cob_y_local)
235 };
236
237 let force_mag = fluid.density * gravity * vol_sub;
238 let force = [0.0, force_mag, 0.0]; let cob = [body.position[0], cob_y, body.position[2]];
240 BuoyancyResult {
241 force,
242 submerged_volume: vol_sub,
243 centre_of_buoyancy: cob,
244 }
245}
246
247pub fn buoyancy_force(fluid_density: f64, gravity: f64, submerged_volume: f64) -> [f64; 3] {
251 let mag = fluid_density * gravity * submerged_volume;
252 [0.0, mag, 0.0]
253}
254
255pub fn drag_coefficient_sphere(re: f64) -> f64 {
266 if re < 1e-10 {
267 return 24.0 / 1e-10; }
269 if re < 1.0 {
270 24.0 / re
271 } else if re <= 1000.0 {
272 (24.0 / re) * (1.0 + 0.15 * re.powf(0.687))
273 } else {
274 0.44
275 }
276}
277
278pub fn drag_force(
289 body_velocity: [f64; 3],
290 fluid_velocity: [f64; 3],
291 fluid: &FluidProperties,
292 cd: f64,
293 reference_area: f64,
294) -> [f64; 3] {
295 let v_rel = sub3(body_velocity, fluid_velocity);
296 let speed = len3(v_rel);
297 if speed < 1e-15 {
298 return [0.0; 3];
299 }
300 let mag = 0.5 * fluid.density * cd * reference_area * speed * speed;
301 let dir = normalize3(neg3(v_rel));
302 scale3(dir, mag)
303}
304
305pub fn drag_force_sphere(
307 body: &RigidBodyFluidState,
308 fluid_velocity: [f64; 3],
309 fluid: &FluidProperties,
310) -> [f64; 3] {
311 let v_rel = sub3(body.velocity, fluid_velocity);
312 let speed = len3(v_rel);
313 let re = reynolds_number(speed, 2.0 * body.radius, fluid.kinematic_viscosity);
314 let cd = drag_coefficient_sphere(re);
315 drag_force(
316 body.velocity,
317 fluid_velocity,
318 fluid,
319 cd,
320 body.cross_section_area,
321 )
322}
323
324pub fn lift_force(
330 body_velocity: [f64; 3],
331 fluid_velocity: [f64; 3],
332 fluid: &FluidProperties,
333 cl: f64,
334 reference_area: f64,
335) -> [f64; 3] {
336 let v_rel = sub3(body_velocity, fluid_velocity);
337 let speed = len3(v_rel);
338 if speed < 1e-15 {
339 return [0.0; 3];
340 }
341 let up = [0.0_f64, 1.0, 0.0];
342 let lift_dir = normalize3(cross3(v_rel, cross3(v_rel, up)));
343 let mag = 0.5 * fluid.density * cl * reference_area * speed * speed;
344 scale3(lift_dir, mag)
345}
346
347#[derive(Debug, Clone, Copy, PartialEq, Eq)]
353pub enum SubmersionState {
354 Airborne,
356 Crossing,
358 Submerged,
360}
361
362pub fn submersion_state_sphere(body: &RigidBodyFluidState, free_surface_y: f64) -> SubmersionState {
368 let cy = body.position[1];
369 let r = body.radius;
370 if cy - r >= free_surface_y {
371 SubmersionState::Airborne
372 } else if cy + r <= free_surface_y {
373 SubmersionState::Submerged
374 } else {
375 SubmersionState::Crossing
376 }
377}
378
379#[derive(Debug, Clone, Copy)]
381pub struct WaterEntryEvent {
382 pub position: [f64; 3],
384 pub velocity: [f64; 3],
386 pub entry_speed: f64,
388 pub vertical_speed: f64,
390 pub is_entry: bool,
392}
393
394pub fn detect_water_crossing(
398 prev: &RigidBodyFluidState,
399 curr: &RigidBodyFluidState,
400 free_surface_y: f64,
401) -> Option<WaterEntryEvent> {
402 let prev_state = submersion_state_sphere(prev, free_surface_y);
403 let curr_state = submersion_state_sphere(curr, free_surface_y);
404 if prev_state == curr_state {
405 return None;
406 }
407 let is_entry = matches!(
408 (prev_state, curr_state),
409 (SubmersionState::Airborne, SubmersionState::Crossing)
410 | (SubmersionState::Airborne, SubmersionState::Submerged)
411 | (SubmersionState::Crossing, SubmersionState::Submerged)
412 );
413 let speed = len3(curr.velocity);
414 Some(WaterEntryEvent {
415 position: curr.position,
416 velocity: curr.velocity,
417 entry_speed: speed,
418 vertical_speed: curr.velocity[1].abs(),
419 is_entry,
420 })
421}
422
423pub fn water_entry_slamming_force(
435 fluid_density: f64,
436 entry_speed: f64,
437 contact_radius: f64,
438 added_mass_coeff: f64,
439) -> f64 {
440 fluid_density
441 * PI
442 * contact_radius
443 * contact_radius
444 * entry_speed
445 * entry_speed
446 * (1.0 + added_mass_coeff)
447}
448
449pub fn sphere_water_contact_radius(sphere_radius: f64, penetration_depth: f64) -> f64 {
453 if penetration_depth <= 0.0 {
454 return 0.0;
455 }
456 (2.0 * sphere_radius * penetration_depth.min(2.0 * sphere_radius)).sqrt()
457}
458
459#[derive(Debug, Clone, Copy)]
465pub struct SplashParticle {
466 pub position: [f64; 3],
468 pub velocity: [f64; 3],
470 pub mass: f64,
472 pub lifetime: f64,
474}
475
476pub fn generate_splash_particles(
488 event: &WaterEntryEvent,
489 n_particles: usize,
490 fluid: &FluidProperties,
491 particle_mass: f64,
492 seed: u64,
493) -> Vec<SplashParticle> {
494 let mut rng = FcRng::new(seed);
495 let mut particles = Vec::with_capacity(n_particles);
496 let base_speed = event.entry_speed * 0.6;
497 let cone_half_angle = PI / 4.0; for i in 0..n_particles {
500 let azimuth = (i as f64 / n_particles as f64) * 2.0 * PI
501 + rng.next_f64() * (2.0 * PI / n_particles as f64);
502 let polar = rng.next_f64() * cone_half_angle;
503 let speed = base_speed * (0.5 + rng.next_f64() * 0.5);
504 let vx = speed * polar.sin() * azimuth.cos();
505 let vy = speed * polar.cos();
506 let vz = speed * polar.sin() * azimuth.sin();
507 let lifetime = 2.0 * speed / 9.81 + 0.1; let _surface_tension_ref = fluid.surface_tension;
510 particles.push(SplashParticle {
511 position: event.position,
512 velocity: [vx, vy, vz],
513 mass: particle_mass,
514 lifetime,
515 });
516 }
517 particles
518}
519
520pub fn integrate_splash_particle(particle: &mut SplashParticle, dt: f64, gravity: f64) {
524 particle.velocity[1] -= gravity * dt;
525 particle.position = add3(particle.position, scale3(particle.velocity, dt));
526 particle.lifetime -= dt;
527}
528
529#[derive(Debug, Clone, Copy)]
535pub struct FluidDomain {
536 pub min: [f64; 3],
538 pub max: [f64; 3],
540 pub free_surface_y: f64,
542}
543
544impl FluidDomain {
545 pub fn new(min: [f64; 3], max: [f64; 3], free_surface_y: f64) -> Self {
547 Self {
548 min,
549 max,
550 free_surface_y,
551 }
552 }
553
554 pub fn contains_fluid_point(&self, p: [f64; 3]) -> bool {
556 p[0] >= self.min[0]
557 && p[0] <= self.max[0]
558 && p[1] >= self.min[1]
559 && p[1] <= self.free_surface_y
560 && p[2] >= self.min[2]
561 && p[2] <= self.max[2]
562 }
563
564 pub fn in_bounds(&self, p: [f64; 3]) -> bool {
566 p[0] >= self.min[0]
567 && p[0] <= self.max[0]
568 && p[1] >= self.min[1]
569 && p[1] <= self.max[1]
570 && p[2] >= self.min[2]
571 && p[2] <= self.max[2]
572 }
573}
574
575pub fn ray_free_surface(
580 origin: [f64; 3],
581 dir: [f64; 3],
582 free_surface_y: f64,
583 max_t: f64,
584) -> Option<f64> {
585 if dir[1].abs() < 1e-14 {
587 return None; }
589 let t = (free_surface_y - origin[1]) / dir[1];
590 if t >= 0.0 && t <= max_t {
591 Some(t)
592 } else {
593 None
594 }
595}
596
597pub fn sphere_submersion_fraction(cy: f64, r: f64, free_surface_y: f64) -> f64 {
599 let depth = free_surface_y - cy; if depth >= r {
601 1.0
602 } else if depth <= -r {
603 0.0
604 } else {
605 let h = (r + depth).max(0.0).min(2.0 * r);
606 let vol_cap = PI * h * h * (r - h / 3.0);
608 let vol_sphere = 4.0 / 3.0 * PI * r * r * r;
609 (vol_cap / vol_sphere).clamp(0.0, 1.0)
610 }
611}
612
613pub fn handle_domain_boundary_collision(
626 body: &mut RigidBodyFluidState,
627 domain: &FluidDomain,
628 restitution: f64,
629) -> bool {
630 let mut hit = false;
631 let r = body.radius;
632 if body.position[0] - r < domain.min[0] {
634 body.position[0] = domain.min[0] + r;
635 body.velocity[0] = body.velocity[0].abs() * restitution;
636 hit = true;
637 } else if body.position[0] + r > domain.max[0] {
638 body.position[0] = domain.max[0] - r;
639 body.velocity[0] = -body.velocity[0].abs() * restitution;
640 hit = true;
641 }
642 if body.position[1] - r < domain.min[1] {
644 body.position[1] = domain.min[1] + r;
645 body.velocity[1] = body.velocity[1].abs() * restitution;
646 hit = true;
647 }
648 if body.position[2] - r < domain.min[2] {
650 body.position[2] = domain.min[2] + r;
651 body.velocity[2] = body.velocity[2].abs() * restitution;
652 hit = true;
653 } else if body.position[2] + r > domain.max[2] {
654 body.position[2] = domain.max[2] - r;
655 body.velocity[2] = -body.velocity[2].abs() * restitution;
656 hit = true;
657 }
658 hit
659}
660
661#[derive(Debug, Clone)]
667pub struct SphParticle {
668 pub position: [f64; 3],
670 pub velocity: [f64; 3],
672 pub density: f64,
674 pub pressure: f64,
676 pub mass: f64,
678}
679
680pub fn sph_cubic_kernel(r: f64, h: f64) -> f64 {
684 let sigma = 1.0 / (PI * h * h * h); let q = r / h;
686 if q < 1.0 {
687 sigma * (1.0 - 1.5 * q * q * (1.0 - 0.5 * q))
688 } else if q < 2.0 {
689 let f = 2.0 - q;
690 sigma * 0.25 * f * f * f
691 } else {
692 0.0
693 }
694}
695
696pub fn sph_cubic_kernel_gradient(r_vec: [f64; 3], h: f64) -> [f64; 3] {
700 let r = len3(r_vec);
701 if r < 1e-15 {
702 return [0.0; 3];
703 }
704 let sigma = 1.0 / (PI * h * h * h);
705 let q = r / h;
706 let dw_dr = if q < 1.0 {
707 sigma * (-3.0 * q + 2.25 * q * q) / h
708 } else if q < 2.0 {
709 let f = 2.0 - q;
710 sigma * (-0.75 * f * f) / h
711 } else {
712 0.0
713 };
714 scale3(r_vec, dw_dr / r)
715}
716
717pub fn sph_rigid_coupling_force(
727 body_center: [f64; 3],
728 body_radius: f64,
729 particles: &[SphParticle],
730 h: f64,
731) -> [f64; 3] {
732 let mut force = [0.0_f64; 3];
733 for p in particles {
734 let r_vec = sub3(body_center, p.position);
735 let r = len3(r_vec);
736 if r > h || r < body_radius * 0.5 {
737 continue; }
739 let grad_w = sph_cubic_kernel_gradient(r_vec, h);
740 if p.density > 1e-10 {
742 let coeff = p.mass * p.pressure / (p.density * p.density);
743 force = add3(force, scale3(grad_w, coeff));
744 }
745 }
746 force
747}
748
749pub fn sph_rigid_boundary_reflect(
759 particle: &mut SphParticle,
760 body: &RigidBodyFluidState,
761 stiffness: f64,
762 dt: f64,
763) -> bool {
764 let r_vec = sub3(particle.position, body.position);
765 let r = len3(r_vec);
766 if r >= body.radius {
767 return false; }
769 let penetration = body.radius - r;
770 let normal = normalize3(r_vec);
771 let dv = scale3(normal, stiffness * penetration * dt);
773 particle.velocity = add3(particle.velocity, dv);
774 particle.position = add3(body.position, scale3(normal, body.radius + 1e-6));
776 true
777}
778
779pub const D3Q19_VELOCITIES: [[i32; 3]; 19] = [
787 [0, 0, 0],
788 [1, 0, 0],
789 [-1, 0, 0],
790 [0, 1, 0],
791 [0, -1, 0],
792 [0, 0, 1],
793 [0, 0, -1],
794 [1, 1, 0],
795 [-1, -1, 0],
796 [1, -1, 0],
797 [-1, 1, 0],
798 [1, 0, 1],
799 [-1, 0, -1],
800 [1, 0, -1],
801 [-1, 0, 1],
802 [0, 1, 1],
803 [0, -1, -1],
804 [0, 1, -1],
805 [0, -1, 1],
806];
807
808pub const D3Q19_WEIGHTS: [f64; 19] = [
810 1.0 / 3.0,
811 1.0 / 18.0,
812 1.0 / 18.0,
813 1.0 / 18.0,
814 1.0 / 18.0,
815 1.0 / 18.0,
816 1.0 / 18.0,
817 1.0 / 36.0,
818 1.0 / 36.0,
819 1.0 / 36.0,
820 1.0 / 36.0,
821 1.0 / 36.0,
822 1.0 / 36.0,
823 1.0 / 36.0,
824 1.0 / 36.0,
825 1.0 / 36.0,
826 1.0 / 36.0,
827 1.0 / 36.0,
828 1.0 / 36.0,
829];
830
831pub fn lbm_equilibrium(rho: f64, u: [f64; 3], i: usize) -> f64 {
842 let e = D3Q19_VELOCITIES[i];
843 let ei = [e[0] as f64, e[1] as f64, e[2] as f64];
844 let wi = D3Q19_WEIGHTS[i];
845 let cs2 = 1.0 / 3.0;
846 let eu = dot3(ei, u);
847 let u2 = dot3(u, u);
848 wi * rho * (1.0 + eu / cs2 + eu * eu / (2.0 * cs2 * cs2) - u2 / (2.0 * cs2))
849}
850
851pub fn lbm_bounce_back_force(
858 f_in: &[f64; 19],
859 f_in_opposite: &[f64; 19],
860 body_velocity: [f64; 3],
861 rho: f64,
862) -> [f64; 3] {
863 let mut momentum = [0.0_f64; 3];
864 for i in 1..19usize {
865 let e = D3Q19_VELOCITIES[i];
866 let ei = [e[0] as f64, e[1] as f64, e[2] as f64];
867 let eu_wall = dot3(ei, body_velocity);
869 let cs2 = 1.0 / 3.0;
870 let f_bb = f_in_opposite[i] + 2.0 * D3Q19_WEIGHTS[i] * rho * eu_wall / cs2;
871 let df = f_in[i] - f_bb;
873 momentum = add3(momentum, scale3(ei, 2.0 * df));
874 }
875 momentum
876}
877
878pub fn two_way_coupling_impulse(
897 body: &mut RigidBodyFluidState,
898 fluid_mass: f64,
899 fluid_vel: [f64; 3],
900 contact_normal: [f64; 3],
901 restitution: f64,
902 _dt: f64,
903) -> [f64; 3] {
904 let v_rel = sub3(body.velocity, fluid_vel);
905 let vn = dot3(v_rel, contact_normal);
906 if vn >= 0.0 {
907 return [0.0; 3];
909 }
910 let j_mag = -(1.0 + restitution) * vn / (1.0 / body.mass + 1.0 / fluid_mass.max(1e-10));
912 let impulse = scale3(contact_normal, j_mag);
913 body.velocity = add3(body.velocity, scale3(impulse, 1.0 / body.mass));
915 impulse
916}
917
918#[derive(Debug, Clone, Copy)]
924pub struct UnderwaterFrictionResult {
925 pub friction_force: [f64; 3],
927 pub is_sliding: bool,
929}
930
931pub fn underwater_contact_friction(
945 normal_force: f64,
946 tangential_velocity: [f64; 3],
947 mu_coulomb: f64,
948 lubrication_factor: f64,
949 viscous_coeff: f64,
950) -> UnderwaterFrictionResult {
951 let vt_len = len3(tangential_velocity);
952 let mu_eff = mu_coulomb * (1.0 - lubrication_factor.clamp(0.0, 0.99));
953 let f_coulomb = mu_eff * normal_force.abs();
954 let f_viscous = viscous_coeff * vt_len;
955 let f_limit = f_coulomb.min(f_viscous + f_coulomb);
956
957 if vt_len < 1e-12 {
958 return UnderwaterFrictionResult {
959 friction_force: [0.0; 3],
960 is_sliding: false,
961 };
962 }
963 let vt_dir = normalize3(neg3(tangential_velocity));
964 let f_applied = f_limit.min(f_coulomb + f_viscous);
965 let is_sliding = f_applied >= f_coulomb;
966 UnderwaterFrictionResult {
967 friction_force: scale3(vt_dir, f_applied),
968 is_sliding,
969 }
970}
971
972#[derive(Debug, Clone)]
978pub struct FluidProjectile {
979 pub body: RigidBodyFluidState,
981 pub submerged: bool,
983 pub cavitation_number: f64,
985 pub thrust_impulse: f64,
987}
988
989impl FluidProjectile {
990 pub fn new(position: [f64; 3], velocity: [f64; 3], mass: f64, radius: f64) -> Self {
992 Self {
993 body: RigidBodyFluidState::new(position, velocity, mass, radius),
994 submerged: false,
995 cavitation_number: f64::INFINITY,
996 thrust_impulse: 0.0,
997 }
998 }
999
1000 pub fn update_cavitation_number(&mut self, p_inf: f64, p_vapour: f64, fluid_density: f64) {
1004 let v = len3(self.body.velocity);
1005 let q = 0.5 * fluid_density * v * v;
1006 if q < 1e-12 {
1007 self.cavitation_number = f64::INFINITY;
1008 } else {
1009 self.cavitation_number = (p_inf - p_vapour) / q;
1010 }
1011 }
1012
1013 pub fn is_supercavitating(&self, sigma_crit: f64) -> bool {
1015 self.cavitation_number < sigma_crit
1016 }
1017}
1018
1019pub fn projectile_thrust(
1030 mass_flow_rate: f64,
1031 exhaust_velocity: f64,
1032 pressure_diff: f64,
1033 nozzle_area: f64,
1034 thrust_direction: [f64; 3],
1035) -> [f64; 3] {
1036 let f_mag = mass_flow_rate * exhaust_velocity + pressure_diff * nozzle_area;
1037 scale3(normalize3(thrust_direction), f_mag.max(0.0))
1038}
1039
1040pub fn integrate_fluid_projectile(
1052 proj: &mut FluidProjectile,
1053 fluid: &FluidProperties,
1054 thrust: [f64; 3],
1055 gravity: f64,
1056 free_surface_y: f64,
1057 dt: f64,
1058) {
1059 let submerged_frac =
1060 sphere_submersion_fraction(proj.body.position[1], proj.body.radius, free_surface_y);
1061 proj.submerged = submerged_frac > 0.01;
1062
1063 let gravity_force = [0.0, -proj.body.mass * gravity, 0.0];
1064
1065 let buoyancy = if proj.submerged {
1066 let vol_sub = submerged_frac * (4.0 / 3.0 * PI * proj.body.radius.powi(3));
1067 buoyancy_force(fluid.density, gravity, vol_sub)
1068 } else {
1069 [0.0; 3]
1070 };
1071
1072 let fluid_vel = [0.0_f64; 3]; let drag = if proj.submerged {
1074 drag_force_sphere(&proj.body, fluid_vel, fluid)
1075 } else {
1076 let air = FluidProperties::air();
1078 drag_force_sphere(&proj.body, fluid_vel, &air)
1079 };
1080
1081 let total_force = add3(add3(add3(gravity_force, buoyancy), drag), thrust);
1082 let accel = scale3(total_force, 1.0 / proj.body.mass);
1083
1084 proj.body.velocity = add3(proj.body.velocity, scale3(accel, dt));
1085 proj.body.position = add3(proj.body.position, scale3(proj.body.velocity, dt));
1086 proj.thrust_impulse += len3(thrust) * dt;
1087}
1088
1089pub fn added_mass(fluid_density: f64, body_volume: f64, cm: f64) -> f64 {
1103 cm * fluid_density * body_volume
1104}
1105
1106pub fn effective_mass(body_mass: f64, added_mass_val: f64) -> f64 {
1108 body_mass + added_mass_val
1109}
1110
1111pub fn added_mass_force(added_mass_val: f64, body_acceleration: [f64; 3]) -> [f64; 3] {
1115 scale3(body_acceleration, -added_mass_val)
1116}
1117
1118pub fn froude_number(velocity: f64, length: f64, gravity: f64) -> f64 {
1124 velocity / (gravity * length).sqrt().max(1e-15)
1125}
1126
1127pub fn wave_drag_coefficient(fr: f64) -> f64 {
1132 if fr < 1e-6 {
1133 return 0.0;
1134 }
1135 (-1.0 / (fr * fr)).exp() / (fr * fr)
1136}
1137
1138pub fn wave_drag_force(
1148 speed: f64,
1149 waterline_length: f64,
1150 fluid: &FluidProperties,
1151 gravity: f64,
1152) -> f64 {
1153 let fr = froude_number(speed, waterline_length, gravity);
1154 let cw = wave_drag_coefficient(fr);
1155 0.5 * fluid.density * speed * speed * waterline_length * waterline_length * cw
1156}
1157
1158struct FcRng {
1163 state: u64,
1164}
1165
1166impl FcRng {
1167 fn new(seed: u64) -> Self {
1168 Self { state: seed.max(1) }
1169 }
1170
1171 fn next_u64(&mut self) -> u64 {
1172 self.state = self
1173 .state
1174 .wrapping_mul(6_364_136_223_846_793_005)
1175 .wrapping_add(1_442_695_040_888_963_407);
1176 self.state
1177 }
1178
1179 fn next_f64(&mut self) -> f64 {
1180 (self.next_u64() >> 11) as f64 * (1.0 / (1u64 << 53) as f64)
1181 }
1182}
1183
1184#[cfg(test)]
1189mod tests {
1190 use super::*;
1191
1192 #[test]
1195 fn test_add3_basic() {
1196 let a = [1.0, 2.0, 3.0];
1197 let b = [4.0, 5.0, 6.0];
1198 let c = add3(a, b);
1199 assert!((c[0] - 5.0).abs() < 1e-12);
1200 assert!((c[1] - 7.0).abs() < 1e-12);
1201 assert!((c[2] - 9.0).abs() < 1e-12);
1202 }
1203
1204 #[test]
1205 fn test_normalize3_unit() {
1206 let v = normalize3([3.0, 4.0, 0.0]);
1207 let len = len3(v);
1208 assert!((len - 1.0).abs() < 1e-12);
1209 }
1210
1211 #[test]
1212 fn test_normalize3_zero_vector() {
1213 let v = normalize3([0.0, 0.0, 0.0]);
1214 assert!((v[2] - 1.0).abs() < 1e-12); }
1216
1217 #[test]
1220 fn test_fluid_properties_water() {
1221 let w = FluidProperties::water();
1222 assert!((w.density - 998.2).abs() < 0.1);
1223 assert!(w.kinematic_viscosity > 0.0);
1224 }
1225
1226 #[test]
1227 fn test_fluid_properties_air() {
1228 let a = FluidProperties::air();
1229 assert!(a.density < 2.0);
1230 assert!(a.viscosity > 0.0);
1231 }
1232
1233 #[test]
1234 fn test_kinematic_viscosity_derived() {
1235 let f = FluidProperties::new(1000.0, 0.001, 0.07, 1500.0);
1236 assert!((f.kinematic_viscosity - 1e-6).abs() < 1e-10);
1237 }
1238
1239 #[test]
1242 fn test_reynolds_number_basic() {
1243 let re = reynolds_number(1.0, 0.01, 1e-6);
1245 assert!((re - 10_000.0).abs() < 1.0);
1246 }
1247
1248 #[test]
1249 fn test_reynolds_number_zero_velocity() {
1250 assert_eq!(reynolds_number(0.0, 1.0, 1e-6), 0.0);
1251 }
1252
1253 #[test]
1256 fn test_drag_coefficient_stokes() {
1257 let cd = drag_coefficient_sphere(0.1);
1259 assert!((cd - 240.0).abs() < 1.0);
1260 }
1261
1262 #[test]
1263 fn test_drag_coefficient_newton() {
1264 let cd = drag_coefficient_sphere(1e6);
1266 assert!((cd - 0.44).abs() < 1e-10);
1267 }
1268
1269 #[test]
1270 fn test_drag_coefficient_positive() {
1271 for re in [0.01, 1.0, 10.0, 100.0, 1000.0, 1e5] {
1272 assert!(drag_coefficient_sphere(re) > 0.0, "re={re}");
1273 }
1274 }
1275
1276 #[test]
1279 fn test_drag_force_opposing() {
1280 let body_vel = [10.0, 0.0, 0.0];
1282 let fluid_vel = [0.0, 0.0, 0.0];
1283 let fluid = FluidProperties::water();
1284 let f = drag_force(body_vel, fluid_vel, &fluid, 0.44, 0.01);
1285 assert!(f[0] < 0.0, "drag should oppose motion, f={f:?}");
1286 }
1287
1288 #[test]
1289 fn test_drag_force_zero_velocity() {
1290 let fluid = FluidProperties::water();
1291 let f = drag_force([0.0; 3], [0.0; 3], &fluid, 0.44, 0.01);
1292 assert_eq!(f, [0.0; 3]);
1293 }
1294
1295 #[test]
1298 fn test_buoyancy_fully_submerged() {
1299 let body = RigidBodyFluidState::new([0.0, -2.0, 0.0], [0.0; 3], 1.0, 0.5);
1300 let fluid = FluidProperties::water();
1301 let result = buoyancy_sphere(&body, &fluid, 0.0, 9.81);
1302 let vol = 4.0 / 3.0 * PI * 0.5_f64.powi(3);
1303 let expected = fluid.density * 9.81 * vol;
1304 assert!(
1305 (result.force[1] - expected).abs() < 1.0,
1306 "force={}",
1307 result.force[1]
1308 );
1309 assert!((result.submerged_volume - vol).abs() < 1e-6);
1310 }
1311
1312 #[test]
1313 fn test_buoyancy_above_surface() {
1314 let body = RigidBodyFluidState::new([0.0, 5.0, 0.0], [0.0; 3], 1.0, 0.5);
1315 let fluid = FluidProperties::water();
1316 let result = buoyancy_sphere(&body, &fluid, 0.0, 9.81);
1317 assert!((result.force[1]).abs() < 1e-10, "force={}", result.force[1]);
1318 assert_eq!(result.submerged_volume, 0.0);
1319 }
1320
1321 #[test]
1322 fn test_buoyancy_partial_submersion() {
1323 let r = 1.0;
1325 let body = RigidBodyFluidState::new([0.0, 0.0, 0.0], [0.0; 3], 1.0, r);
1326 let fluid = FluidProperties::water();
1327 let result = buoyancy_sphere(&body, &fluid, 0.0, 9.81);
1328 let half_vol = 2.0 / 3.0 * PI * r * r * r;
1329 assert!(
1330 (result.submerged_volume - half_vol).abs() < 0.05,
1331 "vol={} expected≈{half_vol}",
1332 result.submerged_volume
1333 );
1334 }
1335
1336 #[test]
1339 fn test_submersion_state_airborne() {
1340 let body = RigidBodyFluidState::new([0.0, 2.0, 0.0], [0.0; 3], 1.0, 0.5);
1341 assert_eq!(
1342 submersion_state_sphere(&body, 0.0),
1343 SubmersionState::Airborne
1344 );
1345 }
1346
1347 #[test]
1348 fn test_submersion_state_submerged() {
1349 let body = RigidBodyFluidState::new([0.0, -2.0, 0.0], [0.0; 3], 1.0, 0.5);
1350 assert_eq!(
1351 submersion_state_sphere(&body, 0.0),
1352 SubmersionState::Submerged
1353 );
1354 }
1355
1356 #[test]
1357 fn test_submersion_state_crossing() {
1358 let body = RigidBodyFluidState::new([0.0, 0.0, 0.0], [0.0; 3], 1.0, 0.5);
1359 assert_eq!(
1360 submersion_state_sphere(&body, 0.0),
1361 SubmersionState::Crossing
1362 );
1363 }
1364
1365 #[test]
1368 fn test_water_crossing_detection_entry() {
1369 let prev = RigidBodyFluidState::new([0.0, 1.0, 0.0], [0.0, -5.0, 0.0], 1.0, 0.3);
1370 let curr = RigidBodyFluidState::new([0.0, 0.0, 0.0], [0.0, -5.0, 0.0], 1.0, 0.3);
1371 let ev = detect_water_crossing(&prev, &curr, 0.0);
1372 assert!(ev.is_some());
1373 assert!(ev.unwrap().is_entry);
1374 }
1375
1376 #[test]
1377 fn test_water_crossing_none_when_stable() {
1378 let b1 = RigidBodyFluidState::new([0.0, -2.0, 0.0], [0.0; 3], 1.0, 0.3);
1379 let b2 = RigidBodyFluidState::new([0.0, -2.1, 0.0], [0.0; 3], 1.0, 0.3);
1380 let ev = detect_water_crossing(&b1, &b2, 0.0);
1381 assert!(ev.is_none());
1382 }
1383
1384 #[test]
1387 fn test_splash_particle_count() {
1388 let event = WaterEntryEvent {
1389 position: [0.0; 3],
1390 velocity: [0.0, -5.0, 0.0],
1391 entry_speed: 5.0,
1392 vertical_speed: 5.0,
1393 is_entry: true,
1394 };
1395 let particles = generate_splash_particles(&event, 20, &FluidProperties::water(), 1e-4, 42);
1396 assert_eq!(particles.len(), 20);
1397 }
1398
1399 #[test]
1400 fn test_splash_particle_lifetime_positive() {
1401 let event = WaterEntryEvent {
1402 position: [0.0; 3],
1403 velocity: [0.0, -3.0, 0.0],
1404 entry_speed: 3.0,
1405 vertical_speed: 3.0,
1406 is_entry: true,
1407 };
1408 let particles = generate_splash_particles(&event, 5, &FluidProperties::water(), 1e-4, 1);
1409 for p in &particles {
1410 assert!(p.lifetime > 0.0, "lifetime={}", p.lifetime);
1411 }
1412 }
1413
1414 #[test]
1417 fn test_ray_free_surface_hit() {
1418 let t = ray_free_surface([0.0, 2.0, 0.0], [0.0, -1.0, 0.0], 0.0, 10.0);
1419 assert!(t.is_some());
1420 assert!((t.unwrap() - 2.0).abs() < 1e-12);
1421 }
1422
1423 #[test]
1424 fn test_ray_free_surface_parallel_miss() {
1425 let t = ray_free_surface([0.0, 1.0, 0.0], [1.0, 0.0, 0.0], 0.0, 10.0);
1426 assert!(t.is_none());
1427 }
1428
1429 #[test]
1430 fn test_ray_free_surface_behind_miss() {
1431 let t = ray_free_surface([0.0, -1.0, 0.0], [0.0, -1.0, 0.0], 0.0, 10.0);
1432 assert!(t.is_none());
1433 }
1434
1435 #[test]
1438 fn test_submersion_fraction_full() {
1439 let frac = sphere_submersion_fraction(-2.0, 0.5, 0.0);
1440 assert!((frac - 1.0).abs() < 1e-10);
1441 }
1442
1443 #[test]
1444 fn test_submersion_fraction_zero() {
1445 let frac = sphere_submersion_fraction(2.0, 0.5, 0.0);
1446 assert!((frac).abs() < 1e-10);
1447 }
1448
1449 #[test]
1450 fn test_submersion_fraction_half() {
1451 let frac = sphere_submersion_fraction(0.0, 1.0, 0.0);
1452 assert!((frac - 0.5).abs() < 0.05, "frac={frac}");
1453 }
1454
1455 #[test]
1458 fn test_domain_boundary_collision_x() {
1459 let domain = FluidDomain::new([-5.0, -5.0, -5.0], [5.0, 5.0, 5.0], 0.0);
1460 let mut body = RigidBodyFluidState::new([4.8, 0.0, 0.0], [2.0, 0.0, 0.0], 1.0, 0.3);
1461 let hit = handle_domain_boundary_collision(&mut body, &domain, 0.8);
1462 assert!(hit);
1463 assert!(body.velocity[0] < 0.0, "vx should be reflected");
1464 }
1465
1466 #[test]
1467 fn test_domain_boundary_no_collision() {
1468 let domain = FluidDomain::new([-5.0, -5.0, -5.0], [5.0, 5.0, 5.0], 0.0);
1469 let mut body = RigidBodyFluidState::new([0.0, 0.0, 0.0], [1.0, 0.0, 0.0], 1.0, 0.3);
1470 let hit = handle_domain_boundary_collision(&mut body, &domain, 0.8);
1471 assert!(!hit);
1472 }
1473
1474 #[test]
1477 fn test_sph_cubic_kernel_positive() {
1478 assert!(sph_cubic_kernel(0.0, 1.0) > 0.0);
1479 assert!(sph_cubic_kernel(0.5, 1.0) > 0.0);
1480 }
1481
1482 #[test]
1483 fn test_sph_cubic_kernel_outside_support() {
1484 assert_eq!(sph_cubic_kernel(3.0, 1.0), 0.0);
1485 }
1486
1487 #[test]
1488 fn test_sph_rigid_coupling_zero_particles() {
1489 let f = sph_rigid_coupling_force([0.0; 3], 0.5, &[], 1.0);
1490 assert_eq!(f, [0.0; 3]);
1491 }
1492
1493 #[test]
1496 fn test_lbm_equilibrium_sum_to_rho() {
1497 let rho = 1.0;
1498 let u = [0.0_f64; 3];
1499 let sum: f64 = (0..19).map(|i| lbm_equilibrium(rho, u, i)).sum();
1500 assert!((sum - rho).abs() < 1e-10, "sum={sum}");
1501 }
1502
1503 #[test]
1504 fn test_lbm_equilibrium_rest_node() {
1505 let rho = 1.5;
1507 let u = [0.0_f64; 3];
1508 let feq = lbm_equilibrium(rho, u, 0);
1509 let expected = D3Q19_WEIGHTS[0] * rho;
1510 assert!((feq - expected).abs() < 1e-10);
1511 }
1512
1513 #[test]
1516 fn test_two_way_coupling_separating() {
1517 let mut body = RigidBodyFluidState::new([0.0; 3], [1.0, 0.0, 0.0], 1.0, 0.5);
1518 let impulse =
1519 two_way_coupling_impulse(&mut body, 100.0, [0.0; 3], [1.0, 0.0, 0.0], 0.5, 0.01);
1520 assert_eq!(impulse, [0.0; 3]);
1522 }
1523
1524 #[test]
1525 fn test_two_way_coupling_approaching() {
1526 let mut body = RigidBodyFluidState::new([0.0; 3], [-1.0, 0.0, 0.0], 1.0, 0.5);
1527 let impulse =
1528 two_way_coupling_impulse(&mut body, 100.0, [0.0; 3], [1.0, 0.0, 0.0], 0.8, 0.01);
1529 assert!(impulse[0] > 0.0, "impulse={impulse:?}");
1531 }
1532
1533 #[test]
1536 fn test_underwater_friction_no_tangential_velocity() {
1537 let result = underwater_contact_friction(100.0, [0.0; 3], 0.5, 0.3, 0.1);
1538 assert_eq!(result.friction_force, [0.0; 3]);
1539 assert!(!result.is_sliding);
1540 }
1541
1542 #[test]
1543 fn test_underwater_friction_opposing() {
1544 let result = underwater_contact_friction(100.0, [1.0, 0.0, 0.0], 0.5, 0.0, 0.0);
1545 assert!(
1547 result.friction_force[0] < 0.0,
1548 "f={:?}",
1549 result.friction_force
1550 );
1551 }
1552
1553 #[test]
1556 fn test_added_mass_sphere() {
1557 let r = 0.1;
1558 let vol = 4.0 / 3.0 * PI * r * r * r;
1559 let rho_f = 1000.0;
1560 let ma = added_mass(rho_f, vol, 0.5);
1561 let expected = 0.5 * rho_f * vol;
1562 assert!((ma - expected).abs() < 1e-12);
1563 }
1564
1565 #[test]
1566 fn test_effective_mass_gt_body_mass() {
1567 let ma = 0.3;
1568 let mb = 1.0;
1569 assert!(effective_mass(mb, ma) > mb);
1570 }
1571
1572 #[test]
1575 fn test_froude_number_known() {
1576 let fr = froude_number(3.132, 1.0, 9.81);
1578 assert!((fr - 1.0).abs() < 0.01, "fr={fr}");
1579 }
1580
1581 #[test]
1584 fn test_projectile_falls_under_gravity() {
1585 let mut proj = FluidProjectile::new([0.0, 10.0, 0.0], [0.0; 3], 1.0, 0.05);
1586 let fluid = FluidProperties::water();
1587 let y0 = proj.body.position[1];
1588 integrate_fluid_projectile(&mut proj, &fluid, [0.0; 3], 9.81, 0.0, 0.1);
1589 assert!(proj.body.position[1] < y0, "should fall under gravity");
1590 }
1591
1592 #[test]
1593 fn test_projectile_thrust_impulse_accumulates() {
1594 let mut proj = FluidProjectile::new([0.0; 3], [0.0; 3], 1.0, 0.05);
1595 let fluid = FluidProperties::water();
1596 let thrust = [10.0, 0.0, 0.0];
1597 integrate_fluid_projectile(&mut proj, &fluid, thrust, 9.81, -10.0, 0.01);
1598 assert!(proj.thrust_impulse > 0.0);
1599 }
1600
1601 #[test]
1604 fn test_slamming_force_positive() {
1605 let f = water_entry_slamming_force(1000.0, 10.0, 0.1, 1.0);
1606 assert!(f > 0.0, "slamming force={f}");
1607 }
1608
1609 #[test]
1610 fn test_slamming_force_scales_with_speed_squared() {
1611 let f1 = water_entry_slamming_force(1000.0, 5.0, 0.1, 1.0);
1612 let f2 = water_entry_slamming_force(1000.0, 10.0, 0.1, 1.0);
1613 assert!((f2 / f1 - 4.0).abs() < 1e-10, "ratio={}", f2 / f1);
1614 }
1615
1616 #[test]
1619 fn test_contact_radius_zero_penetration() {
1620 assert_eq!(sphere_water_contact_radius(1.0, 0.0), 0.0);
1621 }
1622
1623 #[test]
1624 fn test_contact_radius_increases_with_depth() {
1625 let r1 = sphere_water_contact_radius(1.0, 0.1);
1626 let r2 = sphere_water_contact_radius(1.0, 0.5);
1627 assert!(r2 > r1, "r1={r1} r2={r2}");
1628 }
1629
1630 #[test]
1633 fn test_wave_drag_force_positive() {
1634 let fluid = FluidProperties::water();
1635 let f = wave_drag_force(5.0, 10.0, &fluid, 9.81);
1636 assert!(f >= 0.0, "wave drag={f}");
1637 }
1638
1639 #[test]
1642 fn test_fluid_domain_contains_fluid_point() {
1643 let domain = FluidDomain::new([-5.0, -5.0, -5.0], [5.0, 5.0, 5.0], 0.0);
1644 assert!(domain.contains_fluid_point([1.0, -1.0, 0.0]));
1645 assert!(!domain.contains_fluid_point([1.0, 1.0, 0.0])); }
1647}