Files
rustytorch/crates/specialized/rtx-cfd/tests/embedded3_flag_geometry.rs
T
Omar SobhandClaude Fable 5.1 11d856b3d4
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 / Build (macos-latest) (push) Waiting to run
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
Performance Benchmarks / Run Benchmarks (push) Failing after 4s
CI / Format Check (push) Failing after 4s
CI / Build CPU-Only (Explicit) (push) Failing after 3s
Documentation / Build User Guide (push) Successful in 4s
Documentation / Build API Documentation (push) Failing after 19s
CI / Clippy Check (push) Failing after 33s
CI / Build (ubuntu-latest) (push) Failing after 1m21s
embedded3: operator export profile at ny 62 (poisson_operator 0.03 s, export_hierarchy 1.44 s); export and poisson_operator public for profiling
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-09-17 19:44:17 -05:00

252 lines
9.9 KiB
Rust

//! 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;
const FLAG_X0: f64 = 0.6;
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()
);
}