rtx-fsi: FSI2 flag in-vacuo modal instrument (smodes, test-only, #[ignore])
fsi2_flag_modes_dump writes the march's structure operators (TL SVK tangent at u = 0, plane strain; consistent mass; root-clamped free DoFs) per mesh for an outside eigen-solve; fsi2_flag_free_vibration releases the march's NonlinearDynamicStepper from a mode shape at a given Newmark gamma and records the probe's uy(t). Nothing in the march changes. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
c860783c81
commit
66aab2833c
@@ -0,0 +1,227 @@
|
||||
//! The FSI2 flag's in-vacuo structure model, as the coupled march builds
|
||||
//! it (track 1 round 4, `smodes`): the question is whether the lift
|
||||
//! excess's 5f = 9.67 Hz content is the flag's 4th bending mode, and
|
||||
//! whether the 35×2 Quad8 mesh or the Newmark γ = 0.7 dissipation puts
|
||||
//! that mode where it is.
|
||||
//!
|
||||
//! Both tests are `#[ignore]`d instruments (test-only code; nothing in the
|
||||
//! march changes):
|
||||
//!
|
||||
//! * `fsi2_flag_modes_dump` writes the linearised operators of the
|
||||
//! march's structure — the total-Lagrangian St. Venant–Kirchhoff tangent
|
||||
//! at u = 0 (plane strain, the element's default quadrature) and the
|
||||
//! consistent mass, on the free DoFs of the root-clamped flag — for
|
||||
//! each `SMODES_MESHES` entry (`35x2,70x4,140x8` by default) as COO
|
||||
//! files, for a generalised eigen-solve outside (scipy `eigsh`).
|
||||
//! * `fsi2_flag_free_vibration` releases the march's own
|
||||
//! `NonlinearDynamicStepper` (35×2 by default) from a small-amplitude
|
||||
//! mode shape (`SMODES_MODE`: lines `node_id ux uy`; `SMODES_OMEGA2` =
|
||||
//! its ω² for a consistent start acceleration) at `SMODES_GAMMA` /
|
||||
//! β = (γ + ½)²/4 and `SMODES_DT`, and writes the probe node's uy(t) —
|
||||
//! the measured per-period decay of that mode in the real stepper.
|
||||
|
||||
mod fsi2_harness;
|
||||
|
||||
use std::io::Write as _;
|
||||
|
||||
use fsi2_harness::{FLAG_X0, FSI2, clamp_left, flag_mesh};
|
||||
use nalgebra::{DVector, Vector3};
|
||||
use rtx_fea::analysis::{
|
||||
AnalysisConfig, ConvergenceCriteria, DynamicState, NonlinearDynamicAnalysis,
|
||||
};
|
||||
use rtx_fea::elements::total_lagrangian::{internal_force_and_tangent, saint_venant_kirchhoff};
|
||||
use rtx_fea::elements::{ElementMatrixComputer, StandardFiniteElement};
|
||||
use rtx_fea::materials::{LinearElastic, Material as _, MaterialDatabase};
|
||||
use rtx_fea::mesh::{MaterialId, NodeId};
|
||||
|
||||
fn env_str(name: &str, default: &str) -> String {
|
||||
std::env::var(name).unwrap_or_else(|_| default.to_string())
|
||||
}
|
||||
|
||||
fn env_num(name: &str, default: f64) -> f64 {
|
||||
std::env::var(name)
|
||||
.map(|v| v.parse().expect(name))
|
||||
.unwrap_or(default)
|
||||
}
|
||||
|
||||
fn parse_mesh(spec: &str) -> (usize, usize) {
|
||||
let (a, b) = spec.split_once('x').expect("mesh spec NXxNY");
|
||||
(a.trim().parse().unwrap(), b.trim().parse().unwrap())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "instrument: writes the flag's K and M for an outside eigen-solve"]
|
||||
fn fsi2_flag_modes_dump() {
|
||||
let out = env_str("SMODES_OUT", ".");
|
||||
let meshes = env_str("SMODES_MESHES", "35x2,70x4,140x8");
|
||||
let (lambda, mu) = LinearElastic::new(FSI2.e_s, FSI2.nu_s)
|
||||
.with_density(FSI2.rho_s)
|
||||
.properties()
|
||||
.lame_parameters();
|
||||
println!(
|
||||
"FSI2 flag: E {:.4e} ν {} ρ_s {} → λ {lambda:.6e} μ {mu:.6e} (plane strain SVK)",
|
||||
FSI2.e_s, FSI2.nu_s, FSI2.rho_s
|
||||
);
|
||||
let constitutive = saint_venant_kirchhoff(lambda, mu, 2);
|
||||
for spec in meshes.split(',') {
|
||||
let (nx, ny) = parse_mesh(spec);
|
||||
let mesh = flag_mesh(nx, ny);
|
||||
let mut ids: Vec<NodeId> = mesh.nodes.keys().copied().collect();
|
||||
ids.sort();
|
||||
// Free DoFs: every node off the clamped root edge, (x, y) each.
|
||||
let mut free = std::collections::HashMap::new();
|
||||
let mut dof_lines = Vec::new();
|
||||
for id in &ids {
|
||||
let p = mesh.get_node(*id).unwrap().position();
|
||||
if (p.x - FLAG_X0).abs() < 1e-9 {
|
||||
continue;
|
||||
}
|
||||
for c in 0..2 {
|
||||
free.insert((*id, c), dof_lines.len());
|
||||
dof_lines.push(format!(
|
||||
"{} {} {:.12e} {:.12e} {c}",
|
||||
dof_lines.len(),
|
||||
id.0,
|
||||
p.x,
|
||||
p.y
|
||||
));
|
||||
}
|
||||
}
|
||||
let mut k_trip: std::collections::BTreeMap<(usize, usize), f64> = Default::default();
|
||||
let mut m_trip: std::collections::BTreeMap<(usize, usize), f64> = Default::default();
|
||||
for element in mesh.elements.values() {
|
||||
let coords: Vec<Vector3<f64>> = element
|
||||
.nodes
|
||||
.iter()
|
||||
.map(|id| mesh.get_node(*id).unwrap().position())
|
||||
.collect();
|
||||
let fe = StandardFiniteElement::new(element.element_type, coords.clone());
|
||||
let zero = DVector::zeros(2 * element.nodes.len());
|
||||
let (_, k_e) =
|
||||
internal_force_and_tangent(&fe, &coords, &zero, constitutive.as_ref(), None)
|
||||
.unwrap();
|
||||
let m_s = ElementMatrixComputer::compute_consistent_mass_matrix(
|
||||
&fe, &coords, FSI2.rho_s, None,
|
||||
)
|
||||
.unwrap();
|
||||
let local: Vec<Option<usize>> = element
|
||||
.nodes
|
||||
.iter()
|
||||
.flat_map(|n| [free.get(&(*n, 0)).copied(), free.get(&(*n, 1)).copied()])
|
||||
.collect();
|
||||
for (a, ga) in local.iter().enumerate() {
|
||||
let Some(ga) = ga else { continue };
|
||||
for (b, gb) in local.iter().enumerate() {
|
||||
let Some(gb) = gb else { continue };
|
||||
*k_trip.entry((*ga, *gb)).or_default() += k_e[(a, b)];
|
||||
if a % 2 == b % 2 {
|
||||
*m_trip.entry((*ga, *gb)).or_default() += m_s.matrix[(a / 2, b / 2)];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let tag = format!("{nx}x{ny}");
|
||||
let write = |name: &str, trip: &std::collections::BTreeMap<(usize, usize), f64>| {
|
||||
let mut f = std::fs::File::create(format!("{out}/{name}_{tag}.coo")).unwrap();
|
||||
for ((i, j), v) in trip {
|
||||
if *v != 0.0 {
|
||||
writeln!(f, "{i} {j} {v:.17e}").unwrap();
|
||||
}
|
||||
}
|
||||
};
|
||||
write("k", &k_trip);
|
||||
write("m", &m_trip);
|
||||
std::fs::write(format!("{out}/dofs_{tag}.txt"), dof_lines.join("\n") + "\n").unwrap();
|
||||
let mass: f64 = m_trip
|
||||
.iter()
|
||||
.filter(|((i, _), _)| i % 2 == 0)
|
||||
.map(|(_, v)| v)
|
||||
.sum();
|
||||
println!(
|
||||
" {tag}: {} free DoFs, K nnz {}, M nnz {}, free-node x-mass {mass:.4} kg/m (flag 0.35·0.02·1e4 = 70 minus the root column's share)",
|
||||
dof_lines.len(),
|
||||
k_trip.len(),
|
||||
m_trip.len()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "instrument: the march's stepper released from a mode shape"]
|
||||
fn fsi2_flag_free_vibration() {
|
||||
let (nx, ny) = parse_mesh(&env_str("SMODES_MESH", "35x2"));
|
||||
let gamma = env_num("SMODES_GAMMA", 0.5);
|
||||
let beta = (gamma + 0.5).powi(2) / 4.0;
|
||||
let dt = env_num("SMODES_DT", 1.601e-4);
|
||||
let steps = env_num("SMODES_STEPS", 2000.0) as usize;
|
||||
let amp = env_num("SMODES_AMP", 1e-6);
|
||||
let omega2 = env_num("SMODES_OMEGA2", 0.0);
|
||||
let probe = NodeId(env_num("SMODES_PROBE", 0.0) as usize);
|
||||
let mode_path = std::env::var("SMODES_MODE").expect("SMODES_MODE");
|
||||
let out = std::env::var("SMODES_CSV").expect("SMODES_CSV");
|
||||
|
||||
let mesh = flag_mesh(nx, ny);
|
||||
let mut db = MaterialDatabase::new();
|
||||
db.add_material(
|
||||
MaterialId(0),
|
||||
LinearElastic::new(FSI2.e_s, FSI2.nu_s).with_density(FSI2.rho_s),
|
||||
None,
|
||||
);
|
||||
let analysis = NonlinearDynamicAnalysis::new(
|
||||
mesh.clone(),
|
||||
db,
|
||||
clamp_left(&mesh),
|
||||
dt,
|
||||
1,
|
||||
AnalysisConfig::default(),
|
||||
)
|
||||
.with_total_lagrangian()
|
||||
.with_convergence_criteria(ConvergenceCriteria {
|
||||
max_iterations: 60,
|
||||
..ConvergenceCriteria::default()
|
||||
})
|
||||
.with_newmark_parameters(gamma, beta);
|
||||
let mut stepper = analysis.stepper().unwrap();
|
||||
let rest = stepper.rest_state().unwrap();
|
||||
let n = rest.displacement.len();
|
||||
let mut u = DVector::zeros(n);
|
||||
for line in std::fs::read_to_string(&mode_path).unwrap().lines() {
|
||||
let f: Vec<f64> = line
|
||||
.split_whitespace()
|
||||
.map(|t| t.parse().unwrap())
|
||||
.collect();
|
||||
if f.len() < 3 {
|
||||
continue;
|
||||
}
|
||||
let dofs = stepper.node_dofs(NodeId(f[0] as usize));
|
||||
u[dofs[0]] = amp * f[1];
|
||||
u[dofs[1]] = amp * f[2];
|
||||
}
|
||||
let mut state = DynamicState {
|
||||
acceleration: &u * (-omega2),
|
||||
velocity: DVector::zeros(n),
|
||||
displacement: u,
|
||||
};
|
||||
let probe_dofs = stepper.node_dofs(probe);
|
||||
let mut csv = std::fs::File::create(&out).unwrap();
|
||||
writeln!(csv, "t,uy,ux").unwrap();
|
||||
writeln!(
|
||||
csv,
|
||||
"0,{:.17e},{:.17e}",
|
||||
state.displacement[probe_dofs[1]], state.displacement[probe_dofs[0]]
|
||||
)
|
||||
.unwrap();
|
||||
for k in 1..=steps {
|
||||
let (next, _) = stepper.step(&state).unwrap();
|
||||
state = next;
|
||||
writeln!(
|
||||
csv,
|
||||
"{:.9e},{:.17e},{:.17e}",
|
||||
k as f64 * dt,
|
||||
state.displacement[probe_dofs[1]],
|
||||
state.displacement[probe_dofs[0]]
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
println!("free vibration {nx}x{ny} γ {gamma} β {beta:.4} dt {dt:.4e}: {steps} steps → {out}");
|
||||
}
|
||||
Reference in New Issue
Block a user