Files
rustytorch/crates/specialized/rtx-cfd/tests/embedded3_flag_geometry.rs
T
Omar SobhandClaude Fable 5.1 e9b2e7887b
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 / Format Check (push) Failing after 5s
CI / Clippy Check (push) Failing after 5s
CI / Build (ubuntu-latest) (push) Failing after 4s
Performance Benchmarks / Run Benchmarks (push) Failing after 5s
Documentation / Build User Guide (push) Successful in 6s
Documentation / Build API Documentation (push) Failing after 17s
CI / Build CPU-Only (Explicit) (push) Failing after 57s
embedded3 S2-6: the oblique-wall instrument (tests/embedded3_wall_position_oblique.rs: z-flow / in-plane Poiseuille + Couette linear exactness; gate oblique_wall_position_is_second_order) and the closures it found, host + device, default OFF — the transverse centroid correction (RTX_E3_DIFFUSION_TRANSVERSE=1, wall_order bit 7), the fine distance floor (RTX_E3_DISTANCE_FLOOR=fine, bit 8); kernel geometry factored into cut_cv; DFG 2D-1 x-shift knob (RTX_E3_DFG_SHIFT_X); FLAG_X0 0.6 -> 0.25 in the three flag tests (the flag of every record was detached)
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-09-18 16:22:38 -05:00

321 lines
13 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! S2-3 pre-flight (host): the flag + cylinder body at ny 62 — the mask
//! builds, its cut geometry closes, the counts and the build time (the
//! moving body's per-step host rebuild cost) are recorded.
use rtx_cfd::solvers::incompressible::embedded3::{Body, Boundaries, Grid, Mask, Side};
const H: f64 = 0.41;
/// The flag's ROOT: the cylinder's rear (the TurekHron flag runs from the
/// cylinder to its tip A at x = 0.6). Until 2026-09-18 this was 0.6 — the
/// flag sat DETACHED, its root where the benchmark's tip is; every flag
/// record before that date is of that geometry.
const FLAG_X0: f64 = 0.25;
const FLAG_LEN: f64 = 0.35;
const FLAG_HALF: f64 = 0.01;
const FLAG_SPAN: f64 = 0.2;
const AMP: f64 = 0.084;
const BETA_L: f64 = 1.875_104_069;
fn mode(s: f64) -> f64 {
let b = BETA_L;
let sigma = (b.sinh() - b.sin()) / (b.cosh() + b.cos());
let phi = |s: f64| (b * s).cosh() - (b * s).cos() - sigma * ((b * s).sinh() - (b * s).sin());
phi(s) / phi(1.0)
}
fn flag_2d(x: f64, y: f64, phase: f64) -> f64 {
let n = 40;
let mut best = f64::INFINITY;
let point = |m: usize| {
let s = m as f64 / n as f64;
(FLAG_X0 + s * FLAG_LEN, 0.2 + AMP * mode(s) * phase)
};
for m in 0..n {
let (ax, ay) = point(m);
let (bx, by) = point(m + 1);
let (ex, ey) = (bx - ax, by - ay);
let u = (((x - ax) * ex + (y - ay) * ey) / (ex * ex + ey * ey)).clamp(0.0, 1.0);
let d = ((x - ax - u * ex).powi(2) + (y - ay - u * ey).powi(2)).sqrt();
best = best.min(d);
}
best - FLAG_HALF
}
fn flag_3d(x: f64, y: f64, z: f64, phase: f64, r: f64) -> f64 {
let d2 = flag_2d(x, y, phase);
let q1 = d2 + r;
let q2 = (z - 0.5 * H).abs() - 0.5 * FLAG_SPAN + r;
(q1.max(0.0).powi(2) + q2.max(0.0).powi(2)).sqrt() + q1.max(q2).min(0.0) - r
}
#[test]
fn flag_body_builds_at_ny_62() {
let ny = 62;
let h = H / ny as f64;
let nx = (2.5 / h).round() as usize;
let g = Grid::cubic(nx, ny, ny, h);
let cyl = |x: f64, y: f64| ((x - 0.2_f64).powi(2) + (y - 0.2_f64).powi(2)).sqrt() - 0.05;
for phase in [0.0, 1.0] {
let body = Body::from_sdf(move |x, y, z, _t| cyl(x, y).min(flag_3d(x, y, z, phase, h)));
let b = Boundaries {
x1: Side::PressureOutlet,
..Boundaries::default()
};
let start = std::time::Instant::now();
let mask = Mask::build_cut(&body, g, 0.0, b).expect("mask");
let build = start.elapsed().as_secs_f64();
let cut = mask.cut().unwrap();
let (area, closure) = cut.wall_area_and_closure();
let solid = g.cells() - mask.fluid_cells();
println!(
" phase {phase}: {} cells, {} fluid, {solid} solid, {} merged; wall area {area:.4} m² (cylinder 0.129 + flag ~0.156), closure {:.2e}; build {build:.2} s",
g.cells(),
mask.fluid_cells(),
mask.merged_cells(),
(closure[0].powi(2) + closure[1].powi(2) + closure[2].powi(2)).sqrt()
);
assert!(solid > 1000 && mask.fluid_cells() > g.cells() / 2);
assert!((closure[0].powi(2) + closure[1].powi(2) + closure[2].powi(2)).sqrt() < 1e-9);
}
}
/// Where the per-step host rebuild's time goes at ny 62 (S2-2b's target).
#[test]
#[ignore = "profile: the rebuild's parts at ny 62 (seconds)"]
fn rebuild_profile_at_ny_62() {
use rtx_cfd::solvers::incompressible::embedded3::CutGeometry;
let ny = 62;
let h = H / ny as f64;
let nx = (2.5 / h).round() as usize;
let g = Grid::cubic(nx, ny, ny, h);
let cyl = |x: f64, y: f64| ((x - 0.2_f64).powi(2) + (y - 0.2_f64).powi(2)).sqrt() - 0.05;
let body = Body::from_sdf(move |x, y, z, _t| cyl(x, y).min(flag_3d(x, y, z, 0.3, h)));
let b = Boundaries {
x1: Side::PressureOutlet,
..Boundaries::default()
};
let t0 = std::time::Instant::now();
let _cut = CutGeometry::build(&body, g, 0.0);
let t_cut = t0.elapsed().as_secs_f64();
let t1 = std::time::Instant::now();
let nodes = (nx + 1) * (ny + 1) * (ny + 1);
let mut acc = 0.0;
for n in 0..nodes {
let (k, j, i) = (
n / ((nx + 1) * (ny + 1)),
(n / (nx + 1)) % (ny + 1),
n % (nx + 1),
);
acc += body.phi(i as f64 * h, j as f64 * h, k as f64 * h, 0.0);
}
let t_phi = t1.elapsed().as_secs_f64();
let t2 = std::time::Instant::now();
let mask = Mask::build_cut(&body, g, 0.0, b).expect("mask");
let t_mask = t2.elapsed().as_secs_f64();
let t3 = std::time::Instant::now();
let (_table, _) = mask.wall_flux_table(&body, 0.0);
let t_flux = t3.elapsed().as_secs_f64();
println!(
" ny 62 rebuild parts: cut geometry {t_cut:.2} s (of which φ at {nodes} nodes {t_phi:.2} s), whole mask build {t_mask:.2} s, wall-flux table {t_flux:.2} s (Σφ {acc:.1})"
);
}
/// The narrow band is invisible to the solution: the moving circle
/// marched with and without `max_surface_speed` gives bit-identical
/// fields; the flag body's rebuild time at ny 62 with the band recorded.
#[test]
fn narrow_band_is_bit_identical_and_fast() {
use rtx_cfd::solvers::incompressible::ConvectionScheme;
use rtx_cfd::solvers::incompressible::embedded3::{
CutGeometry, Field, Fluid, Parameters, Solver, WallScheme,
};
let n = 76;
let h = 1.0 / n as f64;
let dt = 3.24e-4;
let run = |band: bool| {
let mut solver = Solver::new(
Fluid {
density: 1000.0,
viscosity: 1.0,
reference_velocity: 1.0,
reference_length: 0.1,
},
Parameters {
corrector_steps: 2,
tolerance: 1e-8,
convection_scheme: ConvectionScheme::Upwind,
wall_scheme: WallScheme::CutCell,
boundaries: Boundaries {
z0: Side::Periodic,
z1: Side::Periodic,
..Boundaries::default()
},
max_surface_speed: band.then_some(1.0),
..Parameters::default()
},
);
solver.set_boundary_velocity(|_, _, _, _| (0.0, 0.0, 0.0));
let yc = |t: f64| 0.5 + 0.08 * (t / 0.08).sin();
let vc = |t: f64| (t / 0.08).cos();
solver.set_moving_body(
Body::from_sdf(move |x, y, _z, t| {
((x - 0.5_f64).powi(2) + (y - yc(t)).powi(2)).sqrt() - 0.05
})
.with_surface_velocity(move |_, _, _, t| (0.0, vc(t), 0.0)),
);
let g = Grid::cubic(n, n, 4, h);
let mut f = Field::new(g);
solver.initialize(&mut f);
let start = std::time::Instant::now();
for _ in 0..40 {
solver.advance(&mut f, dt);
}
(f, start.elapsed().as_secs_f64())
};
let (full, t_full) = run(false);
let (band, t_band) = run(true);
let same = full.u == band.u && full.v == band.v && full.w == band.w && full.p == band.p;
println!(
" moving circle 76²×4, 40 steps: full {t_full:.2} s, band {t_band:.2} s; fields bit-identical: {same}"
);
assert!(same, "the narrow band changed the solution");
// The flag body at ny 62: a full build, then a banded rebuild after a small motion.
let ny = 62;
let hh = H / ny as f64;
let nx = (2.5 / hh).round() as usize;
let g = Grid::cubic(nx, ny, ny, hh);
let cyl = |x: f64, y: f64| ((x - 0.2_f64).powi(2) + (y - 0.2_f64).powi(2)).sqrt() - 0.05;
let body = Body::from_sdf(move |x, y, z, t| cyl(x, y).min(flag_3d(x, y, z, t, hh)));
let t0 = std::time::Instant::now();
let prev = CutGeometry::build(&body, g, 0.3);
let t_prev = t0.elapsed().as_secs_f64();
let t1 = std::time::Instant::now();
let next = CutGeometry::build_from(&body, g, 0.31, Some((&prev, 3.0 * hh, 1.0e-3)));
let t_next = t1.elapsed().as_secs_f64();
let exact = CutGeometry::build(&body, g, 0.31);
let vol_same = next.vol == exact.vol
&& next.a_u == exact.a_u
&& next.a_v == exact.a_v
&& next.a_w == exact.a_w;
println!(
" flag ny 62: full build {t_prev:.2} s, banded rebuild {t_next:.2} s; volumes and apertures identical to a full build: {vol_same}"
);
assert!(vol_same);
}
/// The per-step operator cost at ny 62 (host side of the device CG's
/// rebuild: the problem, the fine level, the components, the hierarchy
/// export) — S2-2b-ii's target.
#[test]
#[ignore = "profile: the operator export at ny 62 (seconds)"]
fn operator_export_profile_at_ny_62() {
use rtx_cfd::solvers::incompressible::embedded3::poisson::export::export_hierarchy;
use rtx_cfd::solvers::incompressible::embedded3::{
Field, Fluid, Parameters, Solver, WallScheme,
};
use rtx_cfd::solvers::incompressible::{ConvectionScheme, MultigridParameters};
let ny = 62;
let h = H / ny as f64;
let nx = (2.5 / h).round() as usize;
let g = Grid::cubic(nx, ny, ny, h);
let cyl = |x: f64, y: f64| ((x - 0.2_f64).powi(2) + (y - 0.2_f64).powi(2)).sqrt() - 0.05;
let body = Body::from_sdf(move |x, y, z, _t| cyl(x, y).min(flag_3d(x, y, z, 0.3, h)));
let mut solver = Solver::new(
Fluid {
density: 1000.0,
viscosity: 1.0,
reference_velocity: 1.0,
reference_length: 0.1,
},
Parameters {
wall_scheme: WallScheme::CutCell,
convection_scheme: ConvectionScheme::TvdVanAlbada,
boundaries: Boundaries {
x1: Side::PressureOutlet,
..Boundaries::default()
},
..Parameters::default()
},
);
solver.set_body(body);
let mut f = Field::new(g);
let t0 = std::time::Instant::now();
solver.initialize(&mut f);
let t_init = t0.elapsed().as_secs_f64();
let t1 = std::time::Instant::now();
let problem = solver.poisson_operator(g, 1e-3);
let t_op = t1.elapsed().as_secs_f64();
let t2 = std::time::Instant::now();
let levels = export_hierarchy(&problem, &MultigridParameters::default());
let t_exp = t2.elapsed().as_secs_f64();
println!(
" ny 62 operator: initialize (mask + impose) {t_init:.2} s, poisson_operator {t_op:.2} s, export_hierarchy {t_exp:.2} s ({} levels, {} links)",
levels.len(),
problem.links.len()
);
}
/// The residual clause's remedy, measured on the moving circle (host):
/// the worst mass residual over 100 steps with two correctors at the
/// 1e-2 inner stop against three correctors at 1e-3.
#[test]
#[ignore = "residual study on the moving circle (host, a minute)"]
fn moving_circle_residual_remedy() {
use rtx_cfd::solvers::incompressible::ConvectionScheme;
use rtx_cfd::solvers::incompressible::embedded3::{
Field, Fluid, Parameters, Solver, WallScheme,
};
let n = 152;
let h = 1.0 / n as f64;
let dt = 3.24e-4;
for (correctors, inner) in [(2usize, 1e-2), (3, 1e-3), (4, 1e-4)] {
let mut solver = Solver::new(
Fluid {
density: 1000.0,
viscosity: 1.0,
reference_velocity: 1.0,
reference_length: 0.1,
},
Parameters {
corrector_steps: correctors,
tolerance: 1e-8,
inner_stop_factor: inner,
convection_scheme: ConvectionScheme::Upwind,
wall_scheme: WallScheme::CutCell,
boundaries: Boundaries {
z0: Side::Periodic,
z1: Side::Periodic,
..Boundaries::default()
},
max_surface_speed: Some(1.0),
..Parameters::default()
},
);
solver.set_boundary_velocity(|_, _, _, _| (0.0, 0.0, 0.0));
let yc = |t: f64| 0.5 + 0.08 * (t / 0.08).sin();
let vc = |t: f64| (t / 0.08).cos();
solver.set_moving_body(
Body::from_sdf(move |x, y, _z, t| {
((x - 0.5_f64).powi(2) + (y - yc(t)).powi(2)).sqrt() - 0.05
})
.with_surface_velocity(move |_, _, _, t| (0.0, vc(t), 0.0)),
);
let g = Grid::cubic(n, n, 4, h);
let mut f = Field::new(g);
solver.initialize(&mut f);
let (mut worst, mut sum_cg, mut sum_corr) = (0.0_f64, 0usize, 0usize);
let start = std::time::Instant::now();
for _ in 0..100 {
let r = solver.advance(&mut f, dt);
worst = worst.max(r.final_residual);
sum_cg += r.poisson_iterations;
sum_corr += r.corrector_steps_performed;
}
println!(
" correctors {correctors} inner {inner:.0e}: worst residual {worst:.2e}, CG {:.1}/step, correctors {:.2}/step, {:.2} s",
sum_cg as f64 / 100.0,
sum_corr as f64 / 100.0,
start.elapsed().as_secs_f64()
);
}
}