P5-1: FSI2 with the fluid on the overset — fsi2_harness/overset.rs (the background without a body, the cylinder–flag patch regenerated around the deformed flag every pass via set_patch_mesh, the wall velocity from the interface velocities along the wetted polygon, the load from the patch's wall faces into WettedSurface::transfer_load — no probes, no clamp, no smoothing), fsi2_harness/overset_march.rs (the harness's rigid phase / release / subiterated coupling with its acceptance rule, no rescue machinery, death returned not panicked), tests/turek_hron_fsi2_overset.rs (RTX_FSI2O_* knobs); rtx-cfd: CurvilinearPisoSolver::wall_tractions (per-face pressure + full-stress traction, surface_force sums the same terms bit-identically); Interface::edges (bottom/tip/top for the generator); cylinder_flag_mms RTX_CF_BEND (P5-0 gate iv: Stokes orders 2.14 / 2.09 on the flag bent to 80 mm)
Documentation / Build API Documentation (push) Canceled after 0s
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
Documentation / Build User Guide (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
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
Documentation / Build API Documentation (push) Canceled after 0s
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
Documentation / Build User Guide (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
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
Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_0116sg1Qz1gMv9hdcKP1XUam
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
c2451fbacf
commit
f0b2563bf8
@@ -11,23 +11,25 @@
|
||||
#![allow(dead_code)] // several test crates share this; each uses a subset
|
||||
|
||||
pub mod march;
|
||||
pub mod overset;
|
||||
pub mod overset_march;
|
||||
pub mod rescue;
|
||||
|
||||
use std::cell::Cell;
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use nalgebra::Vector3;
|
||||
use rtx_cfd::CfdConfig;
|
||||
use rtx_cfd::solvers::incompressible::{
|
||||
AleBoundaries, ConvectionScheme, EmbeddedBody, EmbeddedParameters, EmbeddedPisoSolver,
|
||||
FlowField, PoissonSolverKind, PolygonSdf, SideBoundary, polygon_interface_velocity,
|
||||
polygon_signed_distance,
|
||||
polygon_interface_velocity, polygon_signed_distance, AleBoundaries, ConvectionScheme,
|
||||
EmbeddedBody, EmbeddedParameters, EmbeddedPisoSolver, FlowField, PoissonSolverKind, PolygonSdf,
|
||||
SideBoundary,
|
||||
};
|
||||
use rtx_cfd::CfdConfig;
|
||||
use rtx_fea::assembly::dof_mapping::DofComponent;
|
||||
use rtx_fea::boundary::dirichlet::{DirichletBC, DirichletType};
|
||||
use rtx_fea::boundary::{BoundaryCondition, BoundaryConditionSet, SpatialFunction};
|
||||
use rtx_fea::mesh::{Element, ElementType, MaterialId, Mesh, Node, NodeId};
|
||||
use rtx_fsi::{FluidFace, WettedSurface, smooth_tractions};
|
||||
use rtx_fsi::{smooth_tractions, FluidFace, WettedSurface};
|
||||
|
||||
pub const L: f64 = 2.5;
|
||||
pub const H: f64 = 0.41;
|
||||
@@ -183,6 +185,12 @@ pub struct Interface {
|
||||
/// Ordered boundary walk: indices into `wetted` (`usize::MAX` marks
|
||||
/// the fixed anchor vertices inside the cylinder / at the clamp).
|
||||
pub walk: Vec<(usize, (f64, f64))>,
|
||||
/// The three wetted edges as indices into `wetted`: bottom (root →
|
||||
/// tip), tip (bottom corner → top corner, corners included), top
|
||||
/// (tip → root) — the P5 patch generator's input.
|
||||
pub bottom: Vec<usize>,
|
||||
pub tip: Vec<usize>,
|
||||
pub top: Vec<usize>,
|
||||
}
|
||||
|
||||
impl Interface {
|
||||
@@ -246,10 +254,20 @@ impl Interface {
|
||||
walk.push((usize::MAX, (0.22, FLAG_Y1)));
|
||||
|
||||
let reference = wetted.iter().map(|(_, p)| *p).collect();
|
||||
let bottom_idx: Vec<usize> = bottom.iter().map(|(id, _)| index_of(*id)).collect();
|
||||
// The tip edge with its corners: the last bottom node, the tip's
|
||||
// interior nodes (sorted by y), the first top node.
|
||||
let mut tip_idx: Vec<usize> = vec![*bottom_idx.last().expect("bottom edge")];
|
||||
tip_idx.extend(tip.iter().map(|(id, _)| index_of(*id)));
|
||||
let top_idx: Vec<usize> = top.iter().map(|(id, _)| index_of(*id)).collect();
|
||||
tip_idx.push(*top_idx.first().expect("top edge"));
|
||||
Self {
|
||||
wetted: wetted.into_iter().map(|(id, _)| id).collect(),
|
||||
reference,
|
||||
walk,
|
||||
bottom: bottom_idx,
|
||||
tip: tip_idx,
|
||||
top: top_idx,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,6 +301,21 @@ impl Interface {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The deformed wetted edges for the P5 patch generator, each edge
|
||||
/// starting / ending at the clamp line's fixed corner.
|
||||
pub fn edges(&self, d: &[f64]) -> rtx_cfd::mesh::patch_gen::FlagEdges {
|
||||
let at = |k: usize| -> [f64; 2] {
|
||||
let (x0, y0) = self.reference[k];
|
||||
[x0 + d[2 * k], y0 + d[2 * k + 1]]
|
||||
};
|
||||
let mut bottom = vec![[FLAG_X0, FLAG_Y0]];
|
||||
bottom.extend(self.bottom.iter().map(|&k| at(k)));
|
||||
let tip: Vec<[f64; 2]> = self.tip.iter().map(|&k| at(k)).collect();
|
||||
let mut top: Vec<[f64; 2]> = self.top.iter().map(|&k| at(k)).collect();
|
||||
top.push([FLAG_X0, FLAG_Y1]);
|
||||
rtx_cfd::mesh::patch_gen::FlagEdges { bottom, tip, top }
|
||||
}
|
||||
|
||||
/// Deformed wetted node positions for the transfer.
|
||||
pub fn deformed_nodes(&self, d: &[f64]) -> Vec<Vector3<f64>> {
|
||||
self.reference
|
||||
|
||||
@@ -0,0 +1,407 @@
|
||||
//! P5 (`docs/overset_metal_campaign.md` §5.12): the FSI2 harness's FLUID
|
||||
//! SIDE on the overset — the background without a body, the cylinder–flag
|
||||
//! O-grid regenerated around the deformed flag every time the interface
|
||||
//! moves (`set_patch_mesh`: the overlap rebuilt, fresh cells refilled, the
|
||||
//! fringe re-stamped, the flux balance on), the patch's wall velocity from
|
||||
//! the interface velocities (nearest wetted segment, linear along it), and
|
||||
//! the load from the patch's wall faces (pressure + full-stress traction
|
||||
//! per face into `WettedSurface::transfer_load`) — no probes, no spike
|
||||
//! clamp, no smoothing. The structure side is the harness's, unchanged.
|
||||
|
||||
use std::cell::Cell;
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use nalgebra::Vector3;
|
||||
use rtx_cfd::mesh::patch_gen::cylinder_flag_patch_deformed;
|
||||
use rtx_cfd::mesh::PatchSide;
|
||||
use rtx_cfd::solvers::incompressible::{
|
||||
AleBoundaries, ConvectionScheme, CurvilinearParameters, CurvilinearPisoSolver,
|
||||
EmbeddedParameters, EmbeddedPisoSolver, FlowField, MgPrecision, NormalDiffusion, OversetField,
|
||||
OversetParameters, OversetPisoSolver, OversetResult, OversetSolverState, PatchConvection,
|
||||
PatchField, PoissonSolverKind, SideBoundary,
|
||||
};
|
||||
use rtx_cfd::{CfdConfig, CfdResult};
|
||||
use rtx_fea::mesh::{Mesh, NodeId};
|
||||
use rtx_fsi::{FluidFace, WettedSurface};
|
||||
|
||||
use super::{flag_mesh, inflow_for, BenchmarkCase, Interface, H, L, NU_F, RHO_F};
|
||||
|
||||
const CYL_CENTRE: [f64; 2] = [0.2, 0.2];
|
||||
const CYL_R: f64 = 0.05;
|
||||
const FLAG_T: f64 = 0.01;
|
||||
/// The junction fillet: a fixed 5 mm at every resolution (§5.11).
|
||||
const FILLET: f64 = 0.5 * 0.41 / 41.0;
|
||||
const PATCH_ROWS: usize = 12;
|
||||
const PATCH_STRETCH: f64 = 4.0;
|
||||
|
||||
/// The deforming wall as the patch sees it: the wetted polygon (the
|
||||
/// `Interface` walk, anchors included) and the velocity at each vertex.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct WallMotion {
|
||||
pub polygon: Vec<[f64; 2]>,
|
||||
pub velocity: Vec<[f64; 2]>,
|
||||
}
|
||||
|
||||
impl WallMotion {
|
||||
/// Velocity at `(x, y)`: linear along the nearest polygon segment.
|
||||
pub fn velocity_at(&self, x: f64, y: f64) -> (f64, f64) {
|
||||
let n = self.polygon.len();
|
||||
if n < 2 {
|
||||
return (0.0, 0.0);
|
||||
}
|
||||
let (mut best_d, mut best) = (f64::INFINITY, (0.0, 0.0));
|
||||
for i in 0..n - 1 {
|
||||
let (a, b) = (self.polygon[i], self.polygon[i + 1]);
|
||||
let (dx, dy) = (b[0] - a[0], b[1] - a[1]);
|
||||
let l2 = dx * dx + dy * dy;
|
||||
let t = if l2 > 0.0 {
|
||||
(((x - a[0]) * dx + (y - a[1]) * dy) / l2).clamp(0.0, 1.0)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let (px, py) = (a[0] + t * dx, a[1] + t * dy);
|
||||
let d = (px - x).powi(2) + (py - y).powi(2);
|
||||
if d < best_d {
|
||||
best_d = d;
|
||||
let (va, vb) = (self.velocity[i], self.velocity[i + 1]);
|
||||
best = (va[0] + t * (vb[0] - va[0]), va[1] + t * (vb[1] - va[1]));
|
||||
}
|
||||
}
|
||||
best
|
||||
}
|
||||
}
|
||||
|
||||
/// The overset fluid of the coupled march.
|
||||
pub struct OversetFluid {
|
||||
pub case: BenchmarkCase,
|
||||
pub mesh: Mesh,
|
||||
pub interface: Interface,
|
||||
pub a_node: NodeId,
|
||||
pub ny: usize,
|
||||
pub nx: usize,
|
||||
pub h: f64,
|
||||
pub mu: f64,
|
||||
pub dt_fluid: f64,
|
||||
pub solver: OversetPisoSolver,
|
||||
pub field: OversetField,
|
||||
pub shared: Arc<RwLock<WallMotion>>,
|
||||
pub sweeps: usize,
|
||||
/// Patch regenerations and their wall time.
|
||||
pub regen_count: Cell<usize>,
|
||||
pub regen_seconds: Cell<f64>,
|
||||
/// Background cells reclassified, summed over every fluid step.
|
||||
pub reclassified_total: Cell<usize>,
|
||||
pub fresh_total: Cell<usize>,
|
||||
pub rounds_total: Cell<usize>,
|
||||
pub correctors_total: Cell<usize>,
|
||||
}
|
||||
|
||||
impl OversetFluid {
|
||||
/// Build the composite at rest around the undeformed flag.
|
||||
pub fn build_case(
|
||||
case: BenchmarkCase,
|
||||
ny: usize,
|
||||
flag_nx: usize,
|
||||
sweeps: usize,
|
||||
max_rounds: usize,
|
||||
) -> CfdResult<Self> {
|
||||
let h = H / ny as f64;
|
||||
let nx = (L / h).round() as usize;
|
||||
let mu = RHO_F * NU_F;
|
||||
let u_mean = case.u_mean;
|
||||
let u_peak = 1.5 * 1.5 * u_mean;
|
||||
|
||||
let mesh = flag_mesh(flag_nx, 2);
|
||||
let interface = Interface::build(&mesh);
|
||||
let a_node = mesh
|
||||
.nodes
|
||||
.iter()
|
||||
.find(|(_, n)| {
|
||||
(n.position().x - 0.6).abs() < 1e-9 && (n.position().y - 0.2).abs() < 1e-9
|
||||
})
|
||||
.map(|(&id, _)| id)
|
||||
.expect("point A");
|
||||
let zero_d = vec![0.0; 2 * interface.wetted.len()];
|
||||
|
||||
let config = CfdConfig::new()
|
||||
.with_density(RHO_F)
|
||||
.with_viscosity(mu)
|
||||
.with_reference_velocity(u_mean)
|
||||
.with_reference_length(0.1);
|
||||
let mut background = EmbeddedPisoSolver::new(
|
||||
config.clone(),
|
||||
EmbeddedParameters {
|
||||
corrector_steps: 2,
|
||||
tolerance: 1e-7,
|
||||
boundaries: AleBoundaries {
|
||||
left: SideBoundary::Velocity,
|
||||
right: SideBoundary::PressureOutlet,
|
||||
bottom: SideBoundary::Velocity,
|
||||
top: SideBoundary::Velocity,
|
||||
},
|
||||
poisson_solver: PoissonSolverKind::Multigrid,
|
||||
poisson_precision: MgPrecision::F64,
|
||||
convection_scheme: ConvectionScheme::TvdVanAlbada,
|
||||
},
|
||||
)?;
|
||||
background.set_boundary_velocity(move |x, y, t| {
|
||||
if x <= 0.0 {
|
||||
(inflow_for(u_mean, y, t), 0.0)
|
||||
} else {
|
||||
(0.0, 0.0)
|
||||
}
|
||||
});
|
||||
|
||||
let (patch_mesh, _) = cylinder_flag_patch_deformed(
|
||||
CYL_CENTRE,
|
||||
CYL_R,
|
||||
FLAG_T,
|
||||
&interface.edges(&zero_d),
|
||||
h,
|
||||
FILLET,
|
||||
6.0 * h,
|
||||
PATCH_ROWS,
|
||||
PATCH_STRETCH,
|
||||
sweeps,
|
||||
)?;
|
||||
// The patch's along-body explicit limit (its wall row is
|
||||
// line-implicit) beside the background's combined criterion.
|
||||
let mut hs = f64::INFINITY;
|
||||
for c in 0..patch_mesh.cell_count() {
|
||||
for (f, _) in patch_mesh.cell_faces(c) {
|
||||
if patch_mesh.is_sface(f) {
|
||||
let d = patch_mesh.faces()[f].d;
|
||||
hs = hs.min((d[0] * d[0] + d[1] * d[1]).sqrt());
|
||||
}
|
||||
}
|
||||
}
|
||||
let dt_bg = 0.25 / (2.0 * u_peak / h + 4.0 * NU_F / (h * h));
|
||||
let dt_patch = 0.4 * (hs * hs / (4.0 * NU_F)).min(hs / u_peak);
|
||||
let dt_fluid = dt_bg.min(dt_patch);
|
||||
|
||||
let shared = Arc::new(RwLock::new(WallMotion {
|
||||
polygon: interface
|
||||
.polygon(&zero_d)
|
||||
.iter()
|
||||
.map(|&(x, y)| [x, y])
|
||||
.collect(),
|
||||
velocity: vec![[0.0, 0.0]; interface.walk.len()],
|
||||
}));
|
||||
let wall = shared.clone();
|
||||
let mut patch = CurvilinearPisoSolver::new(
|
||||
config,
|
||||
CurvilinearParameters {
|
||||
tolerance: 1e-5,
|
||||
convection: PatchConvection::TvdVanAlbada,
|
||||
normal_diffusion: NormalDiffusion::LineImplicit,
|
||||
..CurvilinearParameters::default()
|
||||
},
|
||||
patch_mesh,
|
||||
)?;
|
||||
patch.set_side_velocity(PatchSide::Inner, move |x, y, _| {
|
||||
wall.read().unwrap().velocity_at(x, y)
|
||||
});
|
||||
let mut patch_field = PatchField::new(patch.mesh());
|
||||
patch.initialize(&mut patch_field, |_, _| (0.0, 0.0));
|
||||
|
||||
let params = OversetParameters {
|
||||
stall_rounds: 2,
|
||||
max_rounds,
|
||||
..OversetParameters::default()
|
||||
};
|
||||
let mut solver = OversetPisoSolver::new(background, patch, (nx, ny, h, h), params)?;
|
||||
let mut field = OversetField {
|
||||
background: FlowField::new(nx, ny, h, h)?,
|
||||
patch: patch_field,
|
||||
};
|
||||
solver.initialize(&mut field)?;
|
||||
Ok(Self {
|
||||
case,
|
||||
mesh,
|
||||
interface,
|
||||
a_node,
|
||||
ny,
|
||||
nx,
|
||||
h,
|
||||
mu,
|
||||
dt_fluid,
|
||||
solver,
|
||||
field,
|
||||
shared,
|
||||
sweeps,
|
||||
regen_count: Cell::new(0),
|
||||
regen_seconds: Cell::new(0.0),
|
||||
reclassified_total: Cell::new(0),
|
||||
fresh_total: Cell::new(0),
|
||||
rounds_total: Cell::new(0),
|
||||
correctors_total: Cell::new(0),
|
||||
})
|
||||
}
|
||||
|
||||
/// The patch around the interface `d`.
|
||||
pub fn patch_for(&self, d: &[f64]) -> CfdResult<rtx_cfd::mesh::PatchMesh> {
|
||||
let start = std::time::Instant::now();
|
||||
let (mesh, _) = cylinder_flag_patch_deformed(
|
||||
CYL_CENTRE,
|
||||
CYL_R,
|
||||
FLAG_T,
|
||||
&self.interface.edges(d),
|
||||
self.h,
|
||||
FILLET,
|
||||
6.0 * self.h,
|
||||
PATCH_ROWS,
|
||||
PATCH_STRETCH,
|
||||
self.sweeps,
|
||||
)?;
|
||||
self.regen_count.set(self.regen_count.get() + 1);
|
||||
self.regen_seconds
|
||||
.set(self.regen_seconds.get() + start.elapsed().as_secs_f64());
|
||||
Ok(mesh)
|
||||
}
|
||||
|
||||
/// The wall for the next fluid step: geometry `d`, velocity `ddot`.
|
||||
pub fn set_geometry(&mut self, d: &[f64], ddot: &[f64]) -> CfdResult<()> {
|
||||
{
|
||||
let mut w = self.shared.write().unwrap();
|
||||
w.polygon = self
|
||||
.interface
|
||||
.polygon(d)
|
||||
.iter()
|
||||
.map(|&(x, y)| [x, y])
|
||||
.collect();
|
||||
w.velocity = self
|
||||
.interface
|
||||
.walk_velocities(ddot)
|
||||
.iter()
|
||||
.map(|&(u, v)| [u, v])
|
||||
.collect();
|
||||
}
|
||||
let mesh = self.patch_for(d)?;
|
||||
self.solver.set_patch_mesh(mesh)
|
||||
}
|
||||
|
||||
/// One fluid step at the current wall.
|
||||
pub fn step(&mut self) -> CfdResult<OversetResult> {
|
||||
let r = futures::executor::block_on(self.solver.advance(&mut self.field, self.dt_fluid))?;
|
||||
self.reclassified_total
|
||||
.set(self.reclassified_total.get() + r.reclassified_cells);
|
||||
self.fresh_total.set(self.fresh_total.get() + r.fresh_cells);
|
||||
self.rounds_total
|
||||
.set(self.rounds_total.get() + r.rounds.iter().sum::<usize>());
|
||||
self.correctors_total
|
||||
.set(self.correctors_total.get() + r.rounds.len());
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
/// `subcycle` fluid substeps from the current state with the interface
|
||||
/// interpolated from `d_n` to `d_candidate` (the harness's
|
||||
/// `advance_subcycled`, digit for digit in the kinematics).
|
||||
pub fn advance_subcycled(
|
||||
&mut self,
|
||||
d_n: &[f64],
|
||||
d_candidate: &[f64],
|
||||
subcycle: usize,
|
||||
v_n: Option<&[f64]>,
|
||||
) -> CfdResult<()> {
|
||||
let dt = self.dt_fluid * subcycle as f64;
|
||||
let mean_velocity: Vec<f64> = d_candidate
|
||||
.iter()
|
||||
.zip(d_n)
|
||||
.map(|(new, old)| (new - old) / dt)
|
||||
.collect();
|
||||
for m in 1..=subcycle {
|
||||
let fraction = m as f64 / subcycle as f64;
|
||||
let (d_sub, ddot_sub): (Vec<f64>, Vec<f64>) = match v_n {
|
||||
None => (
|
||||
d_n.iter()
|
||||
.zip(d_candidate)
|
||||
.map(|(old, new)| old + fraction * (new - old))
|
||||
.collect(),
|
||||
mean_velocity.clone(),
|
||||
),
|
||||
Some(v_start) => {
|
||||
let mut d_sub = Vec::with_capacity(d_n.len());
|
||||
let mut ddot_sub = Vec::with_capacity(d_n.len());
|
||||
for k in 0..d_n.len() {
|
||||
let v_end = 2.0 * mean_velocity[k] - v_start[k];
|
||||
let accel = (v_end - v_start[k]) / dt;
|
||||
let tau = fraction * dt;
|
||||
d_sub.push(d_n[k] + v_start[k] * tau + 0.5 * accel * tau * tau);
|
||||
ddot_sub.push(v_start[k] + accel * tau);
|
||||
}
|
||||
(d_sub, ddot_sub)
|
||||
}
|
||||
};
|
||||
self.set_geometry(&d_sub, &ddot_sub)?;
|
||||
self.step()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Drag and lift on cylinder + flag from the patch's wall stress.
|
||||
pub fn measure_force(&self) -> (f64, f64) {
|
||||
let f = self
|
||||
.solver
|
||||
.patch()
|
||||
.surface_force(&self.field.patch, PatchSide::Inner, self.solver.time())
|
||||
.total();
|
||||
(f[0], f[1])
|
||||
}
|
||||
|
||||
/// The wall faces' tractions transferred to the flag's wetted nodes
|
||||
/// at geometry `d`: `(nodal forces, conservation defect, faces used)`.
|
||||
/// Faces on the cylinder proper are skipped; the fillets' load goes
|
||||
/// to the nearest (clamped) root nodes.
|
||||
pub fn sample_load(&self, d: &[f64]) -> (Vec<(NodeId, Vector3<f64>)>, f64, usize) {
|
||||
let mut faces = Vec::new();
|
||||
let mut tractions: Vec<Vector3<f64>> = Vec::new();
|
||||
for (centre, normal, len, traction) in self.solver.patch().wall_tractions(
|
||||
&self.field.patch,
|
||||
PatchSide::Inner,
|
||||
self.solver.time(),
|
||||
) {
|
||||
let on_cylinder =
|
||||
((centre[0] - CYL_CENTRE[0]).powi(2) + (centre[1] - CYL_CENTRE[1]).powi(2)).sqrt()
|
||||
< CYL_R + 1e-9;
|
||||
if on_cylinder {
|
||||
continue;
|
||||
}
|
||||
faces.push(FluidFace {
|
||||
centroid: Vector3::new(centre[0], centre[1], 0.0),
|
||||
normal: Vector3::new(normal[0], normal[1], 0.0),
|
||||
area: len,
|
||||
});
|
||||
tractions.push(Vector3::new(traction[0], traction[1], 0.0));
|
||||
}
|
||||
let nodes_now = self.interface.deformed_nodes(d);
|
||||
let surface = WettedSurface::build(&faces, &nodes_now).expect("transfer build");
|
||||
let nodal = surface.transfer_load(&faces, &tractions).unwrap();
|
||||
let total_sampled: Vector3<f64> =
|
||||
faces.iter().zip(&tractions).map(|(f, t)| t * f.area).sum();
|
||||
let total_nodal: Vector3<f64> = nodal.iter().sum();
|
||||
let conservation = (total_nodal - total_sampled).norm() / total_sampled.norm().max(1e-30);
|
||||
(
|
||||
self.interface
|
||||
.wetted
|
||||
.iter()
|
||||
.zip(nodal)
|
||||
.map(|(&id, f)| (id, f))
|
||||
.collect(),
|
||||
conservation,
|
||||
faces.len(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn snapshot(&self) -> (OversetSolverState, OversetField) {
|
||||
(self.solver.snapshot(), self.field.clone())
|
||||
}
|
||||
|
||||
pub fn restore(&mut self, saved: &(OversetSolverState, OversetField)) {
|
||||
self.solver.restore(&saved.0);
|
||||
self.field = saved.1.clone();
|
||||
}
|
||||
|
||||
pub fn time(&self) -> f64 {
|
||||
self.solver.time()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,399 @@
|
||||
//! P5 (§5.12): the coupled march on the overset fluid — the harness's
|
||||
//! rigid phase, release, and per-step subiterated coupling (predictor,
|
||||
//! IQN-ILS / Aitken passes each re-marching the fluid from the step's
|
||||
//! snapshot, the acceptance rule of `march.rs`), without the embedded
|
||||
//! march's rescue machinery (refuted, retired). Returns the harness's
|
||||
//! `MarchResult` plus the composite's own counters.
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::io::Write as _;
|
||||
|
||||
use nalgebra::Vector3;
|
||||
use rtx_fea::analysis::{
|
||||
AnalysisConfig, ConvergenceCriteria, DynamicState, NonlinearDynamicAnalysis,
|
||||
};
|
||||
use rtx_fea::materials::{LinearElastic, MaterialDatabase};
|
||||
use rtx_fea::mesh::{MaterialId, NodeId};
|
||||
use rtx_fsi::{IqnIls, Subiterated};
|
||||
|
||||
use super::march::MarchResult;
|
||||
use super::overset::OversetFluid;
|
||||
use super::{clamp_left, env_or, median, mid_amp, BenchmarkCase};
|
||||
|
||||
/// The overset march's knobs (`RTX_<PREFIX>_*`).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct OversetMarchConfig {
|
||||
pub ny: usize,
|
||||
pub flag_nx: usize,
|
||||
pub t_release: f64,
|
||||
pub t_end: f64,
|
||||
pub subcycle: usize,
|
||||
pub tol_floor: f64,
|
||||
pub rtol: f64,
|
||||
pub stall_accept: f64,
|
||||
pub max_subiterations: usize,
|
||||
pub coupler: String,
|
||||
pub reuse: usize,
|
||||
pub initial_relaxation: f64,
|
||||
pub c1_interface: bool,
|
||||
pub predictor: String,
|
||||
/// Winslow sweeps per patch regeneration.
|
||||
pub sweeps: usize,
|
||||
/// Schwarz rounds per corrector (the P5 budget: 3).
|
||||
pub max_rounds: usize,
|
||||
pub csv_path: Option<String>,
|
||||
pub trace_steps: usize,
|
||||
}
|
||||
|
||||
impl OversetMarchConfig {
|
||||
pub fn from_env(prefix: &str, d: OversetMarchConfig) -> OversetMarchConfig {
|
||||
let num = |k: &str, v: f64| env_or(&format!("RTX_{prefix}_{k}"), v);
|
||||
let text =
|
||||
|k: &str, v: &str| std::env::var(format!("RTX_{prefix}_{k}")).unwrap_or(v.into());
|
||||
OversetMarchConfig {
|
||||
ny: num("NY", d.ny as f64) as usize,
|
||||
flag_nx: num("FLAG_NX", d.flag_nx as f64) as usize,
|
||||
t_release: num("T_RELEASE", d.t_release),
|
||||
t_end: num("T_END", d.t_end),
|
||||
subcycle: num("SUBCYCLE", d.subcycle as f64) as usize,
|
||||
tol_floor: num("TOL_FLOOR", d.tol_floor),
|
||||
rtol: num("RTOL", d.rtol),
|
||||
stall_accept: num("STALL_ACCEPT", d.stall_accept),
|
||||
max_subiterations: num("MAX_SUBIT", d.max_subiterations as f64) as usize,
|
||||
coupler: text("COUPLER", &d.coupler),
|
||||
reuse: num("REUSE", d.reuse as f64) as usize,
|
||||
initial_relaxation: num("OMEGA0", d.initial_relaxation),
|
||||
c1_interface: num("C1", if d.c1_interface { 1.0 } else { 0.0 }) > 0.5,
|
||||
predictor: text("PREDICTOR", &d.predictor),
|
||||
sweeps: num("SWEEPS", d.sweeps as f64) as usize,
|
||||
max_rounds: num("MAX_ROUNDS", d.max_rounds as f64) as usize,
|
||||
csv_path: std::env::var(format!("RTX_{prefix}_CSV"))
|
||||
.ok()
|
||||
.or(d.csv_path),
|
||||
trace_steps: num("TRACE", d.trace_steps as f64) as usize,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The march's outcome: the harness's statistics, the composite's
|
||||
/// counters, and the death (if any) instead of a panic.
|
||||
pub struct OversetMarchResult {
|
||||
pub result: MarchResult,
|
||||
pub death: Option<(usize, f64, String)>,
|
||||
pub rounds_mean: f64,
|
||||
pub reclassified_mean: f64,
|
||||
pub fresh_mean: f64,
|
||||
pub regen_count: usize,
|
||||
pub regen_seconds: f64,
|
||||
pub fluid_seconds: f64,
|
||||
pub structure_seconds: f64,
|
||||
pub faces_used: usize,
|
||||
}
|
||||
|
||||
pub fn run_march_overset(case: BenchmarkCase, config: &OversetMarchConfig) -> OversetMarchResult {
|
||||
let cfg = config.clone();
|
||||
let mut fluid = OversetFluid::build_case(case, cfg.ny, cfg.flag_nx, cfg.sweeps, cfg.max_rounds)
|
||||
.expect("overset fluid");
|
||||
let dt_fluid = fluid.dt_fluid;
|
||||
let dt = dt_fluid * cfg.subcycle as f64;
|
||||
let zero_d = vec![0.0; 2 * fluid.interface.wetted.len()];
|
||||
|
||||
// Phase 1: rigid flag to t_release.
|
||||
let start = std::time::Instant::now();
|
||||
let rigid_steps = (cfg.t_release / dt_fluid).round() as usize;
|
||||
for _ in 0..rigid_steps {
|
||||
fluid.step().expect("rigid fluid step");
|
||||
}
|
||||
let (rigid_drag, rigid_lift) = fluid.measure_force();
|
||||
println!(
|
||||
" {} OVERSET rigid phase: {rigid_steps} steps (dt {dt_fluid:.3e}) to t = {:.2} s in {:.0} s wall; wall drag {rigid_drag:.2} (rigid-flag reference {:.1}; the overset's own CFD2 at ny = 41: 137.8), lift {rigid_lift:.2}; rounds mean {:.2}",
|
||||
case.name,
|
||||
cfg.t_release,
|
||||
start.elapsed().as_secs_f64(),
|
||||
case.rigid_drag_reference,
|
||||
fluid.rounds_total.get() as f64 / fluid.correctors_total.get().max(1) as f64
|
||||
);
|
||||
|
||||
// The flag: nonlinear Newmark stepper at the coupled dt (as march.rs).
|
||||
let mut db = MaterialDatabase::new();
|
||||
db.add_material(
|
||||
MaterialId(0),
|
||||
LinearElastic::new(case.e_s, case.nu_s).with_density(case.rho_s),
|
||||
None,
|
||||
);
|
||||
let analysis = NonlinearDynamicAnalysis::new(
|
||||
fluid.mesh.clone(),
|
||||
db,
|
||||
clamp_left(&fluid.mesh),
|
||||
dt,
|
||||
1,
|
||||
AnalysisConfig::default(),
|
||||
)
|
||||
.with_total_lagrangian()
|
||||
.with_convergence_criteria(ConvergenceCriteria {
|
||||
max_iterations: 60,
|
||||
..ConvergenceCriteria::default()
|
||||
});
|
||||
let flag = RefCell::new(analysis.stepper().unwrap());
|
||||
let wetted_dofs: Vec<[usize; 2]> = fluid
|
||||
.interface
|
||||
.wetted
|
||||
.iter()
|
||||
.map(|&id| {
|
||||
let dofs = flag.borrow().node_dofs(id);
|
||||
[dofs[0], dofs[1]]
|
||||
})
|
||||
.collect();
|
||||
let a_dofs = flag.borrow().node_dofs(fluid.a_node);
|
||||
let extract = |state: &DynamicState| -> Vec<f64> {
|
||||
let mut d = vec![0.0; 2 * wetted_dofs.len()];
|
||||
for (k, dofs) in wetted_dofs.iter().enumerate() {
|
||||
d[2 * k] = state.displacement[dofs[0]];
|
||||
d[2 * k + 1] = state.displacement[dofs[1]];
|
||||
}
|
||||
d
|
||||
};
|
||||
let extract_velocity = |state: &DynamicState| -> Vec<f64> {
|
||||
let mut v = vec![0.0; 2 * wetted_dofs.len()];
|
||||
for (k, dofs) in wetted_dofs.iter().enumerate() {
|
||||
v[2 * k] = state.velocity[dofs[0]];
|
||||
v[2 * k + 1] = state.velocity[dofs[1]];
|
||||
}
|
||||
v
|
||||
};
|
||||
|
||||
// Phase 2: release under the current load.
|
||||
let (nodal0, conservation0, faces0) = fluid.sample_load(&zero_d);
|
||||
flag.borrow_mut().set_nodal_forces(&nodal0);
|
||||
let mut flag_state = flag.borrow_mut().rest_state().unwrap();
|
||||
let mut committed_nodal = nodal0;
|
||||
let mut worst_conservation = conservation0;
|
||||
println!(
|
||||
" release: {faces0} wall faces transferred (conservation defect {conservation0:.2e}); initial tip acceleration |a| = {:.3e}",
|
||||
(a_dofs.iter().map(|&k| flag_state.acceleration[k].powi(2)).sum::<f64>()).sqrt()
|
||||
);
|
||||
|
||||
let fluid = RefCell::new(fluid);
|
||||
let mut iqn = (cfg.coupler == "iqn").then(|| {
|
||||
IqnIls::new(cfg.max_subiterations, 1.0)
|
||||
.unwrap()
|
||||
.with_reuse(cfg.reuse)
|
||||
.with_initial_relaxation(cfg.initial_relaxation)
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
let coupled_steps = ((cfg.t_end - cfg.t_release) / dt).round() as usize;
|
||||
let mut times = Vec::with_capacity(coupled_steps);
|
||||
let mut ux_series = Vec::with_capacity(coupled_steps);
|
||||
let mut uy_series = Vec::with_capacity(coupled_steps);
|
||||
let mut force_times = Vec::new();
|
||||
let mut drag_series = Vec::new();
|
||||
let mut lift_series = Vec::new();
|
||||
let (mut interval_drag, mut interval_lift) = (Vec::new(), Vec::new());
|
||||
let mut total_subiterations = 0usize;
|
||||
let mut max_subiterations = 0usize;
|
||||
let mut stalled_steps = 0usize;
|
||||
let mut retried_steps = 0usize;
|
||||
let mut worst_stall = 0.0_f64;
|
||||
let mut faces_used = faces0;
|
||||
let mut death: Option<(usize, f64, String)> = None;
|
||||
let mut csv = cfg.csv_path.as_ref().map(|p| {
|
||||
let mut f = std::fs::File::create(p).expect("csv");
|
||||
writeln!(f, "t,ux,uy,drag,lift").unwrap();
|
||||
f
|
||||
});
|
||||
let (t_fluid, t_structure) = (std::cell::Cell::new(0.0_f64), std::cell::Cell::new(0.0_f64));
|
||||
let phase_start = std::time::Instant::now();
|
||||
|
||||
for step in 0..coupled_steps {
|
||||
let d_n = extract(&flag_state);
|
||||
let v_n: Option<Vec<f64>> = cfg.c1_interface.then(|| extract_velocity(&flag_state));
|
||||
let d_predicted = if cfg.predictor == "kinematic" {
|
||||
let v = extract_velocity(&flag_state);
|
||||
d_n.iter().zip(&v).map(|(d, v)| d + dt * v).collect()
|
||||
} else {
|
||||
flag.borrow_mut().set_nodal_forces(&committed_nodal);
|
||||
let (predicted, _) = flag.borrow_mut().step(&flag_state).unwrap();
|
||||
extract(&predicted)
|
||||
};
|
||||
let saved = fluid.borrow().snapshot();
|
||||
type PassResult = (DynamicState, Vec<(NodeId, Vector3<f64>)>, f64, usize);
|
||||
let latest: RefCell<Option<PassResult>> = RefCell::new(None);
|
||||
let pass = |d_candidate: &[f64]| -> Vec<f64> {
|
||||
let fs = std::time::Instant::now();
|
||||
let mut fl = fluid.borrow_mut();
|
||||
fl.restore(&saved);
|
||||
fl.advance_subcycled(&d_n, d_candidate, cfg.subcycle, v_n.as_deref())
|
||||
.expect("fluid pass");
|
||||
let (nodal, conservation, faces) = fl.sample_load(d_candidate);
|
||||
t_fluid.set(t_fluid.get() + fs.elapsed().as_secs_f64());
|
||||
let ss = std::time::Instant::now();
|
||||
let mut flag_ref = flag.borrow_mut();
|
||||
flag_ref.set_nodal_forces(&nodal);
|
||||
let (candidate_state, _) = flag_ref.step(&flag_state).unwrap();
|
||||
t_structure.set(t_structure.get() + ss.elapsed().as_secs_f64());
|
||||
let d_new = extract(&candidate_state);
|
||||
if step < cfg.trace_steps {
|
||||
let residual: f64 = d_new
|
||||
.iter()
|
||||
.zip(d_candidate)
|
||||
.map(|(a, b)| (a - b) * (a - b))
|
||||
.sum::<f64>()
|
||||
.sqrt();
|
||||
let load: f64 = nodal.iter().map(|(_, f)| f.norm()).sum();
|
||||
println!(
|
||||
" step {step} pass: |d_new − d_candidate| = {residual:.3e}, |d_new| = {:.3e}, nodal load {load:.2}, {faces} faces",
|
||||
d_new.iter().map(|v| v * v).sum::<f64>().sqrt()
|
||||
);
|
||||
}
|
||||
*latest.borrow_mut() = Some((candidate_state, nodal, conservation, faces));
|
||||
d_new
|
||||
};
|
||||
|
||||
let increment: f64 = d_predicted
|
||||
.iter()
|
||||
.zip(&d_n)
|
||||
.map(|(a, b)| (a - b) * (a - b))
|
||||
.sum::<f64>()
|
||||
.sqrt();
|
||||
let tol_step = cfg.tol_floor.max(cfg.rtol * increment);
|
||||
let retry_at = (5.0 * tol_step).max(0.1 * increment);
|
||||
let acceptable = (cfg.stall_accept * tol_step).max(0.1 * increment);
|
||||
let mut outcome = if let Some(iqn) = iqn.as_mut() {
|
||||
iqn.set_tolerance(tol_step).unwrap();
|
||||
iqn.solve(&d_predicted, pass)
|
||||
} else {
|
||||
Subiterated::aitken(cfg.max_subiterations, tol_step)
|
||||
.unwrap()
|
||||
.solve(&d_predicted, pass)
|
||||
};
|
||||
if let (Err(e), Some(iqn_ref)) = (&outcome, iqn.as_mut()) {
|
||||
let recoverable = matches!(
|
||||
e,
|
||||
rtx_fsi::FsiError::CouplingNotConverged { residual, .. }
|
||||
| rtx_fsi::FsiError::CouplingDiverged { residual, .. }
|
||||
if *residual >= retry_at
|
||||
);
|
||||
if recoverable {
|
||||
iqn_ref.reset_history();
|
||||
retried_steps += 1;
|
||||
outcome = iqn_ref.solve(&d_predicted, pass);
|
||||
}
|
||||
}
|
||||
let t_now = cfg.t_release + (step + 1) as f64 * dt;
|
||||
match outcome {
|
||||
Ok(c) => {
|
||||
total_subiterations += c.iterations;
|
||||
max_subiterations = max_subiterations.max(c.iterations);
|
||||
}
|
||||
Err(
|
||||
rtx_fsi::FsiError::CouplingNotConverged {
|
||||
iterations,
|
||||
residual,
|
||||
..
|
||||
}
|
||||
| rtx_fsi::FsiError::CouplingDiverged {
|
||||
iterations,
|
||||
residual,
|
||||
},
|
||||
) if residual < acceptable => {
|
||||
stalled_steps += 1;
|
||||
worst_stall = worst_stall.max(residual);
|
||||
total_subiterations += iterations;
|
||||
max_subiterations = max_subiterations.max(iterations);
|
||||
}
|
||||
Err(e) => {
|
||||
println!(
|
||||
" {} OVERSET DEATH at step {step} t = {t_now:.4}: {e:?} (increment {increment:.3e}, tol {tol_step:.3e}, acceptable {acceptable:.3e})",
|
||||
case.name
|
||||
);
|
||||
death = Some((step, t_now, format!("{e:?}")));
|
||||
break;
|
||||
}
|
||||
}
|
||||
let (new_state, nodal, conservation, faces) = latest.borrow_mut().take().expect("pass ran");
|
||||
flag_state = new_state;
|
||||
committed_nodal = nodal;
|
||||
worst_conservation = worst_conservation.max(conservation);
|
||||
faces_used = faces;
|
||||
let ux = flag_state.displacement[a_dofs[0]];
|
||||
let uy = flag_state.displacement[a_dofs[1]];
|
||||
times.push(t_now);
|
||||
ux_series.push(ux);
|
||||
uy_series.push(uy);
|
||||
let (drag_now, lift_now) = fluid.borrow().measure_force();
|
||||
interval_drag.push(drag_now);
|
||||
interval_lift.push(lift_now);
|
||||
if (step + 1) % 10 == 0 {
|
||||
let drag = median(&mut interval_drag);
|
||||
let lift = median(&mut interval_lift);
|
||||
interval_drag.clear();
|
||||
interval_lift.clear();
|
||||
force_times.push(t_now);
|
||||
drag_series.push(drag);
|
||||
lift_series.push(lift);
|
||||
if let Some(f) = csv.as_mut() {
|
||||
writeln!(f, "{t_now:.6},{ux:.6e},{uy:.6e},{drag:.6e},{lift:.6e}").unwrap();
|
||||
}
|
||||
} else if let Some(f) = csv.as_mut() {
|
||||
writeln!(f, "{t_now:.6},{ux:.6e},{uy:.6e},,").unwrap();
|
||||
}
|
||||
if (step + 1) % 500 == 0 {
|
||||
let window = &uy_series[uy_series.len().saturating_sub(500)..];
|
||||
let (w_mid, w_amp) = mid_amp(window);
|
||||
let fl = fluid.borrow();
|
||||
println!(
|
||||
" t = {t_now:.3} s ({} steps): uy(A) = {uy:.3e} (window mid {w_mid:.3e} amp {w_amp:.3e}), drag {drag_now:.1} lift {lift_now:.1}, {:.1} subit/step, rounds mean {:.2}, reclassified/step {:.1}, regen {:.0} s of {:.0} s fluid, {:.0} s wall",
|
||||
step + 1,
|
||||
total_subiterations as f64 / (step + 1) as f64,
|
||||
fl.rounds_total.get() as f64 / fl.correctors_total.get().max(1) as f64,
|
||||
fl.reclassified_total.get() as f64 / (rigid_steps + (step + 1) * cfg.subcycle * 4).max(1) as f64,
|
||||
fl.regen_seconds.get(),
|
||||
t_fluid.get(),
|
||||
phase_start.elapsed().as_secs_f64()
|
||||
);
|
||||
}
|
||||
}
|
||||
let fl = fluid.borrow();
|
||||
let final_state_finite = flag_state.displacement.iter().all(|v| v.is_finite());
|
||||
let steps_done = times.len();
|
||||
OversetMarchResult {
|
||||
result: MarchResult {
|
||||
dt,
|
||||
coupled_steps: steps_done,
|
||||
times,
|
||||
ux: ux_series,
|
||||
uy: uy_series,
|
||||
force_times,
|
||||
drag: drag_series,
|
||||
lift: lift_series,
|
||||
rigid_drag,
|
||||
rigid_lift,
|
||||
mean_subiterations: total_subiterations as f64 / steps_done.max(1) as f64,
|
||||
max_subiterations,
|
||||
stalled_steps,
|
||||
retried_steps,
|
||||
worst_stall,
|
||||
worst_conservation,
|
||||
skipped: 0,
|
||||
spiked: 0,
|
||||
newton_rescues: flag.borrow().rescue_counts(),
|
||||
coupling_rescues: 0,
|
||||
coupling_rescue_failures: 0,
|
||||
rescue_records: Vec::new(),
|
||||
final_state_finite,
|
||||
elapsed: start.elapsed().as_secs_f64(),
|
||||
},
|
||||
death,
|
||||
rounds_mean: fl.rounds_total.get() as f64 / fl.correctors_total.get().max(1) as f64,
|
||||
reclassified_mean: fl.reclassified_total.get() as f64
|
||||
/ (rigid_steps + steps_done * cfg.subcycle).max(1) as f64,
|
||||
fresh_mean: fl.fresh_total.get() as f64
|
||||
/ (rigid_steps + steps_done * cfg.subcycle).max(1) as f64,
|
||||
regen_count: fl.regen_count.get(),
|
||||
regen_seconds: fl.regen_seconds.get(),
|
||||
fluid_seconds: t_fluid.get(),
|
||||
structure_seconds: t_structure.get(),
|
||||
faces_used,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user