//! 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, surface: Vec, cv: Vec, } 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 = resolutions.iter().map(|&n| measure(n, scheme, C)).collect(); let errors: Vec = 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; let a = norm([ mm.force_sampler[0] - fe[0], mm.force_sampler[1] - fe[1], mm.force_sampler[2] - fe[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} F_sampler rel {a:.3e}", mm.l2_velocity, mm.max_div, mm.ghost_correction, mm.force_surface, mm.skipped, mm.force_cv ); if scheme == WallScheme::CutCell { // S2-1 (reported, not gated): the traction sampler on the cut // field against the box route — 1.15× at n 24 on the first read // (`docs/embedded3_campaign.md`, S2-1). println!( " sampler / box error ratio at n = {n}: {:.3}", a / c.max(1e-300) ); } 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); }