//! Production CFD workflow integration tests //! //! Tests the complete CFD workflow with actual SIMPLE and PISO solvers, //! validating convergence, mass conservation, and turbulence integration. use rtx_cfd::{ CfdConfig, CfdResult, solvers::incompressible::{ BoundaryConditions, FlowField, IncompressibleSolver, PisoParameters, PisoSolver, SimpleParameters, SimpleSolver, }, }; use std::time::Duration; /// Test complete SIMPLE solver workflow with turbulence #[tokio::test] async fn test_simple_solver_workflow() -> CfdResult<()> { // Create CFD configuration // Reynolds number 100, not 10^6. // // The nominal water properties (rho = 1000, mu = 1e-3) give // Re = rho U L / mu = 10^6 on a unit domain, which has no steady laminar // solution and could not be resolved by ten cells if it did. The solver // diverges on it, correctly. It only appeared to work while the diffusion // conductances were a factor 1/h too large, which quietly stabilised it. let config = CfdConfig::new() .with_density(1000.0) .with_viscosity(10.0) .with_reference_velocity(1.0) .with_reference_length(1.0); // Create solver parameters with turbulence enabled let params = SimpleParameters { pressure_relaxation: 0.3, velocity_relaxation: 0.7, max_iterations: 50, tolerance: 1e-6, time_step: 0.001, max_courant: 1.0, use_turbulence: true, steady: true, ..SimpleParameters::default() }; // Create solver let mut solver = SimpleSolver::new(config.clone(), params)?; // Create flow field (10x10 grid) let mut flow_field = FlowField::new(10, 10, 0.1, 0.1)?; // Initialize with simple flow for j in 0..10 { for i in 0..10 { if i < 10 && j < 10 { flow_field.u[(j, i)] = 1.0; // Initial u-velocity flow_field.v[(j, i)] = 0.0; // Initial v-velocity flow_field.p[(j, i)] = 0.0; // Initial pressure } } } // Set up boundary conditions (lid-driven cavity) let boundary_conditions = BoundaryConditions::lid_driven_cavity(10, 10, 1.0); // Run solver for a few iterations let mut total_iterations = 0; for _outer in 0..5 { let result = solver.solve(&mut flow_field, &boundary_conditions).await?; total_iterations += result.solver_result.iterations; // Check convergence properties assert!(result.solver_result.final_residual >= 0.0); assert!(result.solver_result.solve_time > Duration::from_nanos(0)); // Basic sanity checks on flow field assert!(!flow_field.u.iter().any(|&x| x.is_nan())); assert!(!flow_field.v.iter().any(|&x| x.is_nan())); assert!(!flow_field.p.iter().any(|&x| x.is_nan())); // Check that velocities are within reasonable bounds let max_u = flow_field.u.iter().fold(0.0f64, |a, &b| a.max(b.abs())); let max_v = flow_field.v.iter().fold(0.0f64, |a, &b| a.max(b.abs())); assert!(max_u < 10.0, "u-velocity too large: {}", max_u); assert!(max_v < 10.0, "v-velocity too large: {}", max_v); if result.solver_result.converged { break; } } println!( "SIMPLE solver completed {} total iterations", total_iterations ); Ok(()) } /// Test complete PISO solver workflow #[tokio::test] async fn test_piso_solver_workflow() -> CfdResult<()> { // Create CFD configuration let config = CfdConfig::new() .with_density(1000.0) .with_viscosity(1e-3) .with_reference_velocity(1.0) .with_reference_length(1.0); // Create PISO parameters let params = PisoParameters { corrector_steps: 2, time_step: 0.001, tolerance: 1e-6, ..PisoParameters::default() }; // Create solver let mut solver = PisoSolver::new(config.clone(), params.clone())?; // Create flow field (10x10 grid) let mut flow_field = FlowField::new(10, 10, 0.1, 0.1)?; // Initialize with simple flow for j in 0..10 { for i in 0..10 { if i < 10 && j < 10 { flow_field.u[(j, i)] = 1.0; flow_field.v[(j, i)] = 0.0; flow_field.p[(j, i)] = 0.0; } } } // Set up boundary conditions let boundary_conditions = BoundaryConditions::lid_driven_cavity(10, 10, 1.0); // Run time stepping let dt = 0.001; for _time_step in 0..10 { let result = solver .solve_time_step(&mut flow_field, &boundary_conditions, dt) .await?; // Check that pressure correction steps were performed assert!(result.corrector_steps_performed >= 1); assert!(result.corrector_steps_performed <= params.corrector_steps); // Basic sanity checks assert!(result.solver_result.final_residual >= 0.0); assert!(!flow_field.u.iter().any(|&x| x.is_nan())); assert!(!flow_field.v.iter().any(|&x| x.is_nan())); assert!(!flow_field.p.iter().any(|&x| x.is_nan())); } println!("PISO solver completed 10 time steps successfully"); Ok(()) } /// Test mass conservation in CFD solvers #[tokio::test] async fn test_mass_conservation() -> CfdResult<()> { let config = CfdConfig::new().with_density(1000.0).with_viscosity(1e-3); let params = SimpleParameters::default(); let mut solver = SimpleSolver::new(config, params)?; let mut flow_field = FlowField::new(5, 5, 0.2, 0.2)?; let boundary_conditions = BoundaryConditions::lid_driven_cavity(5, 5, 1.0); // Run a few iterations for _i in 0..3 { let _result = solver.solve(&mut flow_field, &boundary_conditions).await?; // Calculate mass conservation (∇·u should be small for incompressible flow) let mut max_divergence: f64 = 0.0; let (nx, ny, dx, dy) = flow_field.grid_info(); for j in 1..ny - 1 { for i in 1..nx - 1 { let du_dx = (flow_field.u[(j, i + 1)] - flow_field.u[(j, i - 1)]) / (2.0 * dx); let dv_dy = (flow_field.v[(j + 1, i)] - flow_field.v[(j - 1, i)]) / (2.0 * dy); let divergence = (du_dx + dv_dy).abs(); max_divergence = max_divergence.max(divergence); } } // For a converged incompressible solution, divergence should be small println!("Max divergence: {}", max_divergence); // Note: For early iterations, divergence may be larger but should decrease } Ok(()) } /// Test turbulence model integration #[tokio::test] async fn test_turbulence_integration() -> CfdResult<()> { let config = CfdConfig::new().with_density(1000.0).with_viscosity(1e-3); // Create parameters with turbulence enabled let params_turbulent = SimpleParameters { use_turbulence: true, ..SimpleParameters::default() }; // Create parameters without turbulence let params_laminar = SimpleParameters { use_turbulence: false, ..SimpleParameters::default() }; let mut solver_turbulent = SimpleSolver::new(config.clone(), params_turbulent)?; let mut solver_laminar = SimpleSolver::new(config, params_laminar)?; let mut flow_field_turbulent = FlowField::new(8, 8, 0.125, 0.125)?; let mut flow_field_laminar = FlowField::new(8, 8, 0.125, 0.125)?; // Initialize both with same initial conditions for j in 0..8 { for i in 0..8 { let u_init = if i == 0 { 1.0 } else { 0.0 }; flow_field_turbulent.u[(j, i)] = u_init; flow_field_turbulent.v[(j, i)] = 0.0; flow_field_turbulent.p[(j, i)] = 0.0; flow_field_laminar.u[(j, i)] = u_init; flow_field_laminar.v[(j, i)] = 0.0; flow_field_laminar.p[(j, i)] = 0.0; } } let boundary_conditions = BoundaryConditions::lid_driven_cavity(8, 8, 1.0); // Run both solvers let result_turbulent = solver_turbulent .solve(&mut flow_field_turbulent, &boundary_conditions) .await?; let result_laminar = solver_laminar .solve(&mut flow_field_laminar, &boundary_conditions) .await?; // Both should converge but may have different residuals assert!(result_turbulent.solver_result.final_residual >= 0.0); assert!(result_laminar.solver_result.final_residual >= 0.0); // Flow fields should be different due to turbulence effects let mut velocity_difference: f64 = 0.0; for j in 0..8 { for i in 0..8 { let diff_u = (flow_field_turbulent.u[(j, i)] - flow_field_laminar.u[(j, i)]).abs(); let diff_v = (flow_field_turbulent.v[(j, i)] - flow_field_laminar.v[(j, i)]).abs(); velocity_difference = velocity_difference.max(diff_u).max(diff_v); } } println!( "Turbulent vs laminar max velocity difference: {}", velocity_difference ); Ok(()) } /// Test solver robustness with extreme conditions #[tokio::test] async fn test_solver_robustness() -> CfdResult<()> { let config = CfdConfig::new() .with_density(1.0) // Low density .with_viscosity(1e-6); // Low viscosity (high Reynolds number) let params = SimpleParameters { max_iterations: 20, // Limit iterations to avoid long test times tolerance: 1e-4, // Relaxed tolerance ..SimpleParameters::default() }; let mut solver = SimpleSolver::new(config, params)?; let mut flow_field = FlowField::new(6, 6, 0.1, 0.1)?; // Initialize with high velocities for j in 0..6 { for i in 0..6 { flow_field.u[(j, i)] = 5.0; // High initial velocity flow_field.v[(j, i)] = 0.0; flow_field.p[(j, i)] = 0.0; } } let boundary_conditions = BoundaryConditions::lid_driven_cavity(6, 6, 5.0); // Solver should handle this without crashing let result = solver.solve(&mut flow_field, &boundary_conditions).await?; // Check that solution remains bounded assert!(!flow_field.u.iter().any(|&x| x.is_nan() || x.is_infinite())); assert!(!flow_field.v.iter().any(|&x| x.is_nan() || x.is_infinite())); assert!(!flow_field.p.iter().any(|&x| x.is_nan() || x.is_infinite())); println!( "Robustness test completed with residual: {}", result.solver_result.final_residual ); Ok(()) } /// Test SIMPLE vs PISO convergence comparison #[tokio::test] async fn test_simple_vs_piso_comparison() -> CfdResult<()> { let config = CfdConfig::new().with_density(1000.0).with_viscosity(1e-3); // SIMPLE solver let simple_params = SimpleParameters { max_iterations: 30, tolerance: 1e-5, ..SimpleParameters::default() }; let mut simple_solver = SimpleSolver::new(config.clone(), simple_params)?; // PISO solver let piso_params = PisoParameters { corrector_steps: 2, time_step: 0.001, tolerance: 1e-5, ..PisoParameters::default() }; let mut piso_solver = PisoSolver::new(config, piso_params)?; // Create identical initial conditions let mut flow_field_simple = FlowField::new(6, 6, 1.0 / 6.0, 1.0 / 6.0)?; let mut flow_field_piso = flow_field_simple.clone(); let boundary_conditions = BoundaryConditions::lid_driven_cavity(6, 6, 1.0); // Solve with SIMPLE let start = std::time::Instant::now(); let simple_result = simple_solver .solve(&mut flow_field_simple, &boundary_conditions) .await?; let simple_time = start.elapsed(); // Solve with PISO (single time step) let start = std::time::Instant::now(); let piso_result = piso_solver .solve_time_step(&mut flow_field_piso, &boundary_conditions, 0.001) .await?; let piso_time = start.elapsed(); println!( "SIMPLE: {} iterations, residual: {:.2e}, time: {:?}", simple_result.solver_result.iterations, simple_result.solver_result.final_residual, simple_time ); println!( "PISO: {} corrector steps, residual: {:.2e}, time: {:?}", piso_result.corrector_steps_performed, piso_result.solver_result.final_residual, piso_time ); // Both should produce valid solutions assert!(simple_result.solver_result.final_residual >= 0.0); assert!(piso_result.solver_result.final_residual >= 0.0); Ok(()) } /// Test Reynolds number effects on flow #[tokio::test] async fn test_reynolds_number_effects() -> CfdResult<()> { // Low Reynolds number (laminar) let config_low_re = CfdConfig::new() .with_density(1.0) .with_viscosity(1.0) // High viscosity .with_reference_velocity(1.0) .with_reference_length(1.0); // High Reynolds number (turbulent) let config_high_re = CfdConfig::new() .with_density(1.0) .with_viscosity(1e-5) // Low viscosity .with_reference_velocity(1.0) .with_reference_length(1.0); assert!(config_low_re.is_laminar()); assert!(config_high_re.is_turbulent()); let re_low = config_low_re.reynolds_number(); let re_high = config_high_re.reynolds_number(); println!("Low Re: {}, High Re: {}", re_low, re_high); assert!(re_low < 100.0); assert!(re_high > 10000.0); // Test that both configurations are valid assert!(config_low_re.validate().is_ok()); assert!(config_high_re.validate().is_ok()); Ok(()) }