Files
rustytorch/crates/specialized/rtx-cfd/tests/embedded3_added_mass.rs
T
Omar SobhandClaude Fable 5.1 847502f7c3
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) (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 / Python Bindings (maturin) (macos-latest) (push) Blocked by required conditions
CI / Build CPU-Only (Explicit) (push) Failing after 3s
Documentation / Build API Documentation (push) Failing after 4s
Documentation / Build User Guide (push) Successful in 5s
CI / Format Check (push) Failing after 13s
CI / Build (ubuntu-latest) (push) Failing after 1m34s
CI / Clippy Check (push) Failing after 1m45s
Performance Benchmarks / Run Benchmarks (push) Successful in 2m15s
R2-b: the added-mass pins print the confined target (potential flow 1.071 in the unit box + Stokes 0.064 = 1.135); R4 probe: RTX_FSI2O_STEP_CSV — every step's raw force with the accepted fluid step's overlap defects (the march CSV carries 10-step medians)
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-09-21 20:22:15 -05:00

181 lines
7.1 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} unbounded / 1.135 confined (potential flow 1.071 in the unit box + 0.064); 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()
);
}