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 API Documentation (push) Canceled after 0s
Documentation / Build User Guide (push) Canceled after 0s
CI / Python Bindings (maturin) (macos-latest) (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 / Build (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
650 lines
24 KiB
Rust
650 lines
24 KiB
Rust
//! 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, FLAG_X1, 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]>,
|
||
/// The net normal velocity removed from every segment (the imposed
|
||
/// velocity's volume flux over the wall length) when the wall is
|
||
/// made volume-preserving; zero otherwise.
|
||
pub q: f64,
|
||
}
|
||
|
||
impl WallMotion {
|
||
/// The imposed velocity's net volume flux INTO the fluid over the
|
||
/// polygon (counter-clockwise around the body: outward normal
|
||
/// `(dy, −dx)/L`), and the polygon's length.
|
||
pub fn net_flux(&self) -> (f64, f64) {
|
||
let (mut flux, mut len) = (0.0, 0.0);
|
||
for i in 0..self.polygon.len().saturating_sub(1) {
|
||
let (a, b) = (self.polygon[i], self.polygon[i + 1]);
|
||
let (dx, dy) = (b[0] - a[0], b[1] - a[1]);
|
||
let (va, vb) = (self.velocity[i], self.velocity[i + 1]);
|
||
let (vx, vy) = (0.5 * (va[0] + vb[0]), 0.5 * (va[1] + vb[1]));
|
||
flux += vx * dy - vy * dx;
|
||
len += (dx * dx + dy * dy).sqrt();
|
||
}
|
||
(flux, len)
|
||
}
|
||
|
||
/// The polygon's enclosed area (shoelace; the walk is closed across
|
||
/// the cylinder by its anchors, so this is the flag's area up to a
|
||
/// constant).
|
||
pub fn area(&self) -> f64 {
|
||
let n = self.polygon.len();
|
||
let mut a = 0.0;
|
||
for i in 0..n {
|
||
let (p, q) = (self.polygon[i], self.polygon[(i + 1) % n]);
|
||
a += p[0] * q[1] - q[0] * p[1];
|
||
}
|
||
0.5 * a
|
||
}
|
||
}
|
||
|
||
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]);
|
||
let l = l2.sqrt().max(1e-300);
|
||
best = (
|
||
va[0] + t * (vb[0] - va[0]) - self.q * dy / l,
|
||
va[1] + t * (vb[1] - va[1]) + self.q * dx / l,
|
||
);
|
||
}
|
||
}
|
||
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>,
|
||
/// The interface of the last `set_geometry`.
|
||
pub last_d: Vec<f64>,
|
||
/// The last fluid step's mass defects (patch acceptor ring, background
|
||
/// fringe) and the patch's max divergence.
|
||
pub last_defects: Cell<(f64, f64, f64)>,
|
||
}
|
||
|
||
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),
|
||
FLAG_X1,
|
||
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()],
|
||
q: 0.0,
|
||
}));
|
||
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,
|
||
)?;
|
||
// Discriminators (P5-1 matrix): `RTX_FSI2O_NO_WALL_VEL` keeps the
|
||
// wall at rest in the fluid while the geometry moves.
|
||
let no_wall_vel = std::env::var("RTX_FSI2O_NO_WALL_VEL").is_ok();
|
||
patch.set_side_velocity(PatchSide::Inner, move |x, y, _| {
|
||
if no_wall_vel {
|
||
(0.0, 0.0)
|
||
} else {
|
||
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),
|
||
last_d: zero_d,
|
||
last_defects: Cell::new((0.0, 0.0, 0.0)),
|
||
})
|
||
}
|
||
|
||
/// Save the fluid state (`RTX_FSI2O_SAVE=dir`, tag `fsi2o_ny{ny}`):
|
||
/// the background as `FlowField::save`, the patch vectors raw, the
|
||
/// time.
|
||
pub fn save(&self, dir: &str) -> CfdResult<()> {
|
||
let dir = std::path::Path::new(dir);
|
||
std::fs::create_dir_all(dir).expect("save dir");
|
||
let tag = format!("fsi2o_ny{}", self.ny);
|
||
self.field
|
||
.background
|
||
.save(&dir.join(format!("bg_{tag}.bin")))?;
|
||
for (name, vals) in [
|
||
("u", &self.field.patch.u),
|
||
("v", &self.field.patch.v),
|
||
("p", &self.field.patch.p),
|
||
("flux", &self.field.patch.flux),
|
||
] {
|
||
let bytes: Vec<u8> = vals.iter().flat_map(|x| x.to_le_bytes()).collect();
|
||
std::fs::write(dir.join(format!("patch_{tag}_{name}.bin")), bytes).expect("save patch");
|
||
}
|
||
std::fs::write(
|
||
dir.join(format!("time_{tag}.txt")),
|
||
format!("{:.17e}", self.solver.time()),
|
||
)
|
||
.expect("save time");
|
||
println!(
|
||
" saved the fluid state {tag} at t = {:.4} to {}",
|
||
self.solver.time(),
|
||
dir.display()
|
||
);
|
||
Ok(())
|
||
}
|
||
|
||
/// Load a state saved by [`Self::save`] (same ny, undeformed patch);
|
||
/// returns its time.
|
||
pub fn load(&mut self, dir: &str) -> CfdResult<f64> {
|
||
let dir = std::path::Path::new(dir);
|
||
let tag = format!("fsi2o_ny{}", self.ny);
|
||
self.field.background = FlowField::load(&dir.join(format!("bg_{tag}.bin")))?;
|
||
let read = |name: &str| -> Vec<f64> {
|
||
let bytes = std::fs::read(dir.join(format!("patch_{tag}_{name}.bin")))
|
||
.unwrap_or_else(|e| panic!("load patch {name}: {e}"));
|
||
bytes
|
||
.chunks_exact(8)
|
||
.map(|c| f64::from_le_bytes(c.try_into().expect("8 bytes")))
|
||
.collect()
|
||
};
|
||
self.field.patch.u = read("u");
|
||
self.field.patch.v = read("v");
|
||
self.field.patch.p = read("p");
|
||
self.field.patch.flux = read("flux");
|
||
let t: f64 = std::fs::read_to_string(dir.join(format!("time_{tag}.txt")))
|
||
.expect("time")
|
||
.trim()
|
||
.parse()
|
||
.expect("time value");
|
||
self.solver.set_time(t);
|
||
println!(
|
||
" loaded the fluid state {tag} at t = {t:.4} from {}",
|
||
dir.display()
|
||
);
|
||
Ok(t)
|
||
}
|
||
|
||
/// 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),
|
||
FLAG_X1,
|
||
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();
|
||
// `RTX_FSI2O_VOLUME_PRESERVE`: the fluid sees a volume-preserving
|
||
// wall — the imposed velocity's net flux (the flag's thickness
|
||
// breathing under the pressure step, a ~300 Hz mode of the
|
||
// ν = 0.4 solid) is removed uniformly along the wall.
|
||
w.q = 0.0;
|
||
if std::env::var("RTX_FSI2O_VOLUME_PRESERVE").is_ok() {
|
||
let (flux, len) = w.net_flux();
|
||
w.q = flux / len.max(1e-300);
|
||
}
|
||
}
|
||
// `RTX_FSI2O_FREEZE_PATCH`: the undeformed patch throughout (the
|
||
// wall velocity still moves) — is the regeneration the driver?
|
||
if std::env::var("RTX_FSI2O_FREEZE_PATCH").is_ok() {
|
||
self.last_d = d.to_vec();
|
||
return Ok(());
|
||
}
|
||
let mesh = match self.patch_for(d) {
|
||
Ok(m) => m,
|
||
Err(e) => {
|
||
self.dump_edges(d, &format!("generator refused ({e:?})"));
|
||
return Err(e);
|
||
}
|
||
};
|
||
self.last_d = d.to_vec();
|
||
self.solver.set_patch_mesh(mesh)
|
||
}
|
||
|
||
/// The last geometry, for the offline reproduction
|
||
/// (`patch_cylinder_flag_deformed.rs`, `RTX_CF_EDGES_FILE`): one edge
|
||
/// per block, `x y` per line, when `RTX_FSI2O_DUMP_DIR` is set.
|
||
fn dump_edges(&self, d: &[f64], why: &str) {
|
||
let Ok(dir) = std::env::var("RTX_FSI2O_DUMP_DIR") else {
|
||
return;
|
||
};
|
||
let e = self.interface.edges(d);
|
||
let mut out = String::new();
|
||
for (name, pts) in [("bottom", &e.bottom), ("tip", &e.tip), ("top", &e.top)] {
|
||
out.push_str(&format!("# {name} {}\n", pts.len()));
|
||
for p in pts {
|
||
out.push_str(&format!("{:.17e} {:.17e}\n", p[0], p[1]));
|
||
}
|
||
}
|
||
let path = std::path::Path::new(&dir).join("p5_death_edges.txt");
|
||
std::fs::write(&path, out).expect("dump edges");
|
||
println!(" {why}; edges dumped to {}", path.display());
|
||
}
|
||
|
||
/// One fluid step at the current wall.
|
||
pub fn step(&mut self) -> CfdResult<OversetResult> {
|
||
let r = match futures::executor::block_on(
|
||
self.solver.advance(&mut self.field, self.dt_fluid),
|
||
) {
|
||
Ok(r) => r,
|
||
Err(e) => {
|
||
// The overlap is rebuilt inside `advance`: a refused patch
|
||
// surfaces here, with the last geometry.
|
||
let d = self.last_d.clone();
|
||
self.dump_edges(&d, &format!("advance refused ({e:?})"));
|
||
return Err(e);
|
||
}
|
||
};
|
||
self.last_defects.set((
|
||
r.patch_mass_defect,
|
||
r.background_mass_defect,
|
||
r.patch_max_divergence,
|
||
));
|
||
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(),
|
||
)
|
||
}
|
||
|
||
/// The wall pressure's roughness along the flag: over the flag's
|
||
/// wall faces in order, `Σ|t_n(f+1) − t_n(f)| / Σ|t_n(f)|` with `t_n`
|
||
/// the normal traction, and the count of sign changes of the
|
||
/// face-to-face difference — a checkerboard reads ≫ 1 with a sign
|
||
/// change at every face.
|
||
pub fn wall_roughness(&self) -> (f64, usize, f64, f64) {
|
||
let mut tn: Vec<f64> = Vec::new();
|
||
for (centre, normal, _, 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 {
|
||
tn.push(traction[0] * normal[0] + traction[1] * normal[1]);
|
||
}
|
||
}
|
||
let total: f64 = tn.iter().map(|v| v.abs()).sum();
|
||
let mut jumps = 0.0;
|
||
let mut flips = 0usize;
|
||
let mut prev_diff = 0.0;
|
||
for w in tn.windows(2) {
|
||
let d = w[1] - w[0];
|
||
jumps += d.abs();
|
||
if prev_diff * d < 0.0 {
|
||
flips += 1;
|
||
}
|
||
prev_diff = d;
|
||
}
|
||
let max = tn.iter().fold(0.0_f64, |m, v| m.max(v.abs()));
|
||
(jumps / total.max(1e-300), flips, max, total / tn.len().max(1) as f64)
|
||
}
|
||
|
||
/// The patch's pressure level (mean over its cells) and the wall's
|
||
/// net volume flux from the imposed velocity (`Σ u_wall · S_out` over
|
||
/// the Inner faces, positive = fluid leaving the patch through the
|
||
/// wall), against the inner ring's enclosed-area rate would be the
|
||
/// mesh's own; both must match for a consistent moving wall.
|
||
pub fn level_and_wall_flux(&self) -> (f64, f64, f64, f64, f64) {
|
||
let mesh = self.solver.patch().mesh();
|
||
let p = &self.field.patch.p;
|
||
let level = p.iter().sum::<f64>() / p.len().max(1) as f64;
|
||
let wall = self.shared.read().unwrap();
|
||
let (poly_flux, _) = wall.net_flux();
|
||
let poly_area = wall.area();
|
||
let (mut flux, mut area) = (0.0, 0.0);
|
||
for (f, face) in mesh.faces().iter().enumerate() {
|
||
if mesh.side(f) != Some(PatchSide::Inner) {
|
||
continue;
|
||
}
|
||
// Inner side: S points from the body into the fluid (the
|
||
// neighbour is the cell), so −S is out of the fluid.
|
||
let sign = if face.neigh.is_some() { 1.0 } else { -1.0 };
|
||
let s_in = [sign * face.s[0], sign * face.s[1]];
|
||
let (u, v) = wall.velocity_at(face.centre[0], face.centre[1]);
|
||
// Volume flux INTO the fluid = u_wall · S_into_fluid.
|
||
flux += u * s_in[0] + v * s_in[1];
|
||
area += (s_in[0] * s_in[0] + s_in[1] * s_in[1]).sqrt();
|
||
}
|
||
(level, flux, area, poly_flux, poly_area)
|
||
}
|
||
|
||
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()
|
||
}
|
||
}
|