R4-m: RTX_FSI2O_NODE_CSV — per-node accepted load and displacement along the flag every coupled step
Each wetted node's (dx, dy, fx, fy) grouped bottom / tip / top (sidecar <path>.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) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
d9847cd818
commit
e80b122854
@@ -0,0 +1,121 @@
|
||||
//! R4-m (2026-09-24): the per-node load along the flag —
|
||||
//! `RTX_FSI2O_NODE_CSV=<path>` 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 `<path>.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<std::fs::File>,
|
||||
/// (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<Self> {
|
||||
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<f64>)]) {
|
||||
let u = &state.displacement;
|
||||
let load: HashMap<NodeId, Vector3<f64>> = 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::<f64>() / n;
|
||||
let uy: f64 = dofs.iter().map(|d| u[d[1]]).sum::<f64>() / 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user