Files
rustytorch/crates/specialized/rtx-cfd/tests/overset_added_mass.rs
T
Omar SobhandClaude Fable 5.1 33534ee688
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
Performance Benchmarks / Run Benchmarks (push) Canceled after 0s
CI / Format Check (push) Canceled after 0s
CI / Clippy Check (push) Canceled after 0s
Documentation / Build API Documentation (push) Canceled after 0s
Documentation / Build User Guide (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 / Distributed Training Tests (push) Canceled after 0s
CI / CI Success (push) Canceled after 0s
overset added-mass pin (P5-3): a no-slip cylinder on the moving patch oscillating in a closed box of still fluid, the force fitted over the last two periods — C_m = 1.069 (n = 32) / 1.097 (n = 64) against Stokes's 1.064 plus box blockage: the composite's unsteady pressure response is right, the FSI2 lift excess is not the unsteady term's scale
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_0116sg1Qz1gMv9hdcKP1XUam
2026-09-09 18:01:53 -07:00

207 lines
8.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.
//! P5-3's added-mass pin (`docs/overset_metal_campaign.md`, the replay-to-
//! replay reading): the composite's UNSTEADY pressure response on the
//! moving patch against potential theory.
//!
//! A no-slip circular cylinder of radius `r` on an O-grid patch translates
//! through a closed box of still fluid, `x_c(t) = A sin ωt`, the patch
//! carried with it every step (`set_patch_mesh`, the A-P2 moving path)
//! and the wall carrying the exact velocity. 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 / √(π β)` with
//! `β = r² ω / ν` and a small blockage term for the box. The FSI2
//! ladder's motion-fixed lift response is ~1.4× the reference's at every
//! h and dt (2026-09-09); nothing rigid (CFD13, all within 1 %) can see
//! the unsteady scale, this can. Registered before the run: `C_m` in
//! [1.0, 1.25] at n = 64 means the unsteady scale is right and the
//! discrepancy is the wake's; `C_m ≥ 1.4` locates it here.
//!
//! `RTX_OVERSET_N` (64), `RTX_OVERSET_AM_PERIODS` (4).
use rtx_cfd::mesh::patch_gen::annulus_skewed;
use rtx_cfd::mesh::{PatchMesh, PatchSide};
use rtx_cfd::solvers::incompressible::{
CurvilinearParameters, CurvilinearPisoSolver, EmbeddedParameters, EmbeddedPisoSolver,
FlowField, NormalDiffusion, OversetField, OversetParameters, OversetPisoSolver, PatchField,
PoissonSolverKind,
};
use rtx_cfd::{CfdConfig, CfdResult};
use std::f64::consts::PI;
const RHO: f64 = 1.0;
const NU: f64 = 5e-5;
const R: f64 = 0.1;
const R_OUT: f64 = 0.25;
const CX: f64 = 0.5;
const CY: f64 = 0.5;
const AMP: f64 = 0.005;
const OMEGA: f64 = 2.0 * PI;
fn patch_at(n: usize, x: f64) -> CfdResult<PatchMesh> {
annulus_skewed([CX + x, CY], R, R_OUT, 9 * n / 4, n / 4, 0.0, 3.0)
}
fn env_usize(k: &str, d: usize) -> usize {
std::env::var(k)
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(d)
}
#[tokio::test]
async fn oscillating_cylinder_added_mass_on_the_moving_patch() -> CfdResult<()> {
let n = env_usize("RTX_OVERSET_N", 64);
let periods = env_usize("RTX_OVERSET_AM_PERIODS", 4);
let h = 1.0 / n as f64;
let config = CfdConfig::new()
.with_density(RHO)
.with_viscosity(RHO * NU)
.with_reference_velocity(AMP * OMEGA)
.with_reference_length(2.0 * R);
let mut background = EmbeddedPisoSolver::new(
config.clone(),
EmbeddedParameters {
corrector_steps: 2,
tolerance: 1e-8,
poisson_solver: PoissonSolverKind::Multigrid,
..EmbeddedParameters::default()
},
)?;
background.set_boundary_velocity(|_, _, _| (0.0, 0.0));
let mut patch = CurvilinearPisoSolver::new(
config,
CurvilinearParameters {
tolerance: 1e-6,
normal_diffusion: NormalDiffusion::LineImplicit,
..CurvilinearParameters::default()
},
patch_at(n, 0.0)?,
)?;
// The wall's exact velocity: the patch's clock is the end of the step.
patch.set_side_velocity(PatchSide::Inner, |_, _, t| {
(AMP * OMEGA * (OMEGA * t).cos(), 0.0)
});
let mut patch_field = PatchField::new(patch.mesh());
patch.initialize(&mut patch_field, |_, _| (0.0, 0.0));
let bg_field = FlowField::new(n, n, h, h)?;
let params = OversetParameters {
stall_rounds: 0,
..OversetParameters::default()
};
let mut solver = OversetPisoSolver::new(background, patch, (n, n, h, h), params)?;
let mut field = OversetField {
background: bg_field,
patch: patch_field,
};
solver.initialize(&mut field)?;
// Step: the patch's smallest edge against the explicit limits, and the
// wall's own displacement per step well under a cell.
let mut hp = f64::INFINITY;
for c in 0..solver.patch().mesh().cell_count() {
for (f, _) in solver.patch().mesh().cell_faces(c) {
let d = solver.patch().mesh().faces()[f].d;
hp = hp.min((d[0] * d[0] + d[1] * d[1]).sqrt());
}
}
let period = 2.0 * PI / OMEGA;
let dt_raw = 0.4 * (hp * hp / (4.0 * NU)).min(h).min(0.2 * hp / (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;
println!(
" n = {n}, h = {h:.4}, patch inner edge {hp:.4}, dt = {dt:.3e} ({steps_per_period} per period), A/r = {:.3}, KC = {:.3}, β = {beta:.0}: Stokes C_m ≈ {:.3}",
AMP / R,
2.0 * PI * AMP / R,
1.0 + 4.0 / (PI * beta).sqrt()
);
let mut samples: Vec<(f64, f64, f64, f64)> = Vec::new();
let mut rounds_total = 0usize;
let mut reclass_total = 0usize;
let start = std::time::Instant::now();
for step in 0..periods * steps_per_period {
let t_new = (step + 1) as f64 * dt;
solver.set_patch_mesh(patch_at(n, AMP * (OMEGA * t_new).sin())?)?;
let r = solver.advance(&mut field, dt).await?;
rounds_total += r.rounds.iter().sum::<usize>();
reclass_total += r.reclassified_cells;
let load = solver
.patch()
.surface_force(&field.patch, PatchSide::Inner, solver.time());
let f = load.total();
samples.push((t_new, f[0], load.pressure[0], load.viscous[0]));
if (step + 1) % steps_per_period == 0 {
println!(
" period {}: F_x at the end {:+.4e} (pressure {:+.4e}, viscous {:+.4e}), rounds {:.2}/step, reclassified {:.1}/step, {:.0} s",
(step + 1) / steps_per_period,
f[0],
load.pressure[0],
load.viscous[0],
rounds_total as f64 / (step + 1) as f64,
reclass_total as f64 / (step + 1) as f64,
start.elapsed().as_secs_f64()
);
}
}
// Least squares over the last two periods: F = a sin ωt + b cos ωt (+ c).
// ẍ = A ω² sin ωt, so the added-mass reaction m_a ẍ = m_a A ω² sin ωt:
// C_m = a / (ρ π r² A ω²). ẋ = A ω cos ωt, so b = c_d A ω (damping).
let fit = |col: usize| -> (f64, f64, f64) {
let last: Vec<&(f64, f64, f64, f64)> = samples
.iter()
.filter(|s| s.0 > (periods as f64 - 2.0) * period - 1e-12)
.collect();
let g = |s: &(f64, f64, f64, f64)| match col {
1 => s.1,
2 => s.2,
_ => s.3,
};
// Normal equations for [sin, cos, 1].
let mut m = [[0.0f64; 3]; 3];
let mut rhs = [0.0f64; 3];
for s in &last {
let b = [(OMEGA * s.0).sin(), (OMEGA * s.0).cos(), 1.0];
for i in 0..3 {
rhs[i] += b[i] * g(s);
for j in 0..3 {
m[i][j] += b[i] * b[j];
}
}
}
// 3×3 solve by Cramer's rule.
let det = |a: [[f64; 3]; 3]| {
a[0][0] * (a[1][1] * a[2][2] - a[1][2] * a[2][1])
- a[0][1] * (a[1][0] * a[2][2] - a[1][2] * a[2][0])
+ a[0][2] * (a[1][0] * a[2][1] - a[1][1] * a[2][0])
};
let d = det(m);
let mut sol = [0.0; 3];
for k in 0..3 {
let mut mk = m;
for i in 0..3 {
mk[i][k] = rhs[i];
}
sol[k] = det(mk) / d;
}
(sol[0], sol[1], sol[2])
};
let (a, b, c) = fit(1);
let (ap, bp, _) = fit(2);
let (av, bv, _) = fit(3);
let m_a = RHO * PI * R * R;
let cm = a / (m_a * AMP * OMEGA * OMEGA);
let cm_p = ap / (m_a * AMP * OMEGA * OMEGA);
let cm_v = av / (m_a * AMP * OMEGA * OMEGA);
let damping = -b / (AMP * OMEGA);
println!(
" ADDED MASS n = {n}: C_m = {cm:.4} (pressure {cm_p:.4} + viscous {cm_v:.4}) against 1 + Stokes {:.3}; damping coefficient {damping:.4e} (pressure {:+.3e}, viscous {:+.3e}) [N·s/m per m]; mean force {c:+.3e}; {} steps in {:.0} s",
1.0 + 4.0 / (PI * beta).sqrt(),
-bp / (AMP * OMEGA),
-bv / (AMP * OMEGA),
samples.len(),
start.elapsed().as_secs_f64()
);
assert!(cm.is_finite(), "non-finite added-mass coefficient");
Ok(())
}