Files
rustytorch/crates/specialized/rtx-fsi/src/error.rs
T
Omar SobhandClaude Opus 5 9be5f4a68f
CI / Build CPU-Only (Explicit) (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
Documentation / Build User Guide (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
Performance Benchmarks / Run Benchmarks (push) Canceled after 0s
rtx-fsi: partitioned fluid-structure coupling
rtx-cfd (18,715 lines) and rtx-fea (36,576 lines) both exist and nothing
connects them -- rtx-fea is commented out of rtx-cfd's dependencies. This
is the coupling layer, and it is the piece Prof. Charbel Farhat's 2026
Guggenheim Medal citation is actually about.

It depends on NEITHER solver. The properties that make a partitioned
coupling correct -- conservation of force, moment and interface work --
are statements about the transfer operators alone, so they can be
validated now, on solvers whose canonical-benchmark validation is still
outstanding. Adapters to the concrete solvers belong above this.

TRANSFER (transfer.rs). Weights satisfy two constraints:
  sum(w_i) = 1            partition of unity  -> force conserved
  sum(w_i x_i) = x_face   linear reproduction -> MOMENT conserved

The second is the one that gets skipped. Inverse-distance weighting
satisfies the first and generally violates the second, conserving force
while corrupting moment -- which shows up as slow spurious rotation rather
than as an obvious error. Underdetermined for >4 nodes, so it takes the
minimum-norm solution w = A^T (A A^T)^+ b.

That is a PSEUDO-inverse, and not for defensiveness. A wetted surface is a
surface, so its nodes are usually planar, and for a planar patch the z
constraint row is an affine multiple of the ones row -- A A^T is genuinely
rank-deficient. The constraint is redundant there, not unsatisfiable. An
ordinary inverse rejects the most ordinary interface there is; I found
this because my first test fixture was collinear and the code correctly
refused it. Constraints are then verified against the weights actually
obtained, since a pseudo-inverse returns a least-squares answer whether or
not the system was consistent.

Motion transfer uses the TRANSPOSE of the load operator, which makes
interface work conserved identically: (Hf).v = f.(H^T v). Any other
pairing leaks energy every step, and the leak looks like physics until it
destabilises.

COUPLING (coupling.rs). Staggered and Aitken-relaxed subiteration. The
decisive tests reproduce the added-mass effect: at a gain of 2.5 the
fixed-relaxation scheme DIVERGES and is reported as CouplingDiverged
rather than as an exhausted budget, and Aitken recovers the same case. A
partitioned coupling that cannot reproduce its own classic failure mode is
not being tested hard enough. Aitken is exact for a linear fixed point, so
convergence is asserted at <=4 iterations -- pinning that this is the real
delta-squared formula and not an under-relaxation that happens to work.

SCOPE, stated up front in the crate docs: small-displacement transpiration
coupling on a fixed mesh. Deliberately not ALE and not embedded-boundary,
so the Discrete Geometric Conservation Law does not yet apply -- the mesh
does not move. Large motion needs an embedded boundary treatment; that is
the next phase, not an oversight.

External comparator named at entry: Turek-Hron FSI2/FSI3, not yet reached.

26 tests written red-first; cargo test/fmt/clippy -D warnings clean.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-19 06:49:36 -07:00

94 lines
3.0 KiB
Rust

//! Errors raised when an interface or a transfer cannot be formed.
use thiserror::Error;
/// Why a coupling operation was refused.
#[derive(Debug, Clone, PartialEq, Error)]
pub enum FsiError {
/// One side of the interface was empty.
#[error("empty interface: {side} has no entries")]
EmptyInterface {
/// Which side was empty.
side: &'static str,
},
/// Too few structure nodes to reproduce a face centroid exactly.
///
/// Linear reproduction in three dimensions imposes four constraints,
/// so at least four independent nodes are needed. Falling back to a
/// weaker interpolation would conserve force and silently corrupt
/// moment, which is worse than refusing.
#[error("interface needs at least {needed} structure nodes for linear reproduction, got {got}")]
InsufficientNodes {
/// Nodes supplied.
got: usize,
/// Nodes required.
needed: usize,
},
/// A field had a different length from the mesh it belongs to.
#[error("{field}: expected {expected} values, got {got}")]
CountMismatch {
/// Which field.
field: &'static str,
/// Length supplied.
got: usize,
/// Length required.
expected: usize,
},
/// A field value was NaN or infinite.
#[error("non-finite value in {field} at index {index}")]
NonFinite {
/// Which field.
field: &'static str,
/// Where.
index: usize,
},
/// A parameter given to a coupling scheme was outside its valid
/// range.
#[error("invalid coupling parameter {parameter}: {value}")]
InvalidParameter {
/// Which parameter.
parameter: &'static str,
/// The value supplied.
value: f64,
},
/// The interface iteration ran away.
///
/// In partitioned coupling this is almost always the added-mass
/// effect: once the fluid's added mass exceeds the structural mass the
/// fixed point becomes repulsive, and a fixed relaxation factor cannot
/// recover it. Dynamic relaxation can.
#[error("coupling diverged after {iterations} iterations (residual {residual})")]
CouplingDiverged {
/// Iterations completed before divergence was detected.
iterations: usize,
/// Residual at that point.
residual: f64,
},
/// The iteration budget ran out before the interface agreed.
#[error(
"coupling did not converge in {iterations} iterations (residual {residual}, tolerance {tolerance})"
)]
CouplingNotConverged {
/// Iterations performed.
iterations: usize,
/// Residual reached.
residual: f64,
/// Tolerance required.
tolerance: f64,
},
/// The interpolation system was singular — the selected nodes are
/// degenerate, for example all collinear.
#[error("degenerate node neighbourhood for face {face}: cannot reproduce its centroid")]
DegenerateNeighbourhood {
/// Which face.
face: usize,
},
}