//! 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(); } }