rtx-fea + rtx-cfd: the single-step seams FSI2 stands on
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 / 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
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
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 / 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
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
rtx-fea: NonlinearDynamicAnalysis refactored onto a NonlinearDynamicStepper - set_nodal_forces on both (the interface load of a coupling subiteration, replaceable between steps and between subiterations of one step); - step(&DynamicState) is a pure function of the start-of-step state and the current forces - commits nothing, so a partitioned coupling re-runs one Newmark step to the interface fixed point (the piston semantics); - run() marches through the same stepper: one code path, pinned from both ends (linear limit, CSM3, and a new manual-drive == run() assertion); - new test: a nodal step load oscillates about the *static* nonlinear analysis's deflection (cross-code-path, mean within 3%, amplitude 6%), with re-run determinism and force-swap sensitivity asserted mid-march (a one-step response to a force change is ~ beta dt^2 - the first assertion draft demanded 10% and was corrected against the physics). rtx-cfd: the subiteration seam and the moving no-slip closure - EmbeddedPisoSolver::snapshot()/restore() (mask + time + init flag; the mask is now Clone): re-running a fluid step within a subiteration is bit-identical to never having diverted - proven on a moving body with cells flipping in the re-run window; - polygon_interface_velocity: nearest-edge linear interpolation of per-vertex velocities, exact for the linear-along-edge boundary data a finite-element interface hands over - the no-slip closure that replaces FSI1's zero-velocity polygon. Suites: rtx-fea 567, rtx-cfd 325, rtx-fsi piston+transfer - all green. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Lnyrw33Lu6rUhW42E9KHwq
This commit is contained in:
co-authored by
Claude Fable 5
parent
c0f5a86f03
commit
4534d90684
@@ -87,6 +87,14 @@ impl Default for EmbeddedParameters {
|
||||
}
|
||||
}
|
||||
|
||||
/// A snapshot of [`EmbeddedPisoSolver`]'s per-step state, for re-running a
|
||||
/// step within a coupling subiteration. See [`EmbeddedPisoSolver::snapshot`].
|
||||
pub struct EmbeddedSolverState {
|
||||
mask: Option<EmbeddedMask>,
|
||||
time: f64,
|
||||
initialized: bool,
|
||||
}
|
||||
|
||||
/// Result of one embedded PISO step.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EmbeddedResult {
|
||||
@@ -191,6 +199,29 @@ impl EmbeddedPisoSolver {
|
||||
self.time
|
||||
}
|
||||
|
||||
/// Snapshot of the solver's own per-step state — the mask, the
|
||||
/// accumulated time and the initialization flag. A coupling
|
||||
/// subiteration re-runs one step from the same start: clone the
|
||||
/// [`FlowField`], take this snapshot, and [`Self::restore`] both
|
||||
/// before every re-run — otherwise the moving-body path's fresh-cell
|
||||
/// detection compares against the *previous subiteration's* mask
|
||||
/// instead of the committed step-start mask.
|
||||
pub fn snapshot(&self) -> EmbeddedSolverState {
|
||||
EmbeddedSolverState {
|
||||
mask: self.mask.clone(),
|
||||
time: self.time,
|
||||
initialized: self.initialized,
|
||||
}
|
||||
}
|
||||
|
||||
/// Restore a [`Self::snapshot`]. The snapshot is cloned, so one
|
||||
/// snapshot serves any number of re-runs.
|
||||
pub fn restore(&mut self, state: &EmbeddedSolverState) {
|
||||
self.mask = state.mask.clone();
|
||||
self.time = state.time;
|
||||
self.initialized = state.initialized;
|
||||
}
|
||||
|
||||
/// Reset the accumulated time.
|
||||
pub fn set_time(&mut self, t: f64) {
|
||||
self.time = t;
|
||||
|
||||
@@ -174,8 +174,7 @@ impl EmbeddedBody {
|
||||
* 0.5;
|
||||
let ccw = signed_area > 0.0;
|
||||
let sdf_vertices = vertices.clone();
|
||||
let mut body =
|
||||
Self::from_sdf(move |x, y, _| polygon_signed_distance(&sdf_vertices, x, y));
|
||||
let mut body = Self::from_sdf(move |x, y, _| polygon_signed_distance(&sdf_vertices, x, y));
|
||||
let sampler_vertices = vertices;
|
||||
body.sampler = Some(Box::new(move |ds| {
|
||||
let n = sampler_vertices.len();
|
||||
@@ -291,7 +290,6 @@ pub struct SurfaceForce {
|
||||
pub skipped: usize,
|
||||
}
|
||||
|
||||
|
||||
/// Signed distance to a closed polygon (negative inside, either winding):
|
||||
/// minimum distance over the edges, sign by the even-odd ray-crossing rule.
|
||||
/// Public so a coupling loop can build a time-dependent body from a shared,
|
||||
@@ -324,6 +322,50 @@ pub fn polygon_signed_distance(vertices: &[(f64, f64)], x: f64, y: f64) -> f64 {
|
||||
if inside { -dist } else { dist }
|
||||
}
|
||||
|
||||
/// Velocity of the point on a closed polygon nearest to `(x, y)`, where
|
||||
/// the vertices carry velocities: the nearest edge point is found exactly
|
||||
/// as in [`polygon_signed_distance`], and that edge's endpoint velocities
|
||||
/// are interpolated linearly along it. This is the no-slip closure of a
|
||||
/// deforming body whose boundary nodes move with known velocities — exact
|
||||
/// wherever the boundary velocity is linear along an edge, which is what a
|
||||
/// finite-element interface hands over. `velocities` must have one entry
|
||||
/// per vertex.
|
||||
#[must_use]
|
||||
pub fn polygon_interface_velocity(
|
||||
vertices: &[(f64, f64)],
|
||||
velocities: &[(f64, f64)],
|
||||
x: f64,
|
||||
y: f64,
|
||||
) -> (f64, f64) {
|
||||
assert_eq!(
|
||||
vertices.len(),
|
||||
velocities.len(),
|
||||
"one velocity per polygon vertex"
|
||||
);
|
||||
let n = vertices.len();
|
||||
let mut best = (f64::MAX, 0usize, 0.0f64);
|
||||
for k in 0..n {
|
||||
let (ax, ay) = vertices[k];
|
||||
let (bx, by) = vertices[(k + 1) % n];
|
||||
let (ex, ey) = (bx - ax, by - ay);
|
||||
let len2 = ex * ex + ey * ey;
|
||||
let s = if len2 > 0.0 {
|
||||
(((x - ax) * ex + (y - ay) * ey) / len2).clamp(0.0, 1.0)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let (qx, qy) = (ax + s * ex - x, ay + s * ey - y);
|
||||
let d2 = qx * qx + qy * qy;
|
||||
if d2 < best.0 {
|
||||
best = (d2, k, s);
|
||||
}
|
||||
}
|
||||
let (_, k, s) = best;
|
||||
let (vax, vay) = velocities[k];
|
||||
let (vbx, vby) = velocities[(k + 1) % n];
|
||||
(vax + s * (vbx - vax), vay + s * (vby - vay))
|
||||
}
|
||||
|
||||
/// What a velocity face is.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum FaceKind {
|
||||
@@ -369,7 +411,10 @@ struct Ghost {
|
||||
flux_sign: f64,
|
||||
}
|
||||
|
||||
/// Classification of a grid against a body at one instant.
|
||||
/// Classification of a grid against a body at one instant. `Clone` so a
|
||||
/// coupling loop can snapshot the solver's step state and re-run a step
|
||||
/// within a subiteration ([`super::EmbeddedPisoSolver::snapshot`]).
|
||||
#[derive(Clone)]
|
||||
pub struct EmbeddedMask {
|
||||
nx: usize,
|
||||
ny: usize,
|
||||
@@ -1125,6 +1170,28 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interface_velocity_interpolates_along_the_nearest_edge() {
|
||||
// Unit square, CCW; each vertex carries a distinct velocity.
|
||||
let vertices = vec![(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)];
|
||||
let velocities = vec![(0.0, 0.0), (1.0, -1.0), (2.0, 4.0), (3.0, 9.0)];
|
||||
|
||||
// Near the bottom edge at s = 0.25: linear interpolation of the
|
||||
// edge's endpoint velocities, regardless of the offset off the edge.
|
||||
let (u, v) = polygon_interface_velocity(&vertices, &velocities, 0.25, -0.3);
|
||||
assert!((u - 0.25).abs() < 1e-14 && (v + 0.25).abs() < 1e-14);
|
||||
let (u, v) = polygon_interface_velocity(&vertices, &velocities, 0.25, 0.1);
|
||||
assert!((u - 0.25).abs() < 1e-14 && (v + 0.25).abs() < 1e-14);
|
||||
|
||||
// Near a vertex (outside the corner): the vertex velocity.
|
||||
let (u, v) = polygon_interface_velocity(&vertices, &velocities, 1.2, 1.3);
|
||||
assert!((u - 2.0).abs() < 1e-14 && (v - 4.0).abs() < 1e-14);
|
||||
|
||||
// Midpoint of the right edge.
|
||||
let (u, v) = polygon_interface_velocity(&vertices, &velocities, 1.4, 0.5);
|
||||
assert!((u - 1.5).abs() < 1e-14 && (v - 1.5).abs() < 1e-14);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn body_touching_the_boundary_is_refused() {
|
||||
let body = EmbeddedBody::circle(0.0, 0.5, 0.2);
|
||||
|
||||
@@ -39,9 +39,10 @@ pub use ale::{
|
||||
pub use boundary_conditions::{
|
||||
BoundaryCondition, BoundaryConditions, BoundaryLocation, BoundaryType,
|
||||
};
|
||||
pub use embedded::{EmbeddedParameters, EmbeddedPisoSolver, EmbeddedResult};
|
||||
pub use embedded::{EmbeddedParameters, EmbeddedPisoSolver, EmbeddedResult, EmbeddedSolverState};
|
||||
pub use embedded_body::{
|
||||
EmbeddedBody, EmbeddedMask, FaceKind, SurfaceForce, SurfaceSample, polygon_signed_distance,
|
||||
EmbeddedBody, EmbeddedMask, FaceKind, SurfaceForce, SurfaceSample, polygon_interface_velocity,
|
||||
polygon_signed_distance,
|
||||
};
|
||||
pub use flow_field::FlowField;
|
||||
pub use piso::{PisoParameters, PisoResult, PisoSolver};
|
||||
|
||||
Reference in New Issue
Block a user