//! S2-3 pre-flight (host): the flag + cylinder body at ny 62 — the mask //! builds, its cut geometry closes, the counts and the build time (the //! moving body's per-step host rebuild cost) are recorded. use rtx_cfd::solvers::incompressible::embedded3::{Body, Boundaries, Grid, Mask, Side}; const H: f64 = 0.41; const FLAG_X0: f64 = 0.6; const FLAG_LEN: f64 = 0.35; const FLAG_HALF: f64 = 0.01; const FLAG_SPAN: f64 = 0.2; const AMP: f64 = 0.084; const BETA_L: f64 = 1.875_104_069; fn mode(s: f64) -> f64 { let b = BETA_L; let sigma = (b.sinh() - b.sin()) / (b.cosh() + b.cos()); let phi = |s: f64| (b * s).cosh() - (b * s).cos() - sigma * ((b * s).sinh() - (b * s).sin()); phi(s) / phi(1.0) } fn flag_2d(x: f64, y: f64, phase: f64) -> f64 { let n = 40; let mut best = f64::INFINITY; let point = |m: usize| { let s = m as f64 / n as f64; (FLAG_X0 + s * FLAG_LEN, 0.2 + AMP * mode(s) * phase) }; for m in 0..n { let (ax, ay) = point(m); let (bx, by) = point(m + 1); let (ex, ey) = (bx - ax, by - ay); let u = (((x - ax) * ex + (y - ay) * ey) / (ex * ex + ey * ey)).clamp(0.0, 1.0); let d = ((x - ax - u * ex).powi(2) + (y - ay - u * ey).powi(2)).sqrt(); best = best.min(d); } best - FLAG_HALF } fn flag_3d(x: f64, y: f64, z: f64, phase: f64, r: f64) -> f64 { let d2 = flag_2d(x, y, phase); let q1 = d2 + r; let q2 = (z - 0.5 * H).abs() - 0.5 * FLAG_SPAN + r; (q1.max(0.0).powi(2) + q2.max(0.0).powi(2)).sqrt() + q1.max(q2).min(0.0) - r } #[test] fn flag_body_builds_at_ny_62() { let ny = 62; let h = H / ny as f64; let nx = (2.5 / h).round() as usize; let g = Grid::cubic(nx, ny, ny, h); let cyl = |x: f64, y: f64| ((x - 0.2_f64).powi(2) + (y - 0.2_f64).powi(2)).sqrt() - 0.05; for phase in [0.0, 1.0] { let body = Body::from_sdf(move |x, y, z, _t| cyl(x, y).min(flag_3d(x, y, z, phase, h))); let b = Boundaries { x1: Side::PressureOutlet, ..Boundaries::default() }; let start = std::time::Instant::now(); let mask = Mask::build_cut(&body, g, 0.0, b).expect("mask"); let build = start.elapsed().as_secs_f64(); let cut = mask.cut().unwrap(); let (area, closure) = cut.wall_area_and_closure(); let solid = g.cells() - mask.fluid_cells(); println!( " phase {phase}: {} cells, {} fluid, {solid} solid, {} merged; wall area {area:.4} m² (cylinder 0.129 + flag ~0.156), closure {:.2e}; build {build:.2} s", g.cells(), mask.fluid_cells(), mask.merged_cells(), (closure[0].powi(2) + closure[1].powi(2) + closure[2].powi(2)).sqrt() ); assert!(solid > 1000 && mask.fluid_cells() > g.cells() / 2); assert!((closure[0].powi(2) + closure[1].powi(2) + closure[2].powi(2)).sqrt() < 1e-9); } } /// Where the per-step host rebuild's time goes at ny 62 (S2-2b's target). #[test] #[ignore = "profile: the rebuild's parts at ny 62 (seconds)"] fn rebuild_profile_at_ny_62() { use rtx_cfd::solvers::incompressible::embedded3::CutGeometry; let ny = 62; let h = H / ny as f64; let nx = (2.5 / h).round() as usize; let g = Grid::cubic(nx, ny, ny, h); let cyl = |x: f64, y: f64| ((x - 0.2_f64).powi(2) + (y - 0.2_f64).powi(2)).sqrt() - 0.05; let body = Body::from_sdf(move |x, y, z, _t| cyl(x, y).min(flag_3d(x, y, z, 0.3, h))); let b = Boundaries { x1: Side::PressureOutlet, ..Boundaries::default() }; let t0 = std::time::Instant::now(); let _cut = CutGeometry::build(&body, g, 0.0); let t_cut = t0.elapsed().as_secs_f64(); let t1 = std::time::Instant::now(); let nodes = (nx + 1) * (ny + 1) * (ny + 1); let mut acc = 0.0; for n in 0..nodes { let (k, j, i) = ( n / ((nx + 1) * (ny + 1)), (n / (nx + 1)) % (ny + 1), n % (nx + 1), ); acc += body.phi(i as f64 * h, j as f64 * h, k as f64 * h, 0.0); } let t_phi = t1.elapsed().as_secs_f64(); let t2 = std::time::Instant::now(); let mask = Mask::build_cut(&body, g, 0.0, b).expect("mask"); let t_mask = t2.elapsed().as_secs_f64(); let t3 = std::time::Instant::now(); let (_table, _) = mask.wall_flux_table(&body, 0.0); let t_flux = t3.elapsed().as_secs_f64(); println!( " ny 62 rebuild parts: cut geometry {t_cut:.2} s (of which φ at {nodes} nodes {t_phi:.2} s), whole mask build {t_mask:.2} s, wall-flux table {t_flux:.2} s (Σφ {acc:.1})" ); } /// The narrow band is invisible to the solution: the moving circle /// marched with and without `max_surface_speed` gives bit-identical /// fields; the flag body's rebuild time at ny 62 with the band recorded. #[test] fn narrow_band_is_bit_identical_and_fast() { use rtx_cfd::solvers::incompressible::ConvectionScheme; use rtx_cfd::solvers::incompressible::embedded3::{ CutGeometry, Field, Fluid, Parameters, Solver, WallScheme, }; let n = 76; let h = 1.0 / n as f64; let dt = 3.24e-4; let run = |band: bool| { 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: 1e-8, convection_scheme: ConvectionScheme::Upwind, wall_scheme: WallScheme::CutCell, boundaries: Boundaries { z0: Side::Periodic, z1: Side::Periodic, ..Boundaries::default() }, max_surface_speed: band.then_some(1.0), ..Parameters::default() }, ); solver.set_boundary_velocity(|_, _, _, _| (0.0, 0.0, 0.0)); let yc = |t: f64| 0.5 + 0.08 * (t / 0.08).sin(); let vc = |t: f64| (t / 0.08).cos(); solver.set_moving_body( 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)), ); let g = Grid::cubic(n, n, 4, h); let mut f = Field::new(g); solver.initialize(&mut f); let start = std::time::Instant::now(); for _ in 0..40 { solver.advance(&mut f, dt); } (f, start.elapsed().as_secs_f64()) }; let (full, t_full) = run(false); let (band, t_band) = run(true); let same = full.u == band.u && full.v == band.v && full.w == band.w && full.p == band.p; println!( " moving circle 76²×4, 40 steps: full {t_full:.2} s, band {t_band:.2} s; fields bit-identical: {same}" ); assert!(same, "the narrow band changed the solution"); // The flag body at ny 62: a full build, then a banded rebuild after a small motion. let ny = 62; let hh = H / ny as f64; let nx = (2.5 / hh).round() as usize; let g = Grid::cubic(nx, ny, ny, hh); let cyl = |x: f64, y: f64| ((x - 0.2_f64).powi(2) + (y - 0.2_f64).powi(2)).sqrt() - 0.05; let body = Body::from_sdf(move |x, y, z, t| cyl(x, y).min(flag_3d(x, y, z, t, hh))); let t0 = std::time::Instant::now(); let prev = CutGeometry::build(&body, g, 0.3); let t_prev = t0.elapsed().as_secs_f64(); let t1 = std::time::Instant::now(); let next = CutGeometry::build_from(&body, g, 0.31, Some((&prev, 3.0 * hh, 1.0e-3))); let t_next = t1.elapsed().as_secs_f64(); let exact = CutGeometry::build(&body, g, 0.31); let vol_same = next.vol == exact.vol && next.a_u == exact.a_u && next.a_v == exact.a_v && next.a_w == exact.a_w; println!( " flag ny 62: full build {t_prev:.2} s, banded rebuild {t_next:.2} s; volumes and apertures identical to a full build: {vol_same}" ); assert!(vol_same); } /// The per-step operator cost at ny 62 (host side of the device CG's /// rebuild: the problem, the fine level, the components, the hierarchy /// export) — S2-2b-ii's target. #[test] #[ignore = "profile: the operator export at ny 62 (seconds)"] fn operator_export_profile_at_ny_62() { use rtx_cfd::solvers::incompressible::embedded3::poisson::export::export_hierarchy; use rtx_cfd::solvers::incompressible::embedded3::{ Field, Fluid, Parameters, Solver, WallScheme, }; use rtx_cfd::solvers::incompressible::{ConvectionScheme, MultigridParameters}; let ny = 62; let h = H / ny as f64; let nx = (2.5 / h).round() as usize; let g = Grid::cubic(nx, ny, ny, h); let cyl = |x: f64, y: f64| ((x - 0.2_f64).powi(2) + (y - 0.2_f64).powi(2)).sqrt() - 0.05; let body = Body::from_sdf(move |x, y, z, _t| cyl(x, y).min(flag_3d(x, y, z, 0.3, h))); let mut solver = Solver::new( Fluid { density: 1000.0, viscosity: 1.0, reference_velocity: 1.0, reference_length: 0.1, }, Parameters { wall_scheme: WallScheme::CutCell, convection_scheme: ConvectionScheme::TvdVanAlbada, boundaries: Boundaries { x1: Side::PressureOutlet, ..Boundaries::default() }, ..Parameters::default() }, ); solver.set_body(body); let mut f = Field::new(g); let t0 = std::time::Instant::now(); solver.initialize(&mut f); let t_init = t0.elapsed().as_secs_f64(); let t1 = std::time::Instant::now(); let problem = solver.poisson_operator(g, 1e-3); let t_op = t1.elapsed().as_secs_f64(); let t2 = std::time::Instant::now(); let levels = export_hierarchy(&problem, &MultigridParameters::default()); let t_exp = t2.elapsed().as_secs_f64(); println!( " ny 62 operator: initialize (mask + impose) {t_init:.2} s, poisson_operator {t_op:.2} s, export_hierarchy {t_exp:.2} s ({} levels, {} links)", levels.len(), problem.links.len() ); } /// The residual clause's remedy, measured on the moving circle (host): /// the worst mass residual over 100 steps with two correctors at the /// 1e-2 inner stop against three correctors at 1e-3. #[test] #[ignore = "residual study on the moving circle (host, a minute)"] fn moving_circle_residual_remedy() { use rtx_cfd::solvers::incompressible::ConvectionScheme; use rtx_cfd::solvers::incompressible::embedded3::{ Field, Fluid, Parameters, Solver, WallScheme, }; let n = 152; let h = 1.0 / n as f64; let dt = 3.24e-4; for (correctors, inner) in [(2usize, 1e-2), (3, 1e-3), (4, 1e-4)] { let mut solver = Solver::new( Fluid { density: 1000.0, viscosity: 1.0, reference_velocity: 1.0, reference_length: 0.1, }, Parameters { corrector_steps: correctors, tolerance: 1e-8, inner_stop_factor: inner, convection_scheme: ConvectionScheme::Upwind, wall_scheme: WallScheme::CutCell, boundaries: Boundaries { z0: Side::Periodic, z1: Side::Periodic, ..Boundaries::default() }, max_surface_speed: Some(1.0), ..Parameters::default() }, ); solver.set_boundary_velocity(|_, _, _, _| (0.0, 0.0, 0.0)); let yc = |t: f64| 0.5 + 0.08 * (t / 0.08).sin(); let vc = |t: f64| (t / 0.08).cos(); solver.set_moving_body( 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)), ); let g = Grid::cubic(n, n, 4, h); let mut f = Field::new(g); solver.initialize(&mut f); let (mut worst, mut sum_cg, mut sum_corr) = (0.0_f64, 0usize, 0usize); let start = std::time::Instant::now(); for _ in 0..100 { let r = solver.advance(&mut f, dt); worst = worst.max(r.final_residual); sum_cg += r.poisson_iterations; sum_corr += r.corrector_steps_performed; } println!( " correctors {correctors} inner {inner:.0e}: worst residual {worst:.2e}, CG {:.1}/step, correctors {:.2}/step, {:.2} s", sum_cg as f64 / 100.0, sum_corr as f64 / 100.0, start.elapsed().as_secs_f64() ); } }