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-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]>
94 lines
3.0 KiB
Rust
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,
|
|
},
|
|
}
|