Files
rustytorch/crates/specialized/rtx-cfd/tests/embedded3_added_mass.rs
T
Omar SobhandClaude Fable 5.1 83922974d7
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 / Format Check (push) Failing after 3s
CI / CI Success (push) Blocked by required conditions
Performance Benchmarks / Run Benchmarks (push) Failing after 4s
CI / Build (ubuntu-latest) (push) Failing after 4s
CI / Clippy Check (push) Failing after 4s
Documentation / Build User Guide (push) Successful in 5s
CI / Build CPU-Only (Explicit) (push) Failing after 58s
Documentation / Build API Documentation (push) Failing after 59s
R2-b/R3: embedded3_added_mass (the cut-cell added-mass pin, the overset instrument's parameters: n 64/128 C_m 1.087/1.127 vs the confined potential-flow 1.071 + Stokes 0.064 = 1.135, the overset's 1.097/1.127); flag tests: the capsule's tip inset (RTX_E3_FLAG_TIP_INSET, default FLAG_HALF — the apex ON the benchmark's tip A; every earlier record had it 10 mm beyond, =0 restores), root fillet knob RTX_E3_FLAG_FILLET (5 mm: no effect), the tip CSV column = the record's tip in recorded mode
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-09-21 19:33:59 -05:00

181 lines
7.0 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.
//! R2-b: the cut-cell added-mass pin — the overset's instrument
//! (`overset_added_mass.rs`) rebuilt on embedded3.
//!
//! A no-slip circular cylinder of radius `R` translates through a closed
//! box of still fluid, `x_c(t) = A sin ωt`, on the periodic slab (nz cells).
//! The in-phase force is the added-mass reaction `F = m_a ẍ` with
//! `m_a = ρ π R²` (unbounded potential flow) plus Stokes's viscous
//! correction `4 / √(π β)`, `β = R² ω / ν`, plus a small blockage term for
//! the box. The operator route's F_x is fitted to `c_s sin ωt + c_c cos ωt +
//! c_0` over the last two of `periods` periods: `C_m = c_s / (ρ π R² A ω²)`
//! (with `x_c = A sin ωt`, `m_a ẍ = m_a A ω² sin ωt`), the viscous part
//! `c_c / (ρ π R² A ω²)` against `4 / √(π β)`.
//!
//! Knobs: `RTX_E3_AM_N` (64), `RTX_E3_AM_NZ` (4), `RTX_E3_AM_PERIODS` (4),
//! `RTX_E3_AM_SCHEME` (tvd | upwind), `RTX_E3_AM_CSV=<path>`.
use rtx_cfd::solvers::incompressible::ConvectionScheme;
use rtx_cfd::solvers::incompressible::embedded3::{
Body, Boundaries, Field, Fluid, Grid, Parameters, Side, Solver, WallScheme,
};
use std::f64::consts::PI;
use std::io::Write as _;
const RHO: f64 = 1.0;
const NU: f64 = 5e-5;
const R: f64 = 0.1;
const CX: f64 = 0.5;
const CY: f64 = 0.5;
const AMP: f64 = 0.005;
const OMEGA: f64 = 2.0 * PI;
fn env_f(name: &str, default: f64) -> f64 {
std::env::var(name)
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(default)
}
/// Least squares of `y ≈ c_s sin ωt + c_c cos ωt + c_0`.
fn fit(samples: &[(f64, f64)]) -> (f64, f64, f64) {
let mut m = [[0.0f64; 3]; 3];
let mut rhs = [0.0f64; 3];
for &(t, y) in samples {
let b = [(OMEGA * t).sin(), (OMEGA * t).cos(), 1.0];
for i in 0..3 {
rhs[i] += b[i] * y;
for j in 0..3 {
m[i][j] += b[i] * b[j];
}
}
}
// Gaussian elimination with partial pivoting.
let mut a = [[0.0f64; 4]; 3];
for i in 0..3 {
a[i][..3].copy_from_slice(&m[i]);
a[i][3] = rhs[i];
}
for i in 0..3 {
let piv = (i..3).max_by(|&p, &q| a[p][i].abs().total_cmp(&a[q][i].abs())).unwrap();
a.swap(i, piv);
for k in 0..3 {
if k != i {
let f = a[k][i] / a[i][i];
for j in 0..4 {
a[k][j] -= f * a[i][j];
}
}
}
}
(a[0][3] / a[0][0], a[1][3] / a[1][1], a[2][3] / a[2][2])
}
#[test]
#[ignore = "R2-b: the cut-cell added-mass pin (minutes per rung on the host)"]
fn cut_cell_added_mass() {
let n = env_f("RTX_E3_AM_N", 64.0) as usize;
let nz = env_f("RTX_E3_AM_NZ", 4.0) as usize;
let periods = env_f("RTX_E3_AM_PERIODS", 4.0) as usize;
let scheme = match std::env::var("RTX_E3_AM_SCHEME").as_deref() {
Ok("upwind") => ConvectionScheme::Upwind,
_ => ConvectionScheme::TvdVanAlbada,
};
let h = 1.0 / n as f64;
let lz = nz as f64 * h;
let period = 2.0 * PI / OMEGA;
// The overset pin's step rule (its patch spacing = h here).
let dt_raw = 0.4 * (h * h / (4.0 * NU)).min(h).min(0.2 * h / (AMP * OMEGA));
let steps_per_period = (period / dt_raw).ceil() as usize;
let dt = period / steps_per_period as f64;
let beta = R * R * OMEGA / NU;
let stokes = 4.0 / (PI * beta).sqrt();
println!(
" cut-cell added mass: n = {n} (R/h {:.1}), nz {nz}, dt = {dt:.3e} ({steps_per_period} per period), A/R = {:.3}, KC = {:.3}, β = {beta:.0}: Stokes C_m ≈ {:.3} (viscous {:.3})",
R / h,
AMP / R,
2.0 * PI * AMP / R,
1.0 + stokes,
stokes
);
let mut solver = Solver::new(
Fluid {
density: RHO,
viscosity: RHO * NU,
reference_velocity: AMP * OMEGA,
reference_length: 2.0 * R,
},
Parameters {
corrector_steps: 3,
inner_stop_factor: 1e-3,
tolerance: 1e-8,
convection_scheme: scheme,
wall_scheme: WallScheme::CutCell,
boundaries: Boundaries {
z0: Side::Periodic,
z1: Side::Periodic,
..Boundaries::default()
},
max_surface_speed: Some(AMP * OMEGA * 1.05),
..Parameters::default()
},
);
solver.set_boundary_velocity(|_, _, _, _| (0.0, 0.0, 0.0));
let xc = |t: f64| CX + AMP * (OMEGA * t).sin();
let uc = |t: f64| AMP * OMEGA * (OMEGA * t).cos();
let body = Body::from_sdf(move |x, y, _z, t| ((x - xc(t)).powi(2) + (y - CY).powi(2)).sqrt() - R)
.with_surface_velocity(move |_x, _y, _z, t| (uc(t), 0.0, 0.0));
solver.set_moving_body(body);
let g = Grid::cubic(n, n, nz, h);
let mut field = Field::new(g);
solver.initialize(&mut field);
let mut csv = std::env::var("RTX_E3_AM_CSV").ok().map(|p| {
let mut f = std::fs::File::create(p).expect("csv");
writeln!(f, "t,xc,fx,fy,fx_rec,residual,fresh").unwrap();
f
});
let mut samples: Vec<(f64, f64, f64)> = Vec::new();
let mut worst = 0.0f64;
let start = std::time::Instant::now();
for step in 0..periods * steps_per_period {
let r = solver.advance(&mut field, dt);
let t = (step + 1) as f64 * dt;
worst = worst.max(r.final_residual);
assert!(r.final_residual.is_finite(), "death at step {step}");
let mask = solver.mask().expect("mask");
let body = solver.body().expect("body");
let f = mask.cut_wall_force(body, &field, RHO * NU, t).expect("cut wall");
let fr = mask
.cut_wall_force_reconstructed(body, &field, RHO * NU, t, None)
.expect("reconstructed");
let (fx, fy, fx_rec) = (f[0] / lz, f[1] / lz, fr[0] / lz);
samples.push((t, fx, fx_rec));
if let Some(c) = csv.as_mut() {
writeln!(c, "{t:.6},{:.6},{fx:.6e},{fy:.6e},{fx_rec:.6e},{:.3e},{}", xc(t), r.final_residual, r.fresh_cells).unwrap();
}
if (step + 1) % steps_per_period == 0 {
println!(
" period {}: F_x at the end {fx:+.4e} (reconstructed {fx_rec:+.4e}), residual {:.1e}, {:.0} s",
(step + 1) / steps_per_period,
r.final_residual,
start.elapsed().as_secs_f64()
);
}
}
let t0 = (periods as f64 - 2.0) * period - 1e-12;
let scale = RHO * PI * R * R * AMP * OMEGA * OMEGA;
let op: Vec<(f64, f64)> = samples.iter().filter(|s| s.0 > t0).map(|s| (s.0, s.1)).collect();
let rec: Vec<(f64, f64)> = samples.iter().filter(|s| s.0 > t0).map(|s| (s.0, s.2)).collect();
let (cs, cc, c0) = fit(&op);
let (cs_r, cc_r, _) = fit(&rec);
println!(
" ADDED MASS n = {n}: C_m operator {:.4} (reconstructed {:.4}) vs Stokes {:.3}; viscous {:.4} (reconstructed {:.4}) vs {:.3}; offset {:+.3e}; worst residual {worst:.1e}; {:.0} s",
cs / scale,
cs_r / scale,
1.0 + stokes,
cc / scale,
cc_r / scale,
stokes,
c0,
start.elapsed().as_secs_f64()
);
}