The summit rung C1: all the verified pieces joined into the first coupled Turek–Hron computation (rtx-fsi/tests/turek_hron_fsi1.rs). The embedded fluid computes tractions on the DEFORMED flag surface (EmbeddedMask::traction_at, factored from surface_force); the flag's wetted boundary is a polygon whose vertex list sits behind a lock, so the moving-body mask rebuild picks up every shape update (EmbeddedBody::polygon + pub polygon_signed_distance); WettedSurface — rebuilt on the deformed interface every subiteration — carries the loads to the flag's boundary nodes (NonlinearStaticAnalysis::set_nodal_forces); Subiterated::aitken drives the exchange, each pass marching the fluid to flag-load stagnation so the coupling map is a function of geometry, not of the fluid's transient. Result (ny = 62, 6 Aitken passes, 420 s): coupled drag 15.360 (+7.5%, the rigid CFD1 band at this grid), lift 0.7977 (+4.4%), ux(A) 2.647e-5 vs 2.270e-5 (+16.6%; +6.1% at ny = 82), uy(A) 3.90e-4 vs 8.21e-4 at h = 6.6 mm and 1.124e-3 (+37%) at h = 5 mm — the resolutions BRACKET the reference through the flag's 3 -> 4-cell thickness transition, like the rigid-flag lift; conservation 7.4e-12 every pass. Bands asserted are the measured ones; RTX_FSI1_NY runs studies. Two real rtx-fsi defects found by this rung (15th and 16th of the campaign), both regression-tested (tests/transfer_curved_edge.rs): 1. solve_weights built its constraint Gram from RAW coordinates: the condition number grows as (position/spacing)^2 — ~1e4 for a flag edge at x ~ 0.26 with 5 mm spacing — and the 4x4 SVD pseudo-inverse lost enough accuracy that the (correctly strict) partition-of-unity / reproduction verification rejected healthy neighbourhoods: the operator's behaviour depended on WHERE the interface sat. Now centred on the face and scaled by the neighbourhood radius — identical constraints, O(1) conditioning, translation-invariant. 2. A NEARLY collinear neighbourhood (the nearest nodes of a face on a smoothly deformed edge: y is almost linear in x, off by the curvature sagitta) cannot satisfy exact centroid reproduction with bounded weights — the offending singular value is too large to truncate and too small to invert. The recruitment now widens (8 -> 16 -> 32 -> all) until the verified constraints hold; for a thin structure that pulls in the opposite face, exactly the transverse spread the system needs. Findings measured before believed: the transfer is faithful (a strictly local two-node split of the same tractions moved the tip by 2%); the uy error is the sampled lift PROFILE on a 3-cell flag (a uniform distribution of the same net lift bends 4x more), confirmed by the resolution study; TVD limiter chatter (+-0.5% steady load — limited schemes stall short of machine steady state) defeats steady fixed-point coupling, so steady coupled cases run upwind while the time-marched FSI2/FSI3 keep TVD; and the mask never chattered at FSI1's sub-cell amplitude (fluid-cell count constant through every pass). rtx-fsi 29 -> 31 green (lib 27, piston 2, curved-edge 1, FSI1 1). Co-Authored-By: Claude Fable 5 <[email protected]>
42 lines
1.6 KiB
Rust
42 lines
1.6 KiB
Rust
use nalgebra::Vector3;
|
|
use rtx_fsi::{FluidFace, WettedSurface};
|
|
|
|
/// A face on a smoothly curved, nearly collinear edge (the deformed-flag
|
|
/// neighbourhood that broke the fixed-count recruitment): the transfer must
|
|
/// widen the neighbourhood until the opposite face's nodes give it a
|
|
/// two-dimensional spread, and the verified constraints must then hold.
|
|
#[test]
|
|
fn nearly_collinear_curved_edge_recruits_wider() {
|
|
let mut nodes = Vec::new();
|
|
let mut x = 0.255;
|
|
// Deformed edges: y = y0 + kappa * (x - 0.25)^2 with the FSI1 scale.
|
|
while x < 0.6 + 1e-9 {
|
|
let bend = 0.013 * (x - 0.25) * (x - 0.25) / 2.0;
|
|
nodes.push(Vector3::new(x, 0.19 + bend, 0.0));
|
|
nodes.push(Vector3::new(x, 0.21 + bend, 0.0));
|
|
x += 0.005;
|
|
}
|
|
let bend = |x: f64| 0.013 * (x - 0.25) * (x - 0.25) / 2.0;
|
|
let faces: Vec<FluidFace> = (0..60)
|
|
.map(|k| {
|
|
let x = 0.26 + 0.005 * k as f64 + 0.00125;
|
|
FluidFace {
|
|
centroid: Vector3::new(x, 0.19 + bend(x), 0.0),
|
|
normal: Vector3::new(0.0, -1.0, 0.0),
|
|
area: 0.0025,
|
|
}
|
|
})
|
|
.collect();
|
|
let surface = WettedSurface::build(&faces, &nodes).expect("adaptive recruitment must succeed");
|
|
for face in 0..faces.len() {
|
|
let weights = surface.weights_for(face);
|
|
let unity: f64 = weights.iter().map(|(_, w)| w).sum();
|
|
assert!((unity - 1.0).abs() < 1e-9, "face {face}: unity {unity}");
|
|
let max_weight = weights.iter().map(|(_, w)| w.abs()).fold(0.0, f64::max);
|
|
assert!(
|
|
max_weight < 100.0,
|
|
"face {face}: weight blow-up {max_weight}"
|
|
);
|
|
}
|
|
}
|