CI / Build (macos-latest) (push) Waiting to run
CI / Test (macos-latest) (push) Blocked by required conditions
CI / Test (ubuntu-latest) (push) Blocked by required conditions
CI / Python Bindings (maturin) (macos-latest) (push) Blocked by required conditions
CI / Python Bindings (maturin) (ubuntu-latest) (push) Blocked by required conditions
CI / WASM Build + Size Check (push) Blocked by required conditions
CI / Distributed Training Tests (push) Blocked by required conditions
CI / CI Success (push) Blocked by required conditions
CI / Clippy Check (push) Failing after 3s
CI / Build (ubuntu-latest) (push) Failing after 4s
CI / Format Check (push) Failing after 4s
Performance Benchmarks / Run Benchmarks (push) Failing after 5s
Documentation / Build User Guide (push) Successful in 6s
CI / Build CPU-Only (Explicit) (push) Failing after 1m6s
Documentation / Build API Documentation (push) Failing after 1m9s
Co-Authored-By: Claude Fable 5.1 <[email protected]>
145 lines
4.9 KiB
Rust
145 lines
4.9 KiB
Rust
//! embedded3 gates 9a and 10: the manufactured solution with an embedded
|
||
//! sphere (centre (0.6, 0.45, 0.5), r 0.2, off-centre so the exact force is
|
||
//! not zero by symmetry) carrying the exact field as its surface velocity,
|
||
//! on the binary ghost wall (item 9) and the apertured cut-cell wall (item
|
||
//! 10). The velocity error falls at the scheme's order, every fluid cell
|
||
//! is divergence-free (apertured, with the porous surface's flux, on the
|
||
//! cut wall), the compatibility correction shrinks, and both load routes
|
||
//! converge to the exact surface integral of the manufactured stress (the
|
||
//! control-volume route measures F − M with M the momentum flux through
|
||
//! the porous manufactured surface). Item 10's gate: the cut wall's errors
|
||
//! are at most the binary wall's at every n, its loads within 10 % at the
|
||
//! finest rung.
|
||
|
||
mod embedded3_sphere;
|
||
|
||
use embedded3_sphere::{C, Measurement, exact_force_and_flux, measure};
|
||
use rtx_cfd::solvers::incompressible::embedded3::WallScheme;
|
||
|
||
fn norm(a: [f64; 3]) -> f64 {
|
||
(a[0] * a[0] + a[1] * a[1] + a[2] * a[2]).sqrt()
|
||
}
|
||
|
||
/// The velocity errors and the two routes' relative force errors per rung.
|
||
struct Ladder {
|
||
errors: Vec<f64>,
|
||
surface: Vec<f64>,
|
||
cv: Vec<f64>,
|
||
}
|
||
|
||
fn ladder(resolutions: &[usize], scheme: WallScheme) -> Ladder {
|
||
let (fe, m) = exact_force_and_flux(C);
|
||
let f_scale = norm(fe);
|
||
let fcv = [fe[0] - m[0], fe[1] - m[1], fe[2] - m[2]];
|
||
println!(
|
||
" {scheme:?}: exact force {fe:.5?}; momentum flux {m:.5?}; the control-volume route measures {fcv:.5?}"
|
||
);
|
||
let ms: Vec<Measurement> = resolutions.iter().map(|&n| measure(n, scheme, C)).collect();
|
||
let errors: Vec<f64> = ms.iter().map(|x| x.l2_velocity).collect();
|
||
let mut se = Vec::new();
|
||
let mut ce = Vec::new();
|
||
for (k, (mm, &n)) in ms.iter().zip(resolutions).enumerate() {
|
||
let rate = if k == 0 {
|
||
" -".to_string()
|
||
} else {
|
||
format!("{:5.2}", (errors[k - 1] / errors[k]).log2())
|
||
};
|
||
let s = norm([
|
||
mm.force_surface[0] - fe[0],
|
||
mm.force_surface[1] - fe[1],
|
||
mm.force_surface[2] - fe[2],
|
||
]) / f_scale;
|
||
let c = norm([
|
||
mm.force_cv[0] - fcv[0],
|
||
mm.force_cv[1] - fcv[1],
|
||
mm.force_cv[2] - fcv[2],
|
||
]) / f_scale;
|
||
println!(
|
||
" n = {n:3} L2 u {:.4e} (order {rate}) max div {:.2e} ghost corr {:.2e} F_surface {:.4?} rel {s:.3e} (skipped {}) F_cv {:.4?} rel {c:.3e}",
|
||
mm.l2_velocity,
|
||
mm.max_div,
|
||
mm.ghost_correction,
|
||
mm.force_surface,
|
||
mm.skipped,
|
||
mm.force_cv
|
||
);
|
||
se.push(s);
|
||
ce.push(c);
|
||
}
|
||
assert!(
|
||
errors.windows(2).all(|w| w[1] < w[0]),
|
||
"errors not monotone {errors:?}"
|
||
);
|
||
for w in errors.windows(2) {
|
||
let rate = (w[0] / w[1]).log2();
|
||
assert!(
|
||
rate > 0.75 && rate < 2.3,
|
||
"order {rate:.3} outside [0.75, 2.3]"
|
||
);
|
||
}
|
||
for mm in &ms {
|
||
assert!(mm.max_div < 1e-5, "max div {:.3e}", mm.max_div);
|
||
}
|
||
assert!(
|
||
se.windows(2).all(|w| w[1] < w[0]),
|
||
"surface-route error not falling {se:?}"
|
||
);
|
||
assert!(
|
||
ce.windows(2).all(|w| w[1] < w[0]),
|
||
"control-volume-route error not falling {ce:?}"
|
||
);
|
||
Ladder {
|
||
errors,
|
||
surface: se,
|
||
cv: ce,
|
||
}
|
||
}
|
||
|
||
/// Item 10's comparison: the cut wall's velocity error at most the binary
|
||
/// wall's at every rung; both routes within `load_bound` at the finest.
|
||
fn compare(resolutions: &[usize], load_bound: f64) {
|
||
let ghost = ladder(resolutions, WallScheme::GhostBinary);
|
||
let cut = ladder(resolutions, WallScheme::CutCell);
|
||
for (k, &n) in resolutions.iter().enumerate() {
|
||
println!(
|
||
" n = {n:3} L2 u ghost {:.4e} cut {:.4e} (ratio {:.3})",
|
||
ghost.errors[k],
|
||
cut.errors[k],
|
||
cut.errors[k] / ghost.errors[k]
|
||
);
|
||
assert!(
|
||
cut.errors[k] <= ghost.errors[k],
|
||
"cut-cell error above the binary wall's at n = {n}"
|
||
);
|
||
}
|
||
let last = resolutions.len() - 1;
|
||
assert!(
|
||
cut.surface[last] < load_bound && cut.cv[last] < load_bound,
|
||
"cut-cell loads at the finest rung: surface {:.3e}, control volume {:.3e} (bound {load_bound})",
|
||
cut.surface[last],
|
||
cut.cv[last]
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn embedded_sphere_recovers_the_manufactured_solution() {
|
||
ladder(&[12, 24], WallScheme::GhostBinary);
|
||
}
|
||
|
||
#[test]
|
||
fn cut_cell_wall_recovers_the_manufactured_solution() {
|
||
compare(&[12, 24], 0.2);
|
||
}
|
||
|
||
#[test]
|
||
#[ignore = "the three-rung ladder to n = 48 (minutes on the host)"]
|
||
fn embedded_sphere_three_rungs() {
|
||
ladder(&[12, 24, 48], WallScheme::GhostBinary);
|
||
}
|
||
|
||
#[test]
|
||
#[ignore = "item 10's finest rung: the cut wall's loads within 10 % at n = 48"]
|
||
fn cut_cell_three_rungs() {
|
||
compare(&[12, 24, 48], 0.1);
|
||
}
|