From e80b122854e5849b9a7be372a40bf795f7faa64c Mon Sep 17 00:00:00 2001 From: Omar Sobh Date: Thu, 24 Sep 2026 22:36:17 -0500 Subject: [PATCH] =?UTF-8?q?R4-m:=20RTX=5FFSI2O=5FNODE=5FCSV=20=E2=80=94=20?= =?UTF-8?q?per-node=20accepted=20load=20and=20displacement=20along=20the?= =?UTF-8?q?=20flag=20every=20coupled=20step?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each wetted node's (dx, dy, fx, fy) grouped bottom / tip / top (sidecar .nodes.csv: group, node, reference x, y, arc length) plus the centreline uy at s = 1/4 (mean of the two straddling nodes) and 1/2. Default off; gate on the R4-l line slice 6.0 -> 6.2 s (CPU V-cycle, floor 7.5e-8, rtol 1e-3): main vs branch knob-off vs knob-on march CSV and step CSV byte-identical (md5 70dcf484..., dd535386...); the node sums equal the step CSV's fx_nodal / fy_nodal. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../rtx-fsi/tests/fsi2_harness/mod.rs | 1 + .../rtx-fsi/tests/fsi2_harness/node_csv.rs | 121 ++++++++++++++++++ .../tests/fsi2_harness/overset_march.rs | 12 ++ 3 files changed, 134 insertions(+) create mode 100644 crates/specialized/rtx-fsi/tests/fsi2_harness/node_csv.rs diff --git a/crates/specialized/rtx-fsi/tests/fsi2_harness/mod.rs b/crates/specialized/rtx-fsi/tests/fsi2_harness/mod.rs index 0cca613..a1d61e9 100644 --- a/crates/specialized/rtx-fsi/tests/fsi2_harness/mod.rs +++ b/crates/specialized/rtx-fsi/tests/fsi2_harness/mod.rs @@ -11,6 +11,7 @@ #![allow(dead_code)] // several test crates share this; each uses a subset pub mod march; +pub mod node_csv; pub mod overset; pub mod overset_march; pub mod replay; diff --git a/crates/specialized/rtx-fsi/tests/fsi2_harness/node_csv.rs b/crates/specialized/rtx-fsi/tests/fsi2_harness/node_csv.rs new file mode 100644 index 0000000..1fad237 --- /dev/null +++ b/crates/specialized/rtx-fsi/tests/fsi2_harness/node_csv.rs @@ -0,0 +1,121 @@ +//! R4-m (2026-09-24): the per-node load along the flag — +//! `RTX_FSI2O_NODE_CSV=` writes, every committed coupled step, each +//! wetted node's displacement and the load the structure ACCEPTED +//! (`dx, dy, fx, fy`), the nodes grouped bottom (root → tip), tip (bottom +//! corner excluded → top corner excluded, by y), top (tip → root), plus the +//! centreline displacement at s = ¼ and ½ of the flag (the ¼ point lies +//! between two nodes and is their mean). A sidecar `.nodes.csv` +//! names each column block: group, node id, reference (x, y) and the arc +//! length along the wetted walk. Off (the default) nothing is built. + +use std::collections::HashMap; +use std::io::Write as _; + +use nalgebra::Vector3; +use rtx_fea::analysis::DynamicState; +use rtx_fea::mesh::{Mesh, NodeId}; + +use super::{Interface, FLAG_X0, FLAG_X1, FLAG_Y0, FLAG_Y1}; + +pub struct NodeCsv { + file: std::io::BufWriter, + /// (node id, dof x, dof y) in column order. + columns: Vec<(NodeId, usize, usize)>, + /// Centreline probes: (label, [dofs of the nodes averaged]). + probes: Vec<(&'static str, Vec<[usize; 2]>)>, + a_dofs: [usize; 2], +} + +impl NodeCsv { + /// `None` unless `RTX_FSI2O_NODE_CSV` is set. + pub fn from_env( + mesh: &Mesh, + interface: &Interface, + a_dofs: [usize; 2], + node_dofs: impl Fn(NodeId) -> [usize; 2], + ) -> Option { + let path = std::env::var("RTX_FSI2O_NODE_CSV").ok()?; + let (len, thick) = (FLAG_X1 - FLAG_X0, FLAG_Y1 - FLAG_Y0); + let tip_interior = &interface.tip[1..interface.tip.len() - 1]; + let mut order: Vec<(&str, usize)> = Vec::new(); + order.extend(interface.bottom.iter().map(|&k| ("bottom", k))); + order.extend(tip_interior.iter().map(|&k| ("tip", k))); + order.extend(interface.top.iter().map(|&k| ("top", k))); + let mut side = std::fs::File::create(format!("{path}.nodes.csv")).expect("node sidecar"); + writeln!(side, "col,group,node,x,y,s").unwrap(); + let mut columns = Vec::with_capacity(order.len()); + for (c, (group, k)) in order.iter().enumerate() { + let id = interface.wetted[*k]; + let (x, y) = interface.reference[*k]; + let s = match *group { + "bottom" => x - FLAG_X0, + "tip" => len + (y - FLAG_Y0), + _ => len + thick + (FLAG_X1 - x), + }; + writeln!(side, "{c},{group},{},{x:.6},{y:.6},{s:.6}", id.0).unwrap(); + let d = node_dofs(id); + columns.push((id, d[0], d[1])); + } + let yc = 0.5 * (FLAG_Y0 + FLAG_Y1); + let at = |x: f64| -> [usize; 2] { + let id = mesh + .nodes + .iter() + .find(|(_, n)| { + (n.position().x - x).abs() < 1e-9 && (n.position().y - yc).abs() < 1e-9 + }) + .map(|(&id, _)| id) + .expect("centreline node"); + node_dofs(id) + }; + // Quad8 centreline nodes sit every len / (2 nx); s = ¼ is between two. + let n_along = interface.bottom.len(); // 2 nx (the clamp node excluded) + let hx = len / n_along as f64; + let quarter = 0.25 * len / hx; + let (i0, i1) = (quarter.floor() as usize, quarter.ceil() as usize); + let q_nodes = if i0 == i1 { + vec![at(FLAG_X0 + i0 as f64 * hx)] + } else { + vec![at(FLAG_X0 + i0 as f64 * hx), at(FLAG_X0 + i1 as f64 * hx)] + }; + let mid = (0.5 * len / hx).round() as usize; + let probes = vec![("q", q_nodes), ("m", vec![at(FLAG_X0 + mid as f64 * hx)])]; + let mut file = std::io::BufWriter::new(std::fs::File::create(&path).expect("node csv")); + let mut header = String::from("t,ux_A,uy_A,ux_q,uy_q,ux_m,uy_m"); + for c in 0..columns.len() { + header += &format!(",dx{c},dy{c},fx{c},fy{c}"); + } + writeln!(file, "{header}").unwrap(); + println!( + " node CSV (R4-m): {} wetted columns ({} bottom, {} tip, {} top) → {path} (+ .nodes.csv); centreline probes s = ¼ ({} nodes), ½", + columns.len(), + interface.bottom.len(), + tip_interior.len(), + interface.top.len(), + probes[0].1.len() + ); + Some(Self { + file, + columns, + probes, + a_dofs, + }) + } + + pub fn write(&mut self, t: f64, state: &DynamicState, nodal: &[(NodeId, Vector3)]) { + let u = &state.displacement; + let load: HashMap> = nodal.iter().map(|(id, f)| (*id, *f)).collect(); + let mut line = format!("{t:.6},{:.6e},{:.6e}", u[self.a_dofs[0]], u[self.a_dofs[1]]); + for (_, dofs) in &self.probes { + let n = dofs.len() as f64; + let ux: f64 = dofs.iter().map(|d| u[d[0]]).sum::() / n; + let uy: f64 = dofs.iter().map(|d| u[d[1]]).sum::() / n; + line += &format!(",{ux:.6e},{uy:.6e}"); + } + for (id, dx, dy) in &self.columns { + let f = load.get(id).copied().unwrap_or_else(Vector3::zeros); + line += &format!(",{:.5e},{:.5e},{:.5e},{:.5e}", u[*dx], u[*dy], f.x, f.y); + } + writeln!(self.file, "{line}").unwrap(); + } +} diff --git a/crates/specialized/rtx-fsi/tests/fsi2_harness/overset_march.rs b/crates/specialized/rtx-fsi/tests/fsi2_harness/overset_march.rs index 458ac41..a04093e 100644 --- a/crates/specialized/rtx-fsi/tests/fsi2_harness/overset_march.rs +++ b/crates/specialized/rtx-fsi/tests/fsi2_harness/overset_march.rs @@ -368,6 +368,15 @@ pub fn run_march_overset(case: BenchmarkCase, config: &OversetMarchConfig) -> Ov .sqrt() ); + let mut node_csv = super::node_csv::NodeCsv::from_env( + &fluid.mesh, + &fluid.interface, + [a_dofs[0], a_dofs[1]], + |id| { + let d = flag.borrow().node_dofs(id); + [d[0], d[1]] + }, + ); let fluid = RefCell::new(fluid); let mut iqn = (cfg.coupler == "iqn").then(|| { IqnIls::new(cfg.max_subiterations, 1.0) @@ -626,6 +635,9 @@ pub fn run_march_overset(case: BenchmarkCase, config: &OversetMarchConfig) -> Ov ) .unwrap(); } + if let Some(nc) = node_csv.as_mut() { + nc.write(t_now, &flag_state, &committed_nodal); + } interval_drag.push(drag_now); interval_lift.push(lift_now); if (step + 1) % 10 == 0 {