//! embedded3 item 11b: the wall's smoothness in the interface position. //! The manufactured sphere is marched to steady state at `M + 1` centres //! spaced `h/M` apart across one cell along x; at each the load error //! `E(δ) = F(δ) − F_exact(δ)` (the exact force moves with the sphere and //! is subtracted) is measured on the scheme's route. The largest jump of //! `E` between neighbouring positions, relative to the load, and the //! Lipschitz quotient `|ΔE| / (Δδ |F|)` are reported for both walls. //! Registered gate (`docs/embedded3_campaign.md` item 11): the cut wall's //! largest neighbouring jump < 1 % of the load with a bounded quotient. //! //! Default run: 8 positions at n = 24 (about two minutes on the host); //! the gated `#[ignore]` variant sweeps 40. mod embedded3_sphere; use embedded3_sphere::{C, 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() } struct Sweep { /// Largest neighbouring jump of the load error relative to the load. max_jump: f64, /// Largest Lipschitz quotient `|ΔE| / (Δδ |F|)` (per unit length). max_quotient: f64, } fn sweep(n: usize, positions: usize, scheme: WallScheme) -> Sweep { let h = 1.0 / n as f64; let step = h / positions as f64; let mut errors: Vec<[f64; 3]> = Vec::new(); let mut scale = 0.0_f64; for m in 0..=positions { let c = (C.0 + m as f64 * step, C.1, C.2); let (fe, _) = exact_force_and_flux(c); let r = measure(n, scheme, c); let e = [ r.force_surface[0] - fe[0], r.force_surface[1] - fe[1], r.force_surface[2] - fe[2], ]; scale = scale.max(norm(fe)); println!( " {scheme:?} δ = {:.4} h: F {:.5?} exact {:.5?} error {:.3e} (rel {:.3e})", m as f64 / positions as f64, r.force_surface, fe, norm(e), norm(e) / norm(fe) ); errors.push(e); } let mut max_jump = 0.0_f64; for w in errors.windows(2) { let d = norm([w[1][0] - w[0][0], w[1][1] - w[0][1], w[1][2] - w[0][2]]); max_jump = max_jump.max(d / scale); } let max_quotient = max_jump / step; println!( " {scheme:?}: largest neighbouring jump {:.3e} of the load (spacing {:.3e} = h/{positions}); Lipschitz quotient {:.3e} per unit length", max_jump, step, max_quotient ); Sweep { max_jump, max_quotient, } } #[test] fn sphere_load_across_one_cell() { let ghost = sweep(24, 8, WallScheme::GhostBinary); let cut = sweep(24, 8, WallScheme::CutCell); println!( " jumps: ghost {:.3e}, cut {:.3e} ({:.1}x smaller); quotients: ghost {:.3e}, cut {:.3e}", ghost.max_jump, cut.max_jump, ghost.max_jump / cut.max_jump.max(1e-300), ghost.max_quotient, cut.max_quotient ); assert!(ghost.max_jump.is_finite() && cut.max_jump.is_finite()); } #[test] #[ignore = "item 11's gated sweep (40 positions, both walls; tens of minutes on the host)"] fn sphere_load_lipschitz_gate() { let ghost = sweep(24, 40, WallScheme::GhostBinary); let cut = sweep(24, 40, WallScheme::CutCell); println!( " GATE: cut largest jump {:.3e} of the load (ghost {:.3e}); cut quotient {:.3e} (ghost {:.3e})", cut.max_jump, ghost.max_jump, cut.max_quotient, ghost.max_quotient ); assert!( cut.max_jump < 0.01, "cut-cell jump {:.3e} of the load", cut.max_jump ); }