Files
rustytorch/crates/specialized/rtx-fsi/tests/fsi2_embedded3/fluid.rs
T
Omar SobhandClaude Opus 5.5 12a27c6fca R8-a: span-uniform coupled FSI2 on embedded3 — DeviceStep snapshot/restore, operator-route load sink, harness fsi2_embedded3.rs
- embedded3 DeviceStep::snapshot/restore (step/device/snapshot.rs): the start-of-step
  device fields + the moving body's host records; a restore rebuilds the R6 persistent
  geometry/classification, the predictor tables, the hierarchy and the guess basis.
  Only called by the new harness (existing paths untouched).
- exchange::set_load_sink: every contribution of the operator load route (p W, wall
  shear, diffusive/convective exchange) with position/component/part; None by default,
  the routes' sums unchanged (slab ny 62 CSV byte-identical to r6_1/base_slab62.csv).
- rtx-fsi feature cuda (= rtx-cfd/cuda) and tests/fsi2_embedded3.rs: the 2D rtx-fea flag
  (35x2 Quad8, TL, Newmark 0.7) drives the capsule's centreline per coupled step; the
  route's contributions are distributed onto the wetted nodes per unit span; Aitken
  subiterations each re-run the fluid step from the snapshot.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-25 18:11:27 -05:00

322 lines
11 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.
//! R8-a: the embedded3 3D cut-cell fluid as the fluid side of a
//! partitioned FSI loop. The body is the embedded flag test's (the circle
//! wall to wall united with a capsule of half-thickness 10 mm around the
//! flag's centreline, its apex on the benchmark's tip A), but the
//! centreline is no longer prescribed: the harness sets it per coupled
//! step from the 2D structure (span-uniform; the centreline's element-corner
//! nodes at reference x 0.25 … 0.59, so the capsule's apex sits on A as the
//! flag test's tip inset puts it), as the pair of lines at the
//! step's start and end; the body's φ and surface velocity at any time in
//! between are the linear blend of the two (the solver asks at the step's
//! two ends only).
//!
//! The loads: the operator route (`Mask::cut_wall_force`) with the R8-a
//! load sink installed — every contribution the route sums, with its
//! position, is returned for the harness to distribute onto the flag.
use std::cell::RefCell;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex, RwLock};
use rtx_cfd::solvers::incompressible::ConvectionScheme;
use rtx_cfd::solvers::incompressible::embedded3::exchange::set_load_sink;
use rtx_cfd::solvers::incompressible::embedded3::step::device::{DeviceSnapshot, DeviceStep};
use rtx_cfd::solvers::incompressible::embedded3::{
Body, Boundaries, DeviceSdf, Field, Fluid, Grid, Parameters, Side, Solver, StepResult,
WallScheme,
};
pub const H: f64 = 0.41;
pub const L: f64 = 2.5;
pub const CX: f64 = 0.2;
pub const CY: f64 = 0.2;
pub const R_CYL: f64 = 0.05;
/// The capsule's half-thickness (the flag's half-thickness).
pub const HALF: f64 = 0.01;
pub const RHO: f64 = 1000.0;
pub const NU: f64 = 1e-3;
/// The flag test's CFL velocity (its dt convention, kept for comparability).
const U_CFL: f64 = 2.25;
/// A centreline at time `t`: points (x, y) and their velocities.
#[derive(Clone, Debug)]
pub struct Line {
pub t: f64,
pub pts: Vec<[f64; 2]>,
pub vel: Vec<[f64; 2]>,
}
/// The step's two lines.
pub struct Lines {
pub a: Line,
pub b: Line,
}
impl Lines {
/// The blend at `t` (the start line before `a.t`, the end line after `b.t`).
fn at(&self, t: f64) -> Line {
let eps = 1e-12 * (1.0 + t.abs());
if (t - self.b.t).abs() <= eps || t >= self.b.t || self.b.t <= self.a.t {
return self.b.clone();
}
if t <= self.a.t + eps {
return self.a.clone();
}
let s = (t - self.a.t) / (self.b.t - self.a.t);
let mix = |p: &[[f64; 2]], q: &[[f64; 2]]| -> Vec<[f64; 2]> {
p.iter()
.zip(q)
.map(|(p, q)| [p[0] + s * (q[0] - p[0]), p[1] + s * (q[1] - p[1])])
.collect()
};
assert_eq!(
self.a.pts.len(),
self.b.pts.len(),
"blend needs equal point counts"
);
Line {
t,
pts: mix(&self.a.pts, &self.b.pts),
vel: mix(&self.a.vel, &self.b.vel),
}
}
}
/// Bumped on every `set_lines` (the host closures' per-thread cache key).
static VERSION: AtomicU64 = AtomicU64::new(0);
/// The capsule's signed distance at (x, y) and the centreline velocity at
/// the closest point (the flag test's `flag_2d_recorded`, cached per thread
/// and time).
fn capsule(lines: &RwLock<Lines>, x: f64, y: f64, t: f64) -> (f64, (f64, f64)) {
thread_local! {
static CACHE: RefCell<(u64, u64, Vec<[f64; 4]>)> = const { RefCell::new((u64::MAX, u64::MAX, Vec::new())) };
}
let ver = VERSION.load(Ordering::Acquire);
CACHE.with(|cell| {
let mut c = cell.borrow_mut();
if c.0 != t.to_bits() || c.1 != ver {
let line = lines.read().expect("lines").at(t);
c.2 = line
.pts
.iter()
.zip(&line.vel)
.map(|(p, v)| [p[0], p[1], v[0], v[1]])
.collect();
c.0 = t.to_bits();
c.1 = ver;
}
let pts = &c.2;
let mut best = f64::INFINITY;
let mut v_best = (0.0, 0.0);
for m in 0..pts.len() - 1 {
let [ax, ay, avx, avy] = pts[m];
let [bx, by, bvx, bvy] = pts[m + 1];
let (ex, ey) = (bx - ax, by - ay);
let l2 = ex * ex + ey * ey;
if l2 == 0.0 {
continue;
}
let u = (((x - ax) * ex + (y - ay) * ey) / l2).clamp(0.0, 1.0);
let (px, py) = (ax + u * ex, ay + u * ey);
let d = ((x - px).powi(2) + (y - py).powi(2)).sqrt();
if d < best {
best = d;
v_best = (avx + u * (bvx - avx), avy + u * (bvy - avy));
}
}
(best - HALF, v_best)
})
}
fn cylinder(x: f64, y: f64) -> f64 {
((x - CX).powi(2) + (y - CY).powi(2)).sqrt() - R_CYL
}
/// One load contribution: position, component, part (0 pressure, 1 wall
/// shear, 2 diffusive exchange, 3 convective exchange), force on the body
/// (N, over the whole z extent).
pub type Contribution = ([f64; 3], usize, usize, f64);
pub struct E3Fluid {
pub device: DeviceStep,
pub field: Field,
pub grid: Grid,
pub h: f64,
pub dt: f64,
/// The z extent the loads are divided by (per unit span).
pub width: f64,
pub lines: Arc<RwLock<Lines>>,
sink: Arc<Mutex<Vec<Contribution>>>,
}
impl E3Fluid {
/// The fluid on the benchmark channel at rung `ny`: `nz_slab > 0` the
/// thin slab periodic in z (the flag as a 2D problem), 0 the full 0.41 m
/// duct with slip side walls; the 2D inflow (parabolic in y, Ū 1) in
/// both. `speed` bounds the flag's surface speed (the narrow band).
pub fn build(ny: usize, nz_slab: usize, speed: f64, rest: Line) -> Self {
let h = H / ny as f64;
let nx = (L / h).round() as usize;
let nz = if nz_slab > 0 {
nz_slab
} else {
(H / h).round() as usize
};
// The flag test's step (its CFL velocity; `speed` is the band's bound only).
let dt = (0.3 * h / U_CFL).min(0.5 * h * h / (6.0 * NU))
* super::env_f("RTX_E3FSI_DT_SCALE", 1.0);
let boundaries = if nz_slab > 0 {
Boundaries {
x1: Side::PressureOutlet,
z0: Side::Periodic,
z1: Side::Periodic,
..Boundaries::default()
}
} else {
Boundaries {
x1: Side::PressureOutlet,
z0: Side::SlipWall,
z1: Side::SlipWall,
..Boundaries::default()
}
};
let mut solver = Solver::new(
Fluid {
density: RHO,
viscosity: RHO * NU,
reference_velocity: 1.0,
reference_length: 2.0 * R_CYL,
},
Parameters {
corrector_steps: super::env_f("RTX_E3FSI_CORRECTORS", 3.0) as usize,
inner_stop_factor: super::env_f("RTX_E3FSI_INNER", 1e-3),
tolerance: 1e-8,
convection_scheme: ConvectionScheme::TvdVanAlbada,
wall_scheme: WallScheme::CutCell,
boundaries,
max_surface_speed: Some(speed),
..Parameters::default()
},
);
let inflow = |y: f64| 6.0 * y * (H - y) / (H * H);
solver.set_boundary_velocity(move |x, y, _z, _t| {
if x <= 0.0 {
(inflow(y), 0.0, 0.0)
} else {
(0.0, 0.0, 0.0)
}
});
let lines = Arc::new(RwLock::new(Lines {
a: rest.clone(),
b: rest,
}));
VERSION.fetch_add(1, Ordering::AcqRel);
let (l1, l2, l3) = (lines.clone(), lines.clone(), lines.clone());
let body = Body::from_sdf(move |x, y, _z, t| cylinder(x, y).min(capsule(&l1, x, y, t).0))
.with_surface_velocity(move |x, y, _z, t| {
let (df, (vx, vy)) = capsule(&l2, x, y, t);
if df <= cylinder(x, y) {
(vx, vy, 0.0)
} else {
(0.0, 0.0, 0.0)
}
});
let width = nz as f64 * h;
let body = body.with_device_sdf(move |t| {
let line = l3.read().expect("lines").at(t);
DeviceSdf {
cyl: [CX, CY, R_CYL],
cyl_cut: false,
flag_cut: false,
zc: 0.5 * width,
span: width,
r_edge: h,
half: HALF,
fillet: 0.0,
poly: line.pts,
vel: line.vel,
}
});
solver.set_moving_body(body);
let g = Grid::cubic(nx, ny, nz, h);
let mut field = Field::new(g);
for k in 0..nz {
for j in 0..ny {
let u0 = inflow((j as f64 + 0.5) * h);
for i in 0..=nx {
field.u[g.uface(k, j, i)] = u0;
}
}
}
solver.initialize(&mut field);
let mut device = DeviceStep::new(solver, g);
device.upload(&field);
println!(
" R8-a fluid: ny {ny}, {nx}×{ny}×{nz} = {} cells ({}), h {h:.4e}, dt {dt:.4e}, speed bound {speed} m/s",
g.cells(),
if nz_slab > 0 {
"slab, periodic z"
} else {
"full duct, slip sides"
}
);
Self {
device,
field,
grid: g,
h,
dt,
width,
lines,
sink: Arc::new(Mutex::new(Vec::new())),
}
}
pub fn time(&self) -> f64 {
self.device.solver.time()
}
/// The step's start and end lines (their times are the lines' own).
pub fn set_lines(&self, a: Line, b: Line) {
*self.lines.write().expect("lines") = Lines { a, b };
VERSION.fetch_add(1, Ordering::AcqRel);
}
pub fn step(&mut self) -> StepResult {
self.device.advance(self.dt)
}
pub fn snapshot(&mut self) -> DeviceSnapshot {
self.device.snapshot()
}
pub fn restore(&mut self, snap: &DeviceSnapshot) {
self.device.restore(snap);
}
/// The operator-route force on the whole body per unit span and the
/// route's contributions (N over the whole z extent).
pub fn loads(&mut self) -> ([f64; 3], Vec<Contribution>) {
self.device.download(&mut self.field);
let t = self.time();
self.sink.lock().expect("sink").clear();
let s = self.sink.clone();
let previous = set_load_sink(Some(Box::new(move |pos, c, part, v| {
s.lock().expect("sink").push((pos, c, part, v));
})));
assert!(previous.is_none(), "a load sink was already installed");
let mask = self.device.solver.mask().expect("mask");
let body = self.device.solver.body().expect("body");
let f = mask
.cut_wall_force(body, &self.field, RHO * NU, t)
.expect("cut wall force");
set_load_sink(None);
let contributions = std::mem::take(&mut *self.sink.lock().expect("sink"));
(
[f[0] / self.width, f[1] / self.width, f[2] / self.width],
contributions,
)
}
}