//! R7 phase 1: the composite Poisson problem with one nested ratio-2 patch //! (`embedded3::composite`, a host prototype nothing else calls). //! //! - P1 (`composite_mms_ladder`, ignored): manufactured solution on the unit //! cube, patch = the middle half, n = 16/32/64 coarse; errors split into //! the interface cells (owners of a coarse–fine face), the rest of the //! patch, and the rest of the coarse grid, for the three interface fluxes //! and the uniform coarse / fine grids; //! - P2 (`composite_cut_sphere`, ignored): a Neumann sphere inside the patch //! cut by embedded3's `CutGeometry` on the fine level, against the //! uniformly fine grid with the same cut; //! - P3 (`composite_cost`, ignored): unknowns and wall time against the //! uniformly fine grid solved by embedded3's production PCG. //! //! Output CSVs go to `$R7_OUT` when set. The default (non-ignored) test is a //! seconds-scale smoke of the assembly and the solve. use rtx_cfd::solvers::incompressible::MultigridParameters; use rtx_cfd::solvers::incompressible::embedded3::Grid; use rtx_cfd::solvers::incompressible::embedded3::composite::{ Composite, CompositeSpec, Interface, Sdf, solve_bicgstab, uniform_problem, }; use rtx_cfd::solvers::incompressible::embedded3::poisson::solve_pcg; use std::f64::consts::PI; use std::io::Write; use std::sync::Arc; fn mms_u(x: f64, y: f64, z: f64) -> f64 { (1.3 * PI * x + 0.2).cos() * (1.1 * PI * y + 0.4).cos() * (0.9 * PI * z + 0.6).cos() } fn mms_f(x: f64, y: f64, z: f64) -> f64 { PI * PI * (1.69 + 1.21 + 0.81) * mms_u(x, y, z) } /// A localized feature inside the patch on top of the smooth field: the /// case local refinement is for (a Gaussian of width 0.06 at the centre). const BUMP_S: f64 = 0.06; fn bump_u(x: f64, y: f64, z: f64) -> f64 { let r2 = (x - 0.5).powi(2) + (y - 0.5).powi(2) + (z - 0.5).powi(2); mms_u(x, y, z) + (-r2 / (BUMP_S * BUMP_S)).exp() } fn bump_f(x: f64, y: f64, z: f64) -> f64 { let r2 = (x - 0.5).powi(2) + (y - 0.5).powi(2) + (z - 0.5).powi(2); let s2 = BUMP_S * BUMP_S; mms_f(x, y, z) - (4.0 * r2 / (s2 * s2) - 6.0 / s2) * (-r2 / s2).exp() } const SPH_C: [f64; 3] = [0.52, 0.49, 0.51]; const SPH_R: f64 = 0.12; const SPH_A: f64 = 2.0 * PI; fn sph_r(x: f64, y: f64, z: f64) -> f64 { ((x - SPH_C[0]).powi(2) + (y - SPH_C[1]).powi(2) + (z - SPH_C[2]).powi(2)).sqrt() } /// `cos(a (r − R))`: zero normal derivative on the sphere. fn sph_u(x: f64, y: f64, z: f64) -> f64 { (SPH_A * (sph_r(x, y, z) - SPH_R)).cos() } fn sph_f(x: f64, y: f64, z: f64) -> f64 { let r = sph_r(x, y, z); let q = SPH_A * (r - SPH_R); SPH_A * SPH_A * q.cos() + 2.0 * SPH_A * q.sin() / r } fn sphere() -> Sdf { Arc::new(|x, y, z| sph_r(x, y, z) - SPH_R) } /// Volume-weighted RMS and max of the error over a subset. #[derive(Default, Clone, Copy)] struct Err { s2: f64, vol: f64, max: f64, count: usize, } impl Err { fn add(&mut self, e: f64, v: f64) { self.s2 += e * e * v; self.vol += v; self.max = self.max.max(e.abs()); self.count += 1; } fn l2(&self) -> f64 { if self.vol > 0.0 { (self.s2 / self.vol).sqrt() } else { f64::NAN } } } fn out_file(name: &str) -> Option { let dir = std::env::var("R7_OUT").ok()?; std::fs::create_dir_all(&dir).ok()?; std::fs::File::create(format!("{dir}/{name}")).ok() } struct CompositeRun { unknowns: usize, fine_unknowns: usize, iterations: usize, rel: f64, setup_s: f64, solve_s: f64, asym: f64, /// interface, patch interior (fine, not interface), coarse (not /// interface), cut cells, all. err: [Err; 5], } fn run_composite( n: usize, lo: usize, hi: usize, iface: Interface, body: Option, exact: fn(f64, f64, f64) -> f64, source: fn(f64, f64, f64) -> f64, measure_asym: bool, ) -> CompositeRun { let g = Grid::cubic(n, n, n, 1.0 / n as f64); let spec = CompositeSpec { coarse: g, lo: [lo; 3], hi: [hi; 3], interface: iface, body, source: &source, dirichlet: &exact, }; let t = std::time::Instant::now(); let c = Composite::build(&spec); let build_s = t.elapsed().as_secs_f64(); let mut x = vec![0.0; c.unknowns()]; let st = solve_bicgstab(&c, &mut x, 1e-11, 400, 2); assert!(st.converged, "composite solve did not converge: {st:?}"); let mut err = [Err::default(); 5]; for u in 0..c.unknowns() { let p = c.centre[u]; let e = x[u] - exact(p[0], p[1], p[2]); let v = c.volume[u]; let fine = u >= c.n_coarse; let class = if c.at_interface[u] { 0 } else if fine { 1 } else { 2 }; err[class].add(e, v); if c.cut[u] { err[3].add(e, v); } err[4].add(e, v); } CompositeRun { unknowns: c.unknowns(), fine_unknowns: c.unknowns() - c.n_coarse, iterations: st.iterations, rel: st.rel_residual, setup_s: build_s + st.setup_s, solve_s: st.solve_s, asym: if measure_asym { c.a.asymmetry() } else { f64::NAN }, err, } } struct UniformRun { cells: usize, iterations: usize, setup_s: f64, solve_s: f64, /// inside the patch region [lo, hi)·h_c, outside it, cut cells, all. err: [Err; 4], } fn run_uniform( n: usize, region: (f64, f64), body: Option<&Sdf>, exact: fn(f64, f64, f64) -> f64, source: fn(f64, f64, f64) -> f64, ) -> UniformRun { let g = Grid::cubic(n, n, n, 1.0 / n as f64); let t = std::time::Instant::now(); let (prob, frac) = uniform_problem(g, body, &source, &exact); let build_s = t.elapsed().as_secs_f64(); let mut p = vec![0.0; g.cells()]; let l1: f64 = prob.rhs.iter().map(|v| v.abs()).sum(); let params = MultigridParameters { max_iterations: 2000, ..MultigridParameters::default() }; let sol = solve_pcg(&prob, &mut p, ¶ms, 1e-11 * l1, None); assert!(sol.converged, "uniform solve did not converge"); let h = g.dx; let mut err = [Err::default(); 4]; for k in 0..n { for j in 0..n { for i in 0..n { let idx = g.cell(k, j, i); if !prob.active[idx] { continue; } let x = [ (i as f64 + 0.5) * h, (j as f64 + 0.5) * h, (k as f64 + 0.5) * h, ]; let e = p[idx] - exact(x[0], x[1], x[2]); let v = frac[idx] * h * h * h; let inside = x.iter().all(|&c| c > region.0 && c < region.1); err[usize::from(!inside)].add(e, v); if frac[idx] < 1.0 - 1e-9 { err[2].add(e, v); } err[3].add(e, v); } } } UniformRun { cells: prob.active.iter().filter(|&&a| a).count(), iterations: sol.iterations, setup_s: build_s + sol.setup_ns as f64 * 1e-9, solve_s: sol.iterate_ns as f64 * 1e-9, err, } } fn order(a: f64, b: f64) -> f64 { (a / b).log2() } #[test] fn composite_smoke() { for iface in [Interface::Direct, Interface::Octree, Interface::Quadratic] { let r = run_composite(12, 3, 9, iface, None, mms_u, mms_f, true); eprintln!( "{iface:?}: {} unknowns, {} it, rel {:.2e}, asym {:.2e}, L2 iface {:.3e} all {:.3e}", r.unknowns, r.iterations, r.rel, r.asym, r.err[0].l2(), r.err[4].l2() ); assert!(r.err[4].l2() < 5e-3); if iface == Interface::Direct { assert!(r.asym < 1e-14); } } } #[test] #[ignore = "P1 ladder (minutes)"] fn composite_mms_ladder() { ladder("p1_mms.csv", mms_u, mms_f); } #[test] #[ignore = "P1b ladder with a localized bump (minutes)"] fn composite_bump_ladder() { ladder("p1b_bump.csv", bump_u, bump_f); } fn ladder(name: &str, exact: fn(f64, f64, f64) -> f64, source: fn(f64, f64, f64) -> f64) { let mut csv = out_file(name); if let Some(f) = csv.as_mut() { writeln!( f, "scheme,n,unknowns,iters,rel,l2_iface,linf_iface,l2_patch,linf_patch,l2_coarse,linf_coarse,l2_all,linf_all,setup_s,solve_s" ) .ok(); } let ns = [16usize, 32, 64]; let mut table: Vec<(String, Vec<[f64; 8]>)> = Vec::new(); for iface in [Interface::Direct, Interface::Octree, Interface::Quadratic] { let mut rows = Vec::new(); for &n in &ns { let r = run_composite(n, n / 4, 3 * n / 4, iface, None, exact, source, false); let e = r.err; eprintln!( "{iface:?} n {n}: {} unk, {} it, rel {:.1e}; iface L2 {:.3e} Linf {:.3e} | patch L2 {:.3e} Linf {:.3e} | coarse L2 {:.3e} Linf {:.3e} | all L2 {:.3e} | {:.2}+{:.2} s", r.unknowns, r.iterations, r.rel, e[0].l2(), e[0].max, e[1].l2(), e[1].max, e[2].l2(), e[2].max, e[4].l2(), r.setup_s, r.solve_s ); if let Some(f) = csv.as_mut() { writeln!( f, "{iface:?},{n},{},{},{:.3e},{:.6e},{:.6e},{:.6e},{:.6e},{:.6e},{:.6e},{:.6e},{:.6e},{:.3},{:.3}", r.unknowns, r.iterations, r.rel, e[0].l2(), e[0].max, e[1].l2(), e[1].max, e[2].l2(), e[2].max, e[4].l2(), e[4].max, r.setup_s, r.solve_s ) .ok(); } rows.push([ e[0].l2(), e[0].max, e[1].l2(), e[1].max, e[2].l2(), e[2].max, e[4].l2(), e[4].max, ]); } table.push((format!("{iface:?}"), rows)); } for (label, n_of) in [("uniform-coarse", 1usize), ("uniform-fine", 2)] { for &n in &ns { let r = run_uniform(n_of * n, (0.25, 0.75), None, exact, source); let e = r.err; eprintln!( "{label} n {}: {} cells, {} it; patch-region L2 {:.3e} Linf {:.3e} | outside L2 {:.3e} Linf {:.3e} | all L2 {:.3e} | {:.2}+{:.2} s", n_of * n, r.cells, r.iterations, e[0].l2(), e[0].max, e[1].l2(), e[1].max, e[3].l2(), r.setup_s, r.solve_s ); if let Some(f) = csv.as_mut() { writeln!( f, "{label},{},{},{},0,nan,nan,{:.6e},{:.6e},{:.6e},{:.6e},{:.6e},{:.6e},{:.3},{:.3}", n_of * n, r.cells, r.iterations, e[0].l2(), e[0].max, e[1].l2(), e[1].max, e[3].l2(), e[3].max, r.setup_s, r.solve_s ) .ok(); } } } eprintln!("orders (16→32, 32→64): iface L2 / Linf | patch L2 | coarse L2 | all L2 / Linf"); for (label, rows) in &table { let o = |c: usize| (order(rows[0][c], rows[1][c]), order(rows[1][c], rows[2][c])); eprintln!( "{label}: iface {:.2},{:.2} / {:.2},{:.2} | patch {:.2},{:.2} | coarse {:.2},{:.2} | all {:.2},{:.2} / {:.2},{:.2}", o(0).0, o(0).1, o(1).0, o(1).1, o(2).0, o(2).1, o(4).0, o(4).1, o(6).0, o(6).1, o(7).0, o(7).1 ); } } #[test] #[ignore = "P2 cut sphere in the patch (minutes)"] fn composite_cut_sphere() { let mut csv = out_file("p2_sphere.csv"); if let Some(f) = csv.as_mut() { writeln!( f, "scheme,n,unknowns,iters,l2_iface,linf_iface,l2_patch,linf_patch,l2_coarse,l2_cut,linf_cut,l2_all,linf_all" ) .ok(); } let body = sphere(); for &n in &[16usize, 32, 64] { for iface in [Interface::Direct, Interface::Octree, Interface::Quadratic] { let r = run_composite( n, n / 4, 3 * n / 4, iface, Some(body.clone()), sph_u, sph_f, false, ); let e = r.err; eprintln!( "{iface:?} n {n}: {} unk ({} fine), {} it; iface L2 {:.3e} | patch L2 {:.3e} Linf {:.3e} | coarse L2 {:.3e} | cut L2 {:.3e} Linf {:.3e} | all L2 {:.3e} Linf {:.3e}", r.unknowns, r.fine_unknowns, r.iterations, e[0].l2(), e[1].l2(), e[1].max, e[2].l2(), e[3].l2(), e[3].max, e[4].l2(), e[4].max ); if let Some(f) = csv.as_mut() { writeln!( f, "{iface:?},{n},{},{},{:.6e},{:.6e},{:.6e},{:.6e},{:.6e},{:.6e},{:.6e},{:.6e},{:.6e}", r.unknowns, r.iterations, e[0].l2(), e[0].max, e[1].l2(), e[1].max, e[2].l2(), e[3].l2(), e[3].max, e[4].l2(), e[4].max ) .ok(); } } for (label, m) in [("uniform-coarse", n), ("uniform-fine", 2 * n)] { let r = run_uniform(m, (0.25, 0.75), Some(&body), sph_u, sph_f); let e = r.err; eprintln!( "{label} n {m}: {} cells, {} it; patch-region L2 {:.3e} Linf {:.3e} | outside L2 {:.3e} | cut L2 {:.3e} Linf {:.3e} | all L2 {:.3e} Linf {:.3e}", r.cells, r.iterations, e[0].l2(), e[0].max, e[1].l2(), e[2].l2(), e[2].max, e[3].l2(), e[3].max ); if let Some(f) = csv.as_mut() { writeln!( f, "{label},{m},{},{},nan,nan,{:.6e},{:.6e},{:.6e},{:.6e},{:.6e},{:.6e},{:.6e}", r.cells, r.iterations, e[0].l2(), e[0].max, e[1].l2(), e[2].l2(), e[2].max, e[3].l2(), e[3].max ) .ok(); } } } } #[test] #[ignore = "P3 cost against the uniformly fine grid (minutes)"] fn composite_cost() { let mut csv = out_file("p3_cost.csv"); if let Some(f) = csv.as_mut() { writeln!( f, "field,case,n,patch,unknowns,iters,setup_s,solve_s,l2_patch_region,linf_patch_region,l2_all" ) .ok(); } let n = 64usize; let h = 1.0 / n as f64; type Pair = ( &'static str, fn(f64, f64, f64) -> f64, fn(f64, f64, f64) -> f64, ); let fields: [Pair; 2] = [("smooth", mms_u, mms_f), ("bump", bump_u, bump_f)]; for (field, exact, source) in fields { for (label, lo, hi) in [("half", 16usize, 48usize), ("quarter", 24, 40)] { let r = run_composite(n, lo, hi, Interface::Quadratic, None, exact, source, false); // The patch region's error: interface fine cells + patch interior. let mut e = Err::default(); let c = { let g = Grid::cubic(n, n, n, h); let spec = CompositeSpec { coarse: g, lo: [lo; 3], hi: [hi; 3], interface: Interface::Quadratic, body: None, source: &source, dirichlet: &exact, }; Composite::build(&spec) }; let mut x = vec![0.0; c.unknowns()]; let _ = solve_bicgstab(&c, &mut x, 1e-11, 400, 2); for u in c.n_coarse..c.unknowns() { let p = c.centre[u]; e.add(x[u] - exact(p[0], p[1], p[2]), c.volume[u]); } eprintln!( "{field} composite {label} (n {n}, patch {lo}..{hi}): {} unknowns, {} it, setup {:.2} s, solve {:.2} s, patch L2 {:.3e} Linf {:.3e}, all L2 {:.3e}", r.unknowns, r.iterations, r.setup_s, r.solve_s, e.l2(), e.max, r.err[4].l2() ); if let Some(f) = csv.as_mut() { writeln!( f, "{field},composite-{label},{n},{lo}..{hi},{},{},{:.3},{:.3},{:.6e},{:.6e},{:.6e}", r.unknowns, r.iterations, r.setup_s, r.solve_s, e.l2(), e.max, r.err[4].l2() ) .ok(); } for (ulabel, m) in [("uniform-fine", 2 * n), ("uniform-coarse", n)] { let r = run_uniform(m, (lo as f64 * h, hi as f64 * h), None, exact, source); eprintln!( "{field} {ulabel} n {m} (region {lo}..{hi}): {} cells, {} it, setup {:.2} s, solve {:.2} s, region L2 {:.3e} Linf {:.3e}, all L2 {:.3e}", r.cells, r.iterations, r.setup_s, r.solve_s, r.err[0].l2(), r.err[0].max, r.err[3].l2() ); if let Some(f) = csv.as_mut() { writeln!( f, "{field},{ulabel},{m},{lo}..{hi},{},{},{:.3},{:.3},{:.6e},{:.6e},{:.6e}", r.cells, r.iterations, r.setup_s, r.solve_s, r.err[0].l2(), r.err[0].max, r.err[3].l2() ) .ok(); } } } } } fn quad_u(x: f64, y: f64, z: f64) -> f64 { x * x + 2.0 * y * y - 1.5 * z * z + 0.7 * x * y - 0.4 * y * z + 0.9 * x * z + 0.3 * x } fn quad_f(_x: f64, _y: f64, _z: f64) -> f64 { -(2.0 + 4.0 - 3.0) } /// Local consistency: the exact quadratic's residual on the rows away from /// the outer boundary, per interface flux (the Quadratic ghost is exact for /// quadratics, so its interface rows must be at round-off). #[test] fn composite_quadratic_consistency() { let n = 12; let g = Grid::cubic(n, n, n, 1.0 / n as f64); for iface in [Interface::Direct, Interface::Octree, Interface::Quadratic] { let spec = CompositeSpec { coarse: g, lo: [3; 3], hi: [9; 3], interface: iface, body: None, source: &quad_f, dirichlet: &quad_u, }; let c = Composite::build(&spec); let u: Vec = c.centre.iter().map(|p| quad_u(p[0], p[1], p[2])).collect(); let mut au = vec![0.0; u.len()]; c.a.apply(&u, &mut au); let h = g.dx; let (mut m_if, mut m_in) = (0.0f64, 0.0f64); for r in 0..u.len() { let p = c.centre[r]; if p.iter().any(|&x| x < h || x > 1.0 - h) { continue; } // Relative to the row's volume source |f| V. let res = (c.rhs[r] - au[r]).abs() / (3.0 * c.volume[r]); if c.at_interface[r] { m_if = m_if.max(res); } else { m_in = m_in.max(res); } } eprintln!("{iface:?}: max |truncation| / |f V|: interface {m_if:.3e}, interior {m_in:.3e}"); assert!(m_in < 1e-10); if iface == Interface::Quadratic { assert!(m_if < 1e-10, "quadratic ghost not exact on a quadratic"); } } } /// P2, direct: the composite solution on the patch's fine cells against /// the uniformly fine solution on the SAME cells with the same cut (the /// fine cut geometry of the patch vs the uniform grid's, compared too). #[test] #[ignore = "P2 composite vs uniform fine on the patch cells (minutes)"] fn composite_sphere_vs_fine() { let mut csv = out_file("p2_vs_fine.csv"); if let Some(f) = csv.as_mut() { writeln!( f, "scheme,n,max_dfrac,l2_diff_patch,linf_diff_patch,l2_diff_cut,linf_diff_cut,l2_err_fine_patch,linf_err_fine_patch" ) .ok(); } let body = sphere(); for &n in &[16usize, 32, 64] { let m = 2 * n; let gf = Grid::cubic(m, m, m, 1.0 / m as f64); let (prob, frac) = uniform_problem(gf, Some(&body), &sph_f, &sph_u); let mut pf = vec![0.0; gf.cells()]; let l1: f64 = prob.rhs.iter().map(|v| v.abs()).sum(); let params = MultigridParameters { max_iterations: 2000, ..MultigridParameters::default() }; assert!(solve_pcg(&prob, &mut pf, ¶ms, 1e-12 * l1, None).converged); for iface in [Interface::Octree, Interface::Quadratic] { let g = Grid::cubic(n, n, n, 1.0 / n as f64); let spec = CompositeSpec { coarse: g, lo: [n / 4; 3], hi: [3 * n / 4; 3], interface: iface, body: Some(body.clone()), source: &sph_f, dirichlet: &sph_u, }; let c = Composite::build(&spec); let mut x = vec![0.0; c.unknowns()]; assert!(solve_bicgstab(&c, &mut x, 1e-12, 400, 2).converged); let (mut d_all, mut d_cut, mut e_fine) = (Err::default(), Err::default(), Err::default()); let mut max_dfrac: f64 = 0.0; let off = n / 2; // the patch's first fine index on the uniform fine grid for k in 0..c.fine.nz { for j in 0..c.fine.ny { for i in 0..c.fine.nx { let u = c.fine_id[c.fine.cell(k, j, i)]; let idx = gf.cell(k + off, j + off, i + off); if u == usize::MAX { assert!(!prob.active[idx] || frac[idx] < 1e-12, "activity differs"); continue; } let hf = c.fine.dx; let fr = c.volume[u] / (hf * hf * hf); max_dfrac = max_dfrac.max((fr - frac[idx]).abs()); let d = x[u] - pf[idx]; d_all.add(d, c.volume[u]); if c.cut[u] { d_cut.add(d, c.volume[u]); } let p = c.centre[u]; e_fine.add(pf[idx] - sph_u(p[0], p[1], p[2]), c.volume[u]); } } } eprintln!( "{iface:?} n {n} vs uniform {m}: max |Δfrac| {max_dfrac:.1e}; composite − fine on the patch L2 {:.3e} Linf {:.3e}; on cut cells ({}) L2 {:.3e} Linf {:.3e}; the fine grid's own error there L2 {:.3e} Linf {:.3e}", d_all.l2(), d_all.max, d_cut.count, d_cut.l2(), d_cut.max, e_fine.l2(), e_fine.max ); if let Some(f) = csv.as_mut() { writeln!( f, "{iface:?},{n},{max_dfrac:.3e},{:.6e},{:.6e},{:.6e},{:.6e},{:.6e},{:.6e}", d_all.l2(), d_all.max, d_cut.l2(), d_cut.max, e_fine.l2(), e_fine.max ) .ok(); } } } }