//! embedded3 S2-2a: a moving body on the device — the circle of the //! falsifier (R 0.05 across a periodic 4-cell slab on h = 1/152, at 1 m/s //! peak) on the cut wall, host vs device over 100 steps under tight //! tolerances to `1e-9·scale`; the host rebuild's share of the step time //! recorded. //! //! `RTX_CUDA_ARCH=sm_120 cargo test --release -p rtx-cfd --features cuda --test embedded3_device_moving -- --nocapture` #![cfg(feature = "cuda")] use rtx_cfd::solvers::incompressible::ConvectionScheme; use rtx_cfd::solvers::incompressible::embedded3::step::device::DeviceStep; use rtx_cfd::solvers::incompressible::embedded3::{ Body, Boundaries, Field, Fluid, Grid, Parameters, Side, Solver, WallScheme, }; const N: usize = 152; const AMP: f64 = 0.08; const U: f64 = 1.0; fn circle(moving: bool) -> Body { let yc = move |t: f64| { if moving { 0.5 + AMP * (U / AMP * t).sin() } else { 0.5 } }; let vc = move |t: f64| if moving { U * (U / AMP * t).cos() } else { 0.0 }; Body::from_sdf(move |x, y, _z, t| ((x - 0.5_f64).powi(2) + (y - yc(t)).powi(2)).sqrt() - 0.05) .with_surface_velocity(move |_, _, _, t| (0.0, vc(t), 0.0)) } fn make(tight: bool) -> (Solver, Grid) { let (tolerance, inner_stop_factor) = if tight { (1e-12, 1e-6) } else { (1e-8, 1e-2) }; let mut solver = Solver::new( Fluid { density: 1000.0, viscosity: 1.0, reference_velocity: 1.0, reference_length: 0.1, }, Parameters { corrector_steps: 2, tolerance, inner_stop_factor, convection_scheme: ConvectionScheme::Upwind, wall_scheme: WallScheme::CutCell, boundaries: Boundaries { z0: Side::Periodic, z1: Side::Periodic, ..Boundaries::default() }, ..Parameters::default() }, ); solver.set_boundary_velocity(|_, _, _, _| (0.0, 0.0, 0.0)); solver.set_moving_body(circle(true)); (solver, Grid::cubic(N, N, 4, 1.0 / N as f64)) } fn max_diff(a: &[f64], b: &[f64]) -> f64 { a.iter() .zip(b) .fold(0.0_f64, |m, (&x, &y)| m.max((x - y).abs())) } fn scale(a: &[f64]) -> f64 { a.iter().fold(0.0_f64, |m, &x| m.max(x.abs())) } fn march(tight: bool, steps: usize, bound: Option) { let dt = 3.24e-4; let (mut host, g) = make(tight); let mut fh = Field::new(g); host.initialize(&mut fh); let (mut ds, _) = make(tight); let mut fd = Field::new(g); ds.initialize(&mut fd); unsafe { std::env::set_var("RTX_PROFILE", "1") }; let mut device = DeviceStep::new(ds, g); device.upload(&fd); let start = std::time::Instant::now(); let (mut differ, mut fresh_h, mut fresh_d) = (0, 0, 0); for _ in 0..steps { let rh = host.advance(&mut fh, dt); let rd = device.advance(dt); if rh.corrector_steps_performed != rd.corrector_steps_performed { differ += 1; } fresh_h += rh.fresh_cells; fresh_d += rd.fresh_cells; } let seconds = start.elapsed().as_secs_f64(); device.download(&mut fd); let du = max_diff(&fh.u, &fd.u) .max(max_diff(&fh.v, &fd.v)) .max(max_diff(&fh.w, &fd.w)); let su = scale(&fh.u).max(scale(&fh.v)).max(scale(&fh.w)); let dp = max_diff(&fh.p, &fd.p); let sp = scale(&fh.p).max(1000.0); let t = device.timers().expect("timers"); println!( " moving circle 152²×4 CutCell (tight {tight}, hierarchy every {} steps): {steps} steps; host vs device max |Δu| {du:.3e} on {su:.3e}, max |Δp| {dp:.3e} on {sp:.3e}; fresh cells host {fresh_h} device {fresh_d}; corrector counts differ on {differ} steps; device CG {:.1}/step; {:.1} ms per step (host + device), device step {:.1} ms of which rebuild {:.1} ms", std::env::var("RTX_E3_PRECOND_REFRESH").unwrap_or_else(|_| "10".into()), t.cg_iterations as f64 / steps as f64, 1e3 * seconds / steps as f64, 1e-6 * (t.predictor_ns + t.poisson_ns + t.apply_ns + t.transfer_ns) as f64 / steps as f64, 1e-6 * t.transfer_ns as f64 / steps as f64 ); assert_eq!(fresh_h, fresh_d, "fresh-cell counts differ"); if let Some(b) = bound { assert!(du < b * su, "velocity differs: {du:.3e} on {su:.3e}"); assert!(dp < b * sp, "pressure differs: {dp:.3e} on {sp:.3e}"); } } #[test] fn moving_circle_host_equals_device() { march(false, 100, None); march(true, 100, Some(1e-9)); } /// S2-2b-ii: the hierarchy kept for ten steps (the fine operator exact /// every step) — the tight identity to the host still holds (the CG /// converges to the tolerance under any preconditioner), the CG count /// and the step time recorded against the every-step rebuild. #[test] fn stale_hierarchy_keeps_the_identity() { unsafe { std::env::set_var("RTX_E3_PRECOND_REFRESH", "1") }; march(true, 60, Some(1e-9)); unsafe { std::env::set_var("RTX_E3_PRECOND_REFRESH", "10") }; march(true, 60, Some(1e-9)); unsafe { std::env::remove_var("RTX_E3_PRECOND_REFRESH") }; }