//! Tests for D3Q19 Lattice Boltzmann Method implementation use approx::assert_relative_eq; use nalgebra::Vector3; use rtx_cfd::solvers::lbm::{D3Q19Parameters, D3Q19Solver}; #[test] fn test_d3q19_lattice_velocities() { let solver = D3Q19Solver::new(10, 10, 10, D3Q19Parameters::default()); let velocities = solver.lattice_velocities(); // D3Q19 should have 19 velocities assert_eq!(velocities.len(), 19); // Check specific velocity directions assert_eq!(velocities[0], Vector3::new(0, 0, 0)); // Rest assert_eq!(velocities[1], Vector3::new(1, 0, 0)); // +X assert_eq!(velocities[2], Vector3::new(-1, 0, 0)); // -X assert_eq!(velocities[3], Vector3::new(0, 1, 0)); // +Y assert_eq!(velocities[4], Vector3::new(0, -1, 0)); // -Y assert_eq!(velocities[5], Vector3::new(0, 0, 1)); // +Z assert_eq!(velocities[6], Vector3::new(0, 0, -1)); // -Z } #[test] fn test_d3q19_weights() { let solver = D3Q19Solver::new(10, 10, 10, D3Q19Parameters::default()); let weights = solver.weights(); // D3Q19 should have 19 weights assert_eq!(weights.len(), 19); // Check weight values assert_relative_eq!(weights[0], 1.0 / 3.0, epsilon = 1e-12); // Rest particle assert_relative_eq!(weights[1], 1.0 / 18.0, epsilon = 1e-12); // Face neighbors assert_relative_eq!(weights[7], 1.0 / 36.0, epsilon = 1e-12); // Edge neighbors // Weights should sum to 1 let sum: f64 = weights.iter().sum(); assert_relative_eq!(sum, 1.0, epsilon = 1e-12); } #[test] fn test_d3q19_equilibrium_distribution() { let solver = D3Q19Solver::new(10, 10, 10, D3Q19Parameters::default()); let density = 1.0; let velocity = Vector3::new(0.1, 0.05, 0.02); let f_eq = solver.equilibrium_distribution(density, &velocity); // Should have 19 components assert_eq!(f_eq.len(), 19); // All components should be positive for &val in &f_eq { assert!( val > 0.0, "Equilibrium distribution component should be positive" ); } // Sum should equal density let sum: f64 = f_eq.iter().sum(); assert_relative_eq!(sum, density, epsilon = 1e-12); } #[test] fn test_d3q19_equilibrium_at_rest() { let solver = D3Q19Solver::new(10, 10, 10, D3Q19Parameters::default()); let density = 1.0; let velocity = Vector3::zeros(); let f_eq = solver.equilibrium_distribution(density, &velocity); let weights = solver.weights(); // For zero velocity, equilibrium should be density * weight for i in 0..19 { assert_relative_eq!(f_eq[i], density * weights[i], epsilon = 1e-12); } } #[test] fn test_d3q19_macroscopic_variables() { let mut solver = D3Q19Solver::new(10, 10, 10, D3Q19Parameters::default()); // Initialize with known state let density = 1.2; let velocity = Vector3::new(0.1, -0.05, 0.03); // Set distribution functions to equilibrium let f_eq = solver.equilibrium_distribution(density, &velocity); solver.set_distribution_at(5, 5, 5, &f_eq); // Extract macroscopic variables let macro_vars = solver.macroscopic_variables_at(5, 5, 5); assert_relative_eq!(macro_vars.density, density, epsilon = 1e-12); assert_relative_eq!(macro_vars.velocity.x, velocity.x, epsilon = 1e-12); assert_relative_eq!(macro_vars.velocity.y, velocity.y, epsilon = 1e-12); assert_relative_eq!(macro_vars.velocity.z, velocity.z, epsilon = 1e-12); } #[test] fn test_d3q19_bgk_collision() { let mut solver = D3Q19Solver::new(10, 10, 10, D3Q19Parameters::new(0.6)); // Initialize all cells with equilibrium let density = 1.0; let velocity = Vector3::new(0.1, 0.0, 0.0); solver.initialize_uniform(density, velocity); // Get original equilibrium distribution let f_eq = solver.equilibrium_distribution(density, &velocity); // Perturb from equilibrium by reducing a component let mut f = f_eq.clone(); f[1] *= 0.8; // Reduce from equilibrium (80% of equilibrium value) solver.set_distribution_at(5, 5, 5, &f); let initial_value = f[1]; // Apply collision step solver.collision_step(); // Check that distribution moves toward equilibrium let f_after = solver.distribution_at(5, 5, 5); // With BGK collision: f_new = f - omega * (f - f_eq) // Since f[1] < f_eq[1], (f - f_eq) is negative, so f_new should increase assert!( f_after[1] > initial_value, "BGK should move towards equilibrium: {} > {}", f_after[1], initial_value ); // Check that it's moving in the right direction (don't check exact value due to neighboring cell effects) let direction_to_equilibrium = f_eq[1] - initial_value; let actual_change = f_after[1] - initial_value; assert!( direction_to_equilibrium > 0.0 && actual_change > 0.0, "Should move towards equilibrium" ); } #[test] fn test_d3q19_streaming_step() { let mut solver = D3Q19Solver::new(5, 5, 5, D3Q19Parameters::default()); // Initialize central cell with specific distribution let mut f = vec![0.0; 19]; f[1] = 1.0; // Only +X component solver.set_distribution_at(2, 2, 2, &f); // Apply streaming step solver.streaming_step(); // Check that the distribution has moved in +X direction let f_east = solver.distribution_at(3, 2, 2); assert_relative_eq!(f_east[1], 1.0, epsilon = 1e-12); // Original cell should have zero +X component let f_original = solver.distribution_at(2, 2, 2); assert_relative_eq!(f_original[1], 0.0, epsilon = 1e-12); } #[test] fn test_d3q19_mass_conservation() { let nx = 8; let ny = 8; let nz = 8; let mut solver = D3Q19Solver::new(nx, ny, nz, D3Q19Parameters::default()); // Initialize with uniform density solver.initialize_uniform(1.0, Vector3::new(0.05, 0.02, 0.01)); let initial_mass = solver.total_mass(); // Run simulation for several steps with periodic boundaries for _ in 0..50 { solver.step_periodic(); } let final_mass = solver.total_mass(); // Mass should be conserved assert_relative_eq!(final_mass, initial_mass, epsilon = 1e-12); } #[test] fn test_d3q19_parameters_validation() { // Valid parameters let valid_params = D3Q19Parameters::new(0.6); assert!(valid_params.validate().is_ok()); // Invalid relaxation time (too small) let invalid_params = D3Q19Parameters::new(0.4); assert!(invalid_params.validate().is_err()); // Invalid relaxation time (too large) let invalid_params = D3Q19Parameters::new(2.1); assert!(invalid_params.validate().is_err()); }