Documentation / Build API Documentation (push) Canceled after 0s
Documentation / Build User Guide (push) Canceled after 0s
Performance Benchmarks / Run Benchmarks (push) Canceled after 0s
CI / Test (ubuntu-latest) (push) Canceled after 0s
CI / Build CPU-Only (Explicit) (push) Canceled after 0s
CI / Python Bindings (maturin) (macos-latest) (push) Canceled after 0s
CI / Python Bindings (maturin) (ubuntu-latest) (push) Canceled after 0s
CI / WASM Build + Size Check (push) Canceled after 0s
CI / Distributed Training Tests (push) Canceled after 0s
CI / CI Success (push) Canceled after 0s
CI / Format Check (push) Canceled after 0s
CI / Clippy Check (push) Canceled after 0s
CI / Build (macos-latest) (push) Canceled after 0s
CI / Build (ubuntu-latest) (push) Canceled after 0s
CI / Test (macos-latest) (push) Canceled after 0s
patch_gen::{stadium, graded_fractions}: the falsifier plate as a stadium O-grid
(semicircular ends r = half-thickness; 16 cells per end arc, straights graded
0.30 h -> h at 1.15, offset 6 h, 12 rows stretched 4x; 148x12 cells, every ray
a normal, worst non-orthogonality 4 deg). CurvilinearPisoSolver::surface_force
(+ PatchLoad): F = sum(-p_f S_f + mu (grad u + grad u^T)_f . S_f) on the wall
faces with the wall cell's LSQ gradients (wall Dirichlet in the velocity fit);
HELD on the phantom circle against the exact stress integral: 1.3e-2 / 6.3e-3 /
3.6e-3 at n = 32/64/128 (orders 1.05 / 0.81), 22x the staircase's accuracy.
OversetPisoSolver: the composite p' level pinned to zero mean over the active
cells every round (the coupled problem is pure Neumann; the temporal warm start
handed each step's level to the next — background pressure 1e7 growing 5e4 per
step on the falsifier; an unpinned level also inflated the relative Schwarz
stop); stall detection (no progress over three rounds = the inner solvers'
noise floor; 6560 of 150k steps burned the 20-round cap at n = 64, a 7.5 h
n = 128 march); schwarz_stalled in the result.
tests/overset_falsifier.rs (records; RTX_OVERSET_FALSIFIER_STRICT asserts the
registered gates, _LADDER runs dt/2 and dt/4, _TRACE the top-12 spike steps):
max spike 594 / 981 / 1720 N/m at dt / dt/2 / dt/4 (staircase 6490 / 12600 /
25600), rms spike 61-89 (810), far probe 502-1509 (7900), KE injection 0.16-0.21
J/m per event on the common cell set (2.6) — every large spike a ~104-cell
full-row reclassification; exponent -0.77 (-1.0). The registered 5% gate (8.75
N/m) is missed 68x: the overset's own reclassification impulse is the finding
(omni-cortex overset_metal_campaign.md §5.10); P3b = locate per cell, then the
fringe flux balance. tests/patch_stadium.rs, curvilinear_loads.rs,
overset_common::plate_patch.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01X2GmJXeQ2njUecEKiJZ1G2
172 lines
6.0 KiB
Rust
172 lines
6.0 KiB
Rust
//! A-P3, first task (`docs/overset_metal_campaign.md` §5.10): the wall
|
||
//! force on the curvilinear patch. On the phantom circle of the embedded
|
||
//! MMS ((0.6, 0.45), r = 0.2, the exact field as the wall velocity) the
|
||
//! patch's surface force must converge to the exact surface integral of
|
||
//! the manufactured stress, `F = ∮ (−p I + μ(∇u + ∇uᵀ)) n ds` — the
|
||
//! embedded solver's staircase reconstruction measured relative errors
|
||
//! 0.52 / 0.29 / 0.15 at n = 16 / 32 / 64 (first order). The patch runs
|
||
//! standalone with its acceptor ring stamped from the exact field (the S3
|
||
//! harness), so the wall force is the patch's own.
|
||
|
||
use rtx_cfd::mesh::PatchSide;
|
||
use rtx_cfd::mesh::patch_gen::annulus_skewed;
|
||
use rtx_cfd::solvers::incompressible::{
|
||
CurvilinearParameters, CurvilinearPisoSolver, NormalDiffusion, PatchField,
|
||
};
|
||
use rtx_cfd::{CfdConfig, CfdResult};
|
||
use std::f64::consts::PI;
|
||
|
||
const RHO: f64 = 1.0;
|
||
const MU: f64 = 0.05;
|
||
const CX: f64 = 0.6;
|
||
const CY: f64 = 0.45;
|
||
const R0: f64 = 0.2;
|
||
const R1: f64 = 0.354;
|
||
|
||
fn u_exact(x: f64, y: f64) -> f64 {
|
||
(PI * x).sin() * (PI * y).cos()
|
||
}
|
||
fn v_exact(x: f64, y: f64) -> f64 {
|
||
-(PI * x).cos() * (PI * y).sin()
|
||
}
|
||
fn p_exact(x: f64, y: f64) -> f64 {
|
||
(PI * x).sin() * (PI * y).sin()
|
||
}
|
||
fn source(x: f64, y: f64) -> (f64, f64) {
|
||
let conv = RHO * 0.5 * PI;
|
||
(
|
||
conv * (2.0 * PI * x).sin()
|
||
+ 2.0 * PI * PI * MU * u_exact(x, y)
|
||
+ PI * (PI * x).cos() * (PI * y).sin(),
|
||
conv * (2.0 * PI * y).sin()
|
||
+ 2.0 * PI * PI * MU * v_exact(x, y)
|
||
+ PI * (PI * x).sin() * (PI * y).cos(),
|
||
)
|
||
}
|
||
|
||
/// `F = ∮ (−p I + μ(∇u + ∇uᵀ)) n ds` on the circle by fine quadrature
|
||
/// (`embedded_mms.rs`).
|
||
fn exact_force() -> (f64, f64) {
|
||
let n = 20_000;
|
||
let (mut fx, mut fy) = (0.0, 0.0);
|
||
for k in 0..n {
|
||
let theta = (k as f64 + 0.5) * 2.0 * PI / n as f64;
|
||
let (s, c) = theta.sin_cos();
|
||
let (x, y) = (CX + R0 * c, CY + R0 * s);
|
||
let ux = PI * (PI * x).cos() * (PI * y).cos();
|
||
let uy = -PI * (PI * x).sin() * (PI * y).sin();
|
||
let vx = PI * (PI * x).sin() * (PI * y).sin();
|
||
let vy = -PI * (PI * x).cos() * (PI * y).cos();
|
||
let p = p_exact(x, y);
|
||
let sxx = -p + 2.0 * MU * ux;
|
||
let syy = -p + 2.0 * MU * vy;
|
||
let sxy = MU * (uy + vx);
|
||
let ds = 2.0 * PI * R0 / n as f64;
|
||
fx += (sxx * c + sxy * s) * ds;
|
||
fy += (sxy * c + syy * s) * ds;
|
||
}
|
||
(fx, fy)
|
||
}
|
||
|
||
async fn wall_force(n: usize) -> CfdResult<((f64, f64), usize)> {
|
||
let mesh = annulus_skewed([CX, CY], R0, R1, 9 * n / 4, n / 4, 0.3, 3.0)?;
|
||
let nu = MU / RHO;
|
||
let mut hs = f64::INFINITY;
|
||
for c in 0..mesh.cell_count() {
|
||
for (f, _) in mesh.cell_faces(c) {
|
||
if mesh.is_sface(f) {
|
||
let d = mesh.faces()[f].d;
|
||
hs = hs.min((d[0] * d[0] + d[1] * d[1]).sqrt());
|
||
}
|
||
}
|
||
}
|
||
let dt = 0.4 * (hs * hs / (4.0 * nu)).min(1.0 / n as f64);
|
||
let config = CfdConfig::new()
|
||
.with_density(RHO)
|
||
.with_viscosity(MU)
|
||
.with_reference_velocity(1.0)
|
||
.with_reference_length(1.0);
|
||
let mut solver = CurvilinearPisoSolver::new(
|
||
config,
|
||
CurvilinearParameters {
|
||
tolerance: 1e-5,
|
||
normal_diffusion: NormalDiffusion::LineImplicit,
|
||
..CurvilinearParameters::default()
|
||
},
|
||
mesh,
|
||
)?;
|
||
solver.set_boundary_velocity(|x, y, _| (u_exact(x, y), v_exact(x, y)));
|
||
solver.set_momentum_source(|x, y, _| source(x, y));
|
||
solver.set_acceptor_ring(true);
|
||
let (ns, nn) = (solver.mesh().ns(), solver.mesh().nn());
|
||
let acc: Vec<(f64, f64, f64)> = (0..ns)
|
||
.map(|i| {
|
||
let xy = solver.mesh().centre(solver.mesh().cell(nn - 1, i));
|
||
(
|
||
u_exact(xy[0], xy[1]),
|
||
v_exact(xy[0], xy[1]),
|
||
p_exact(xy[0], xy[1]),
|
||
)
|
||
})
|
||
.collect();
|
||
let zeros = vec![0.0; ns];
|
||
let mut field = PatchField::new(solver.mesh());
|
||
solver.initialize(&mut field, |_, _| (0.0, 0.0));
|
||
solver.stamp_acceptors(&mut field, &acc);
|
||
solver.set_acceptor_correction(&zeros);
|
||
let mut steady = f64::INFINITY;
|
||
let mut steps = 0;
|
||
for _ in 0..400_000 {
|
||
let before = (field.u.clone(), field.v.clone());
|
||
solver.advance(&mut field, dt).await?;
|
||
solver.stamp_acceptors(&mut field, &acc);
|
||
steps += 1;
|
||
let change = field
|
||
.u
|
||
.iter()
|
||
.zip(&before.0)
|
||
.chain(field.v.iter().zip(&before.1))
|
||
.map(|(a, b)| (a - b).abs())
|
||
.fold(0.0, f64::max);
|
||
steady = change / dt;
|
||
if steady < 1e-6 {
|
||
break;
|
||
}
|
||
}
|
||
assert!(steady < 1e-6, "no steady state: {steady:.3e}");
|
||
let load = solver.surface_force(&field, PatchSide::Inner, solver.time());
|
||
let f = load.total();
|
||
Ok(((f[0], f[1]), steps))
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn wall_force_on_the_phantom_circle_converges_to_the_exact_stress_integral() -> CfdResult<()>
|
||
{
|
||
let (ex, ey) = exact_force();
|
||
let scale = (ex * ex + ey * ey).sqrt();
|
||
println!(" exact force ({ex:.6e}, {ey:.6e}), |F| {scale:.4e}");
|
||
let mut errs = Vec::new();
|
||
for n in [32usize, 64, 128] {
|
||
let ((fx, fy), steps) = wall_force(n).await?;
|
||
let rel = ((fx - ex).powi(2) + (fy - ey).powi(2)).sqrt() / scale;
|
||
println!(
|
||
" patch n = {n} (ns {} nn {}): force ({fx:.6e}, {fy:.6e}), relative error {rel:.3e}, {steps} steps",
|
||
9 * n / 4,
|
||
n / 4
|
||
);
|
||
errs.push(rel);
|
||
}
|
||
let orders: Vec<f64> = errs.windows(2).map(|w| (w[0] / w[1]).log2()).collect();
|
||
println!(" wall-force orders {orders:?} (embedded staircase: 0.52 / 0.29 / 0.15 at 16/32/64)");
|
||
// Better than the staircase reconstruction at equal h, and converging.
|
||
assert!(
|
||
errs[0] < 0.29 && errs[1] < 0.15,
|
||
"patch wall force worse than the staircase: {errs:?}"
|
||
);
|
||
assert!(
|
||
orders.iter().all(|&o| o > 0.8),
|
||
"wall-force orders {orders:?}"
|
||
);
|
||
Ok(())
|
||
}
|