Files
rustytorch/crates/specialized/rtx-cfd/tests/embedded3_composite_poisson.rs
T

641 lines
20 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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<std::fs::File> {
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<Sdf>,
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, &params, 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 {
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<f64> = 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");
}
}
}