rtx-cfd: curvilinear collocated PISO on a structured patch (overset A-P0, WIP) — PatchMesh (right-handed s,n; periodic seam with shift; face metrics), patch generators (TFI, skewed annulus, sheared/varying-skew channels), CSR + Jacobi-BiCGSTAB, the Zang–Street–Koseff incremental step with the node-based 9-point L_f, LSQ gradients, explicit and line-implicit-n predictors, adjustPhi; tests: mesh metrics (5 green), operators exact on linear fields incl. the seam (green), sparse (2 green), MMS ladder (Cartesian 16/32: 1.37–1.39x the staggered error, order 0.83; n=64 stalls at a |du/dt| floor 2e-4 — open, tolerance-scaling hypothesis), annulus/Poiseuille not yet run
CI / Distributed Training Tests (push) Canceled after 0s
Performance Benchmarks / Run Benchmarks (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
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 / CI Success (push) Canceled after 0s
Documentation / Build API Documentation (push) Canceled after 0s
Documentation / Build User Guide (push) Canceled after 0s

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01X2GmJXeQ2njUecEKiJZ1G2
This commit is contained in:
Omar Sobh
2026-09-04 05:00:08 -07:00
co-authored by Claude Fable 5.1
parent 1347bc6772
commit 52da75a3a9
13 changed files with 2659 additions and 0 deletions
@@ -0,0 +1,168 @@
//! P0 step 1: the patch mesh's metrics (`docs/overset_metal_campaign.md`
//! §5.3). A Cartesian patch reproduces `dx, dy` exactly; every cell of a
//! skewed periodic annulus is closed (`Σ sign·S_f = 0`) to rounding, seam
//! included; the annulus area converges at second order; a folded patch
//! is refused.
use rtx_cfd::mesh::patch_gen::{annulus_skewed, cartesian, channel_sheared};
use rtx_cfd::mesh::{PatchMesh, PatchSide};
use std::f64::consts::PI;
fn closure_defect(mesh: &PatchMesh) -> f64 {
let mut worst = 0.0_f64;
for c in 0..mesh.cell_count() {
let mut sum = [0.0, 0.0];
for (f, sign) in mesh.cell_faces(c) {
let s = mesh.faces()[f].s;
sum[0] += sign * s[0];
sum[1] += sign * s[1];
}
worst = worst.max(sum[0].abs()).max(sum[1].abs());
}
worst
}
#[test]
fn cartesian_patch_metrics_are_exact() {
let (nx, ny, lx, ly) = (8, 5, 2.0, 1.0);
let mesh = cartesian(nx, ny, lx, ly, false).unwrap();
let (dx, dy) = (lx / nx as f64, ly / ny as f64);
for c in 0..mesh.cell_count() {
assert!((mesh.area(c) - dx * dy).abs() < 1e-15);
let (k, i) = mesh.cell_ki(c);
assert!((mesh.centre(c)[0] - (i as f64 + 0.5) * dx).abs() < 1e-14);
assert!((mesh.centre(c)[1] - (k as f64 + 0.5) * dy).abs() < 1e-14);
}
for (f, face) in mesh.faces().iter().enumerate() {
let expect_s = if mesh.is_sface(f) {
[dy, 0.0]
} else {
[0.0, dx]
};
assert!((face.s[0] - expect_s[0]).abs() < 1e-15 && (face.s[1] - expect_s[1]).abs() < 1e-15);
if face.owner.is_some() && face.neigh.is_some() {
assert!((face.w - 0.5).abs() < 1e-14);
let expect = if mesh.is_sface(f) {
[dx, 0.0]
} else {
[0.0, dy]
};
assert!((face.d[0] - expect[0]).abs() < 1e-14 && (face.d[1] - expect[1]).abs() < 1e-14);
}
}
assert_eq!(closure_defect(&mesh), 0.0);
assert_eq!(mesh.faces().len(), ny * (nx + 1) + (ny + 1) * nx);
let sides: Vec<_> = (0..mesh.faces().len())
.filter_map(|f| mesh.side(f))
.collect();
assert_eq!(sides.iter().filter(|&&s| s == PatchSide::Inner).count(), nx);
assert_eq!(sides.iter().filter(|&&s| s == PatchSide::Outer).count(), nx);
assert_eq!(
sides.iter().filter(|&&s| s == PatchSide::SStart).count(),
ny
);
assert_eq!(sides.iter().filter(|&&s| s == PatchSide::SEnd).count(), ny);
mesh.validate(60.0).unwrap();
}
#[test]
fn skewed_annulus_cells_are_closed_seam_included() {
let mesh = annulus_skewed([0.3, -0.2], 0.5, 1.5, 24, 6, 0.3, 3.0).unwrap();
assert!(mesh.periodic().is_some());
assert_eq!(mesh.faces().len(), 6 * 24 + 7 * 24);
let defect = closure_defect(&mesh);
assert!(defect < 1e-15, "closure defect {defect:.3e}");
// The seam face is shared: cells ns-1 and 0 of every row see it with
// opposite signs and no other face joins them.
for k in 0..mesh.nn() {
let west_of_0 = mesh.cell_faces(mesh.cell(k, 0))[0];
let east_of_last = mesh.cell_faces(mesh.cell(k, mesh.ns() - 1))[1];
assert_eq!(west_of_0.0, east_of_last.0);
assert_eq!(west_of_0.1, -east_of_last.1);
let f = &mesh.faces()[west_of_0.0];
assert_eq!(f.owner, Some(mesh.cell(k, mesh.ns() - 1)));
assert_eq!(f.neigh, Some(mesh.cell(k, 0)));
assert!(f.w > 0.0 && f.w < 1.0);
}
// No boundary faces along s; nn faces per column across.
assert!(
!(0..mesh.faces().len())
.any(|f| { matches!(mesh.side(f), Some(PatchSide::SStart | PatchSide::SEnd)) })
);
// Seam node cells wrap.
let cells = mesh.node_cells(3, 0);
assert_eq!(cells.len(), 4);
assert!(cells.contains(&mesh.cell(2, mesh.ns() - 1)) && cells.contains(&mesh.cell(3, 0)));
// Stretching: the across-patch spacing grows by the requested factor.
let eta = rtx_cfd::mesh::patch_gen::stretched_fractions(6, 3.0);
let ratio = (eta[6] - eta[5]) / (eta[1] - eta[0]);
assert!((ratio - 3.0).abs() < 1e-12, "stretch ratio {ratio}");
assert!((eta[6] - 1.0).abs() < 1e-15 && eta[0] == 0.0);
mesh.validate(60.0).unwrap();
}
#[test]
fn annulus_area_converges_at_second_order() {
let exact = PI * (1.5_f64.powi(2) - 0.5_f64.powi(2));
let errs: Vec<f64> = [16usize, 32, 64]
.iter()
.map(|&ns| {
let mesh = annulus_skewed([0.0, 0.0], 0.5, 1.5, ns, ns / 4, 0.0, 1.0).unwrap();
let area: f64 = (0..mesh.cell_count()).map(|c| mesh.area(c)).sum();
(area - exact).abs()
})
.collect();
let orders: Vec<f64> = errs.windows(2).map(|p| (p[0] / p[1]).log2()).collect();
println!("annulus area errors {errs:?}, orders {orders:?}");
assert!(
orders.iter().all(|&o| o > 1.9 && o < 2.1),
"orders {orders:?}"
);
}
#[test]
fn sheared_channel_is_periodic_with_a_shift_and_congruent() {
let mesh = channel_sheared(2.0, 1.0, 8, 4, 0.4, true).unwrap();
assert_eq!(mesh.periodic(), Some([2.0, 0.0]));
let a0 = mesh.area(0);
assert!((0..mesh.cell_count()).all(|c| (mesh.area(c) - a0).abs() < 1e-14));
// n-faces stay horizontal; s-faces are tilted.
for (f, face) in mesh.faces().iter().enumerate() {
if mesh.is_sface(f) {
assert!(face.s[1] != 0.0);
} else {
assert_eq!(face.s[0], 0.0);
}
}
// The seam face's d spans one cell width (with the shift applied).
let seam = &mesh.faces()[mesh.sface(1, 0)];
assert!(
(seam.d[0] - 0.25).abs() < 1e-14 && seam.d[1].abs() < 1e-14,
"seam d {:?}",
seam.d
);
assert!(closure_defect(&mesh) < 1e-15);
mesh.validate(60.0).unwrap();
}
#[test]
fn a_folded_patch_is_refused() {
let mut inner = Vec::new();
let mut outer = Vec::new();
for i in 0..=8 {
let th = -2.0 * PI * (i % 8) as f64 / 8.0; // clockwise, as the generator does
inner.push([th.cos(), th.sin()]);
outer.push([0.5 * th.cos(), 0.5 * th.sin()]); // inside the inner ring: inverted cells
}
let err = rtx_cfd::mesh::patch_gen::transfinite(&inner, &outer, 3, 1.0, Some([0.0, 0.0]));
assert!(err.is_err());
// A mismatched periodic column is refused too.
let bad = PatchMesh::from_nodes(
4,
2,
(0..15).map(|i| i as f64).collect(),
vec![0.0; 15],
Some([0.0, 0.0]),
);
assert!(bad.is_err());
}