use std::f64::consts::PI;
fn dot(a: &[f64], b: &[f64]) -> f64 {
a.iter().zip(b.iter()).map(|(x, y)| x * y).sum()
}
fn scale_vec(v: &[f64], s: f64) -> Vec<f64> {
v.iter().map(|x| x * s).collect()
}
#[derive(Debug, Clone)]
pub struct InterfaceNode {
pub global_id: usize,
pub position: [f64; 3],
pub normal: [f64; 3],
pub area_weight: f64,
}
#[derive(Debug, Clone)]
pub struct FsiInterface {
pub structural_nodes: Vec<InterfaceNode>,
pub fluid_nodes: Vec<InterfaceNode>,
pub conforming: bool,
}
impl FsiInterface {
pub fn new(conforming: bool) -> Self {
Self {
structural_nodes: Vec::new(),
fluid_nodes: Vec::new(),
conforming,
}
}
pub fn add_structural_node(&mut self, node: InterfaceNode) {
self.structural_nodes.push(node);
}
pub fn add_fluid_node(&mut self, node: InterfaceNode) {
self.fluid_nodes.push(node);
}
pub fn total_area(&self) -> f64 {
self.structural_nodes.iter().map(|n| n.area_weight).sum()
}
pub fn transfer_displacements(&self, structural_disp: &[f64]) -> Vec<f64> {
let ns = self.structural_nodes.len();
if ns == 0 {
return vec![0.0; 3 * self.fluid_nodes.len()];
}
let mut result = Vec::with_capacity(3 * self.fluid_nodes.len());
for fnode in &self.fluid_nodes {
let mut best = 0usize;
let mut best_d = f64::MAX;
for (si, snode) in self.structural_nodes.iter().enumerate() {
let dx = fnode.position[0] - snode.position[0];
let dy = fnode.position[1] - snode.position[1];
let dz = fnode.position[2] - snode.position[2];
let d = (dx * dx + dy * dy + dz * dz).sqrt();
if d < best_d {
best_d = d;
best = si;
}
}
let base = best * 3;
if base + 2 < structural_disp.len() {
result.push(structural_disp[base]);
result.push(structural_disp[base + 1]);
result.push(structural_disp[base + 2]);
} else {
result.extend_from_slice(&[0.0, 0.0, 0.0]);
}
}
result
}
pub fn transfer_traction(&self, fluid_pressure: &[f64]) -> Vec<f64> {
let nf = self.fluid_nodes.len();
let mut loads = vec![0.0f64; 3 * self.structural_nodes.len()];
for (si, snode) in self.structural_nodes.iter().enumerate() {
let mut best = 0usize;
let mut best_d = f64::MAX;
for (fi, fnode) in self.fluid_nodes.iter().enumerate() {
let dx = snode.position[0] - fnode.position[0];
let dy = snode.position[1] - fnode.position[1];
let dz = snode.position[2] - fnode.position[2];
let d = (dx * dx + dy * dy + dz * dz).sqrt();
if d < best_d {
best_d = d;
best = fi;
}
}
let p = if best < nf {
fluid_pressure[best.min(fluid_pressure.len().saturating_sub(1))]
} else {
0.0
};
let base = si * 3;
loads[base] -= p * snode.normal[0] * snode.area_weight;
loads[base + 1] -= p * snode.normal[1] * snode.area_weight;
loads[base + 2] -= p * snode.normal[2] * snode.area_weight;
}
loads
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum AleBlend {
Lagrangian,
Eulerian,
Linear(f64),
}
#[derive(Debug, Clone)]
pub struct AleMapping {
pub ref_positions: Vec<f64>,
pub cur_positions: Vec<f64>,
pub n_nodes: usize,
pub blend: AleBlend,
}
impl AleMapping {
pub fn new(positions: Vec<f64>, blend: AleBlend) -> Self {
let n_nodes = positions.len() / 3;
Self {
cur_positions: positions.clone(),
ref_positions: positions,
n_nodes,
blend,
}
}
pub fn update(&mut self, displacements: &[f64]) {
for ((cur, &ref_pos), &disp) in self
.cur_positions
.iter_mut()
.zip(self.ref_positions.iter())
.zip(displacements.iter())
{
*cur = ref_pos + disp;
}
}
pub fn theta(&self) -> f64 {
match self.blend {
AleBlend::Lagrangian => 1.0,
AleBlend::Eulerian => 0.0,
AleBlend::Linear(t) => t.clamp(0.0, 1.0),
}
}
pub fn mesh_displacement(&self, structural_disp: &[f64]) -> Vec<f64> {
let t = self.theta();
structural_disp.iter().map(|d| t * d).collect()
}
pub fn convective_factor(&self) -> f64 {
1.0 - self.theta()
}
}
#[derive(Debug, Clone)]
pub struct MeshVelocity {
pub prev_disp: Vec<f64>,
pub cur_disp: Vec<f64>,
pub theta: f64,
}
impl MeshVelocity {
pub fn new(n_dof: usize, theta: f64) -> Self {
Self {
prev_disp: vec![0.0; n_dof],
cur_disp: vec![0.0; n_dof],
theta: theta.clamp(0.0, 1.0),
}
}
pub fn advance(&mut self, new_disp: Vec<f64>) {
self.prev_disp = std::mem::replace(&mut self.cur_disp, new_disp);
}
pub fn velocity(&self, dt: f64) -> Vec<f64> {
let inv_dt = if dt.abs() > 1e-300 { 1.0 / dt } else { 0.0 };
self.cur_disp
.iter()
.zip(self.prev_disp.iter())
.map(|(c, p)| self.theta * (c - p) * inv_dt)
.collect()
}
pub fn max_velocity_norm(&self, dt: f64) -> f64 {
let v = self.velocity(dt);
v.chunks(3)
.map(|c| {
let s: f64 = c.iter().map(|x| x * x).sum();
s.sqrt()
})
.fold(0.0f64, f64::max)
}
}
#[derive(Debug, Clone)]
pub struct AddedMass {
pub coefficient: f64,
pub fluid_density: f64,
pub displaced_volume: f64,
}
impl AddedMass {
pub fn new(coefficient: f64, fluid_density: f64, displaced_volume: f64) -> Self {
Self {
coefficient,
fluid_density,
displaced_volume,
}
}
pub fn scalar_mass(&self) -> f64 {
self.coefficient * self.fluid_density * self.displaced_volume
}
pub fn assemble_diagonal(&self, n_dof: usize) -> Vec<f64> {
let m_a = self.scalar_mass();
let n_nodes = if n_dof >= 3 { n_dof / 3 } else { 1 };
let per_node = if n_nodes > 0 {
m_a / n_nodes as f64
} else {
0.0
};
vec![per_node; n_dof]
}
pub fn effective_acceleration(&self, force: f64, structural_mass: f64) -> f64 {
let total = structural_mass + self.scalar_mass();
if total.abs() < 1e-300 {
0.0
} else {
force / total
}
}
}
#[derive(Debug, Clone)]
pub struct HydroelasticPressure {
pub nodal_pressure: Vec<f64>,
pub normals: Vec<f64>,
pub area_weights: Vec<f64>,
}
impl HydroelasticPressure {
pub fn new(nodal_pressure: Vec<f64>, normals: Vec<f64>, area_weights: Vec<f64>) -> Self {
Self {
nodal_pressure,
normals,
area_weights,
}
}
pub fn load_vector(&self) -> Vec<f64> {
let n = self.nodal_pressure.len();
let mut f = vec![0.0f64; 3 * n];
for i in 0..n {
let p = self.nodal_pressure[i];
let aw = if i < self.area_weights.len() {
self.area_weights[i]
} else {
1.0
};
for d in 0..3 {
let ni = if 3 * i + d < self.normals.len() {
self.normals[3 * i + d]
} else {
0.0
};
f[3 * i + d] = -p * ni * aw;
}
}
f
}
pub fn resultant_force(&self) -> [f64; 3] {
let f = self.load_vector();
let mut res = [0.0f64; 3];
for chunk in f.chunks(3) {
if chunk.len() == 3 {
res[0] += chunk[0];
res[1] += chunk[1];
res[2] += chunk[2];
}
}
res
}
pub fn rms_pressure(&self) -> f64 {
let n = self.nodal_pressure.len();
if n == 0 {
return 0.0;
}
let sum_sq: f64 = self.nodal_pressure.iter().map(|p| p * p).sum();
(sum_sq / n as f64).sqrt()
}
}
#[derive(Debug, Clone)]
pub struct FsiConvergenceMonitor {
pub disp_tol: f64,
pub pressure_tol: f64,
pub max_iter: usize,
pub disp_history: Vec<f64>,
pub pressure_history: Vec<f64>,
}
impl FsiConvergenceMonitor {
pub fn new(disp_tol: f64, pressure_tol: f64, max_iter: usize) -> Self {
Self {
disp_tol,
pressure_tol,
max_iter,
disp_history: Vec::new(),
pressure_history: Vec::new(),
}
}
pub fn record(&mut self, disp_residual: f64, pressure_residual: f64) {
self.disp_history.push(disp_residual);
self.pressure_history.push(pressure_residual);
}
pub fn converged(&self) -> bool {
if let (Some(&dr), Some(&pr)) = (self.disp_history.last(), self.pressure_history.last()) {
dr < self.disp_tol && pr < self.pressure_tol
} else {
false
}
}
pub fn max_iter_reached(&self) -> bool {
self.disp_history.len() >= self.max_iter
}
pub fn disp_convergence_rate(&self) -> Option<f64> {
let n = self.disp_history.len();
if n >= 2 {
let prev = self.disp_history[n - 2];
let curr = self.disp_history[n - 1];
if prev.abs() > 1e-300 {
Some(curr / prev)
} else {
None
}
} else {
None
}
}
pub fn reset(&mut self) {
self.disp_history.clear();
self.pressure_history.clear();
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum CouplingScheme {
Sequential,
Staggered,
StrongCoupling,
}
#[derive(Debug, Clone)]
pub struct PartitionedFsi {
pub interface: FsiInterface,
pub monitor: FsiConvergenceMonitor,
pub scheme: CouplingScheme,
pub omega: f64,
pub use_aitken: bool,
pub prev_delta: Vec<f64>,
}
impl PartitionedFsi {
pub fn new(
interface: FsiInterface,
scheme: CouplingScheme,
disp_tol: f64,
pressure_tol: f64,
max_iter: usize,
) -> Self {
Self {
interface,
monitor: FsiConvergenceMonitor::new(disp_tol, pressure_tol, max_iter),
scheme,
omega: 0.5,
use_aitken: false,
prev_delta: Vec::new(),
}
}
pub fn aitken_relax(&mut self, delta: &[f64]) -> Vec<f64> {
if self.prev_delta.is_empty() || self.prev_delta.len() != delta.len() {
self.prev_delta = delta.to_vec();
return scale_vec(delta, self.omega);
}
let diff: Vec<f64> = delta
.iter()
.zip(self.prev_delta.iter())
.map(|(a, b)| a - b)
.collect();
let denom = dot(&diff, &diff);
if denom > 1e-300 {
let num = dot(&self.prev_delta, &diff);
self.omega = (-self.omega * num / denom).clamp(0.1, 1.0);
}
self.prev_delta = delta.to_vec();
scale_vec(delta, self.omega)
}
pub fn requires_inner_loop(&self) -> bool {
matches!(self.scheme, CouplingScheme::StrongCoupling)
}
pub fn step(&mut self, disp_res: f64, pres_res: f64) -> bool {
self.monitor.record(disp_res, pres_res);
self.monitor.converged() || self.monitor.max_iter_reached()
}
}
#[derive(Debug, Clone)]
pub struct MonolithicFsi {
pub n_structural_dof: usize,
pub n_fluid_pressure_dof: usize,
pub n_fluid_velocity_dof: usize,
pub tau_stab: f64,
}
impl MonolithicFsi {
pub fn new(
n_structural_dof: usize,
n_fluid_pressure_dof: usize,
n_fluid_velocity_dof: usize,
tau_stab: f64,
) -> Self {
Self {
n_structural_dof,
n_fluid_pressure_dof,
n_fluid_velocity_dof,
tau_stab,
}
}
pub fn total_dof(&self) -> usize {
self.n_structural_dof + self.n_fluid_pressure_dof + self.n_fluid_velocity_dof
}
pub fn block_offsets(&self) -> (usize, usize, usize) {
let s = 0;
let p = s + self.n_structural_dof;
let v = p + self.n_fluid_pressure_dof;
(s, p, v)
}
pub fn zero_matrix(&self) -> Vec<f64> {
let n = self.total_dof();
vec![0.0f64; n * n]
}
}
#[derive(Debug, Clone)]
pub struct StructuralDamping {
pub alpha: f64,
pub beta: f64,
}
impl StructuralDamping {
pub fn new(alpha: f64, beta: f64) -> Self {
Self { alpha, beta }
}
pub fn from_frequencies(omega1: f64, omega2: f64, zeta1: f64, zeta2: f64) -> Self {
let denom = omega2 * omega2 - omega1 * omega1;
if denom.abs() < 1e-300 {
return Self::new(0.0, 0.0);
}
let alpha = 2.0 * omega1 * omega2 * (zeta1 * omega2 - zeta2 * omega1) / denom;
let beta = 2.0 * (zeta2 * omega2 - zeta1 * omega1) / denom;
Self { alpha, beta }
}
pub fn damping_ratio(&self, omega: f64) -> f64 {
if omega.abs() < 1e-300 {
return 0.0;
}
self.alpha / (2.0 * omega) + self.beta * omega / 2.0
}
pub fn damping_force(&self, mv: &[f64], kv: &[f64]) -> Vec<f64> {
mv.iter()
.zip(kv.iter())
.map(|(m, k)| self.alpha * m + self.beta * k)
.collect()
}
pub fn assemble_matrix(&self, mass_mat: &[f64], stiff_mat: &[f64]) -> Vec<f64> {
mass_mat
.iter()
.zip(stiff_mat.iter())
.map(|(m, k)| self.alpha * m + self.beta * k)
.collect()
}
}
#[derive(Debug, Clone)]
pub struct VortexInducedVibration {
pub diameter: f64,
pub fn_struct: f64,
pub strouhal: f64,
pub zeta: f64,
pub mass_ratio: f64,
}
impl VortexInducedVibration {
pub fn new(diameter: f64, fn_struct: f64, strouhal: f64, zeta: f64, mass_ratio: f64) -> Self {
Self {
diameter,
fn_struct,
strouhal,
zeta,
mass_ratio,
}
}
pub fn reduced_velocity(&self, flow_velocity: f64) -> f64 {
if self.fn_struct.abs() < 1e-300 || self.diameter.abs() < 1e-300 {
return 0.0;
}
flow_velocity / (self.fn_struct * self.diameter)
}
pub fn shedding_frequency(&self, flow_velocity: f64) -> f64 {
if self.diameter.abs() < 1e-300 {
return 0.0;
}
self.strouhal * flow_velocity / self.diameter
}
pub fn is_lock_in(&self, flow_velocity: f64) -> bool {
let f_vs = self.shedding_frequency(flow_velocity);
if self.fn_struct.abs() < 1e-300 {
return false;
}
(f_vs - self.fn_struct).abs() / self.fn_struct < 0.1
}
pub fn lock_in_range(&self) -> (f64, f64) {
let st = self.strouhal;
if st.abs() < 1e-300 {
return (0.0, 0.0);
}
(1.0 / (st * 1.1), 1.0 / (st * 0.9))
}
pub fn peak_amplitude(&self) -> f64 {
let sg = 2.0 * PI * self.zeta * self.mass_ratio;
1.29 * (-0.41 * sg.powf(0.36)).exp()
}
pub fn scruton_number(&self) -> f64 {
2.0 * PI * self.mass_ratio * self.zeta
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_node(id: usize, x: f64, y: f64, z: f64) -> InterfaceNode {
InterfaceNode {
global_id: id,
position: [x, y, z],
normal: [0.0, 0.0, 1.0],
area_weight: 0.25,
}
}
#[test]
fn test_interface_total_area() {
let mut iface = FsiInterface::new(true);
iface.add_structural_node(make_node(0, 0.0, 0.0, 0.0));
iface.add_structural_node(make_node(1, 1.0, 0.0, 0.0));
assert!((iface.total_area() - 0.5).abs() < 1e-12);
}
#[test]
fn test_interface_empty_area() {
let iface = FsiInterface::new(false);
assert_eq!(iface.total_area(), 0.0);
}
#[test]
fn test_transfer_displacements_conforming() {
let mut iface = FsiInterface::new(true);
iface.add_structural_node(make_node(0, 0.0, 0.0, 0.0));
iface.add_structural_node(make_node(1, 1.0, 0.0, 0.0));
iface.add_fluid_node(make_node(0, 0.05, 0.0, 0.0)); let disp = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
let result = iface.transfer_displacements(&disp);
assert_eq!(result.len(), 3);
assert!((result[0] - 1.0).abs() < 1e-12);
assert!((result[1] - 2.0).abs() < 1e-12);
assert!((result[2] - 3.0).abs() < 1e-12);
}
#[test]
fn test_transfer_displacements_empty() {
let iface = FsiInterface::new(true);
let result = iface.transfer_displacements(&[1.0, 2.0, 3.0]);
assert_eq!(result.len(), 0);
}
#[test]
fn test_transfer_traction_zero_pressure() {
let mut iface = FsiInterface::new(true);
iface.add_structural_node(make_node(0, 0.0, 0.0, 0.0));
iface.add_fluid_node(make_node(0, 0.0, 0.0, 0.0));
let loads = iface.transfer_traction(&[0.0]);
assert_eq!(loads.len(), 3);
assert!(loads.iter().all(|&x| x.abs() < 1e-15));
}
#[test]
fn test_transfer_traction_unit_pressure() {
let mut iface = FsiInterface::new(true);
let mut snode = make_node(0, 0.0, 0.0, 0.0);
snode.normal = [0.0, 0.0, 1.0];
snode.area_weight = 1.0;
iface.add_structural_node(snode);
iface.add_fluid_node(make_node(0, 0.0, 0.0, 0.0));
let loads = iface.transfer_traction(&[1000.0]);
assert!((loads[2] - (-1000.0)).abs() < 1e-9);
}
#[test]
fn test_ale_theta_lagrangian() {
let ale = AleMapping::new(vec![0.0; 6], AleBlend::Lagrangian);
assert_eq!(ale.theta(), 1.0);
}
#[test]
fn test_ale_theta_eulerian() {
let ale = AleMapping::new(vec![0.0; 6], AleBlend::Eulerian);
assert_eq!(ale.theta(), 0.0);
}
#[test]
fn test_ale_theta_linear() {
let ale = AleMapping::new(vec![0.0; 6], AleBlend::Linear(0.7));
assert!((ale.theta() - 0.7).abs() < 1e-12);
}
#[test]
fn test_ale_mesh_displacement_lagrangian() {
let ale = AleMapping::new(vec![0.0; 6], AleBlend::Lagrangian);
let d = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
let md = ale.mesh_displacement(&d);
assert_eq!(md, d);
}
#[test]
fn test_ale_mesh_displacement_eulerian() {
let ale = AleMapping::new(vec![0.0; 6], AleBlend::Eulerian);
let d = vec![1.0, 2.0, 3.0];
let md = ale.mesh_displacement(&d);
assert!(md.iter().all(|&x| x.abs() < 1e-15));
}
#[test]
fn test_ale_update() {
let mut ale = AleMapping::new(vec![0.0, 0.0, 0.0], AleBlend::Lagrangian);
ale.update(&[1.0, 2.0, 3.0]);
assert!((ale.cur_positions[2] - 3.0).abs() < 1e-12);
}
#[test]
fn test_ale_convective_factor() {
let ale = AleMapping::new(vec![], AleBlend::Linear(0.3));
assert!((ale.convective_factor() - 0.7).abs() < 1e-12);
}
#[test]
fn test_mesh_velocity_zero_initial() {
let mv = MeshVelocity::new(6, 1.0);
let v = mv.velocity(0.01);
assert!(v.iter().all(|&x| x.abs() < 1e-15));
}
#[test]
fn test_mesh_velocity_after_advance() {
let mut mv = MeshVelocity::new(3, 1.0);
mv.advance(vec![0.01, 0.02, 0.03]);
let v = mv.velocity(0.01);
assert!((v[0] - 1.0).abs() < 1e-9);
assert!((v[1] - 2.0).abs() < 1e-9);
assert!((v[2] - 3.0).abs() < 1e-9);
}
#[test]
fn test_mesh_velocity_theta_half() {
let mut mv = MeshVelocity::new(3, 0.5);
mv.advance(vec![0.02, 0.0, 0.0]);
let v = mv.velocity(0.01);
assert!((v[0] - 1.0).abs() < 1e-9);
}
#[test]
fn test_mesh_velocity_zero_dt() {
let mv = MeshVelocity::new(3, 1.0);
let v = mv.velocity(0.0);
assert!(v.iter().all(|&x| x.abs() < 1e-15));
}
#[test]
fn test_mesh_velocity_max_norm() {
let mut mv = MeshVelocity::new(3, 1.0);
mv.advance(vec![0.03, 0.04, 0.0]);
let vmax = mv.max_velocity_norm(0.01);
assert!((vmax - 5.0).abs() < 1e-9);
}
#[test]
fn test_added_mass_scalar() {
let am = AddedMass::new(1.0, 1000.0, 0.5);
assert!((am.scalar_mass() - 500.0).abs() < 1e-9);
}
#[test]
fn test_added_mass_coefficient() {
let am = AddedMass::new(0.5, 1000.0, 1.0);
assert!((am.scalar_mass() - 500.0).abs() < 1e-9);
}
#[test]
fn test_added_mass_diagonal_sum() {
let am = AddedMass::new(1.0, 1000.0, 0.3);
let d = am.assemble_diagonal(9);
let total: f64 = d.iter().sum();
let n_nodes = 3usize;
let expected = am.scalar_mass() / n_nodes as f64 * 9.0;
assert!((total - expected).abs() < 1e-6);
}
#[test]
fn test_added_mass_effective_acceleration() {
let am = AddedMass::new(1.0, 1000.0, 1.0);
let a = am.effective_acceleration(2000.0, 1000.0);
assert!((a - 1.0).abs() < 1e-9); }
#[test]
fn test_added_mass_zero_density() {
let am = AddedMass::new(1.0, 0.0, 1.0);
assert_eq!(am.scalar_mass(), 0.0);
}
#[test]
fn test_hydroelastic_load_vector_length() {
let hp = HydroelasticPressure::new(
vec![100.0, 200.0],
vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0],
vec![0.5, 0.5],
);
let f = hp.load_vector();
assert_eq!(f.len(), 6);
}
#[test]
fn test_hydroelastic_load_vector_values() {
let hp = HydroelasticPressure::new(vec![1000.0], vec![0.0, 0.0, 1.0], vec![1.0]);
let f = hp.load_vector();
assert!((f[2] - (-1000.0)).abs() < 1e-9);
}
#[test]
fn test_hydroelastic_resultant_force() {
let hp = HydroelasticPressure::new(
vec![500.0, 500.0],
vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0],
vec![1.0, 1.0],
);
let res = hp.resultant_force();
assert!((res[2] - (-1000.0)).abs() < 1e-9);
}
#[test]
fn test_hydroelastic_rms_pressure() {
let hp = HydroelasticPressure::new(vec![3.0, 4.0], vec![0.0; 6], vec![1.0, 1.0]);
let rms = hp.rms_pressure();
assert!((rms - (12.5f64).sqrt()).abs() < 1e-9);
}
#[test]
fn test_hydroelastic_empty() {
let hp = HydroelasticPressure::new(vec![], vec![], vec![]);
assert_eq!(hp.rms_pressure(), 0.0);
assert_eq!(hp.load_vector().len(), 0);
}
#[test]
fn test_monitor_not_converged_initially() {
let monitor = FsiConvergenceMonitor::new(1e-6, 1e-6, 10);
assert!(!monitor.converged());
}
#[test]
fn test_monitor_converged() {
let mut monitor = FsiConvergenceMonitor::new(1e-4, 1e-4, 10);
monitor.record(1e-5, 1e-5);
assert!(monitor.converged());
}
#[test]
fn test_monitor_not_converged() {
let mut monitor = FsiConvergenceMonitor::new(1e-6, 1e-6, 10);
monitor.record(1e-3, 1e-3);
assert!(!monitor.converged());
}
#[test]
fn test_monitor_max_iter() {
let mut monitor = FsiConvergenceMonitor::new(1e-12, 1e-12, 3);
for _ in 0..3 {
monitor.record(1.0, 1.0);
}
assert!(monitor.max_iter_reached());
}
#[test]
fn test_monitor_convergence_rate() {
let mut monitor = FsiConvergenceMonitor::new(1e-6, 1e-6, 10);
monitor.record(1.0, 0.5);
monitor.record(0.5, 0.25);
let rate = monitor.disp_convergence_rate().unwrap();
assert!((rate - 0.5).abs() < 1e-12);
}
#[test]
fn test_monitor_reset() {
let mut monitor = FsiConvergenceMonitor::new(1e-6, 1e-6, 10);
monitor.record(1.0, 1.0);
monitor.reset();
assert!(monitor.disp_history.is_empty());
assert!(!monitor.converged());
}
#[test]
fn test_partitioned_fsi_requires_inner_loop() {
let iface = FsiInterface::new(true);
let fsi = PartitionedFsi::new(
iface.clone(),
CouplingScheme::StrongCoupling,
1e-6,
1e-6,
10,
);
assert!(fsi.requires_inner_loop());
let fsi2 = PartitionedFsi::new(iface, CouplingScheme::Sequential, 1e-6, 1e-6, 10);
assert!(!fsi2.requires_inner_loop());
}
#[test]
fn test_partitioned_fsi_step_convergence() {
let iface = FsiInterface::new(true);
let mut fsi = PartitionedFsi::new(iface, CouplingScheme::Staggered, 1e-4, 1e-4, 10);
let done = fsi.step(1e-5, 1e-5);
assert!(done); }
#[test]
fn test_aitken_relax_first_step() {
let iface = FsiInterface::new(true);
let mut fsi = PartitionedFsi::new(iface, CouplingScheme::StrongCoupling, 1e-6, 1e-6, 10);
fsi.omega = 0.5;
let delta = vec![2.0, 4.0, 6.0];
let relaxed = fsi.aitken_relax(&delta);
assert!((relaxed[0] - 1.0).abs() < 1e-12);
assert!((relaxed[1] - 2.0).abs() < 1e-12);
}
#[test]
fn test_monolithic_total_dof() {
let mfsi = MonolithicFsi::new(30, 10, 20, 0.1);
assert_eq!(mfsi.total_dof(), 60);
}
#[test]
fn test_monolithic_block_offsets() {
let mfsi = MonolithicFsi::new(30, 10, 20, 0.1);
let (s, p, v) = mfsi.block_offsets();
assert_eq!(s, 0);
assert_eq!(p, 30);
assert_eq!(v, 40);
}
#[test]
fn test_monolithic_zero_matrix_size() {
let mfsi = MonolithicFsi::new(5, 3, 2, 0.1);
let m = mfsi.zero_matrix();
assert_eq!(m.len(), 100); assert!(m.iter().all(|&x| x == 0.0));
}
#[test]
fn test_rayleigh_damping_from_frequencies() {
let sd = StructuralDamping::from_frequencies(10.0, 100.0, 0.05, 0.05);
let z1 = sd.damping_ratio(10.0);
let z2 = sd.damping_ratio(100.0);
assert!((z1 - 0.05).abs() < 1e-6, "z1={}", z1);
assert!((z2 - 0.05).abs() < 1e-6, "z2={}", z2);
}
#[test]
fn test_rayleigh_damping_force() {
let sd = StructuralDamping::new(2.0, 0.01);
let mv = vec![1.0, 2.0, 3.0];
let kv = vec![4.0, 5.0, 6.0];
let f = sd.damping_force(&mv, &kv);
assert!((f[0] - (2.0 * 1.0 + 0.01 * 4.0)).abs() < 1e-12);
}
#[test]
fn test_rayleigh_damping_assemble_matrix() {
let sd = StructuralDamping::new(1.0, 1.0);
let m = vec![1.0, 0.0, 0.0, 1.0];
let k = vec![2.0, 0.0, 0.0, 2.0];
let c = sd.assemble_matrix(&m, &k);
assert!((c[0] - 3.0).abs() < 1e-12);
assert!((c[3] - 3.0).abs() < 1e-12);
}
#[test]
fn test_rayleigh_damping_zero_omega() {
let sd = StructuralDamping::new(2.0, 0.01);
assert_eq!(sd.damping_ratio(0.0), 0.0);
}
#[test]
fn test_rayleigh_from_equal_frequencies() {
let sd = StructuralDamping::from_frequencies(10.0, 10.0, 0.05, 0.05);
assert_eq!(sd.alpha, 0.0);
assert_eq!(sd.beta, 0.0);
}
#[test]
fn test_viv_reduced_velocity() {
let viv = VortexInducedVibration::new(0.1, 5.0, 0.2, 0.01, 2.0);
let ur = viv.reduced_velocity(1.0);
assert!((ur - 2.0).abs() < 1e-9);
}
#[test]
fn test_viv_shedding_frequency() {
let viv = VortexInducedVibration::new(0.1, 5.0, 0.2, 0.01, 2.0);
let fvs = viv.shedding_frequency(1.0);
assert!((fvs - 2.0).abs() < 1e-9); }
#[test]
fn test_viv_lock_in_yes() {
let viv = VortexInducedVibration::new(0.1, 2.0, 0.2, 0.01, 2.0);
assert!(viv.is_lock_in(1.0));
}
#[test]
fn test_viv_lock_in_no() {
let viv = VortexInducedVibration::new(0.1, 2.0, 0.2, 0.01, 2.0);
assert!(!viv.is_lock_in(10.0));
}
#[test]
fn test_viv_lock_in_range() {
let viv = VortexInducedVibration::new(0.1, 2.0, 0.2, 0.01, 2.0);
let (lo, hi) = viv.lock_in_range();
assert!(lo < hi);
let mid = (lo + hi) / 2.0;
assert!(lo < mid && mid < hi);
}
#[test]
fn test_viv_peak_amplitude_positive() {
let viv = VortexInducedVibration::new(0.1, 2.0, 0.2, 0.01, 2.0);
let amp = viv.peak_amplitude();
assert!(amp > 0.0);
}
#[test]
fn test_viv_scruton_number() {
let viv = VortexInducedVibration::new(0.1, 2.0, 0.2, 0.05, 10.0);
let sc = viv.scruton_number();
let expected = 2.0 * PI * 10.0 * 0.05;
assert!((sc - expected).abs() < 1e-9);
}
#[test]
fn test_viv_zero_diameter() {
let viv = VortexInducedVibration::new(0.0, 5.0, 0.2, 0.01, 2.0);
assert_eq!(viv.reduced_velocity(1.0), 0.0);
assert_eq!(viv.shedding_frequency(1.0), 0.0);
}
#[test]
fn test_viv_high_damping_low_amplitude() {
let viv_low = VortexInducedVibration::new(0.1, 2.0, 0.2, 0.001, 1.0);
let viv_high = VortexInducedVibration::new(0.1, 2.0, 0.2, 0.5, 10.0);
assert!(viv_low.peak_amplitude() > viv_high.peak_amplitude());
}
}
pub struct FluidStructureFEM {
pub fluid_nodes: usize,
pub solid_nodes: usize,
pub interface_nodes: usize,
pub rho_f: f64,
pub rho_s: f64,
}
impl FluidStructureFEM {
pub fn new(
fluid_nodes: usize,
solid_nodes: usize,
interface_nodes: usize,
rho_f: f64,
rho_s: f64,
) -> Self {
Self {
fluid_nodes,
solid_nodes,
interface_nodes,
rho_f,
rho_s,
}
}
pub fn total_nodes(&self) -> usize {
self.fluid_nodes + self.solid_nodes
}
pub fn density_ratio(&self) -> f64 {
if self.rho_s.abs() < 1e-300 {
return 0.0;
}
self.rho_f / self.rho_s
}
}
pub fn added_mass_coefficient(geometry: &str) -> f64 {
match geometry {
"sphere" => 0.5,
"cylinder" => 1.0,
"flat_plate" => PI / 4.0,
_ => 1.0,
}
}
pub fn morison_force(cd: f64, cm: f64, rho: f64, d: f64, u: f64, u_dot: f64) -> f64 {
let drag = 0.5 * cd * rho * d * u.abs() * u;
let inertia = cm * rho * PI / 4.0 * d * d * u_dot;
drag + inertia
}
pub fn vortex_induced_vibration_frequency(u: f64, d: f64, st: f64) -> f64 {
if d.abs() < 1e-300 {
return 0.0;
}
st * u / d
}
pub fn lock_in_condition(f_nat: f64, f_viv: f64, tol: f64) -> bool {
(f_nat - f_viv).abs() <= tol
}
pub fn fluid_structure_coupling_factor(
rho_f: f64,
rho_s: f64,
volume_f: f64,
volume_s: f64,
) -> f64 {
let denom = rho_s * volume_s;
if denom.abs() < 1e-300 {
return 0.0;
}
rho_f * volume_f / denom
}
pub fn arbitrary_lagrangian_eulerian_velocity(v_mesh: [f64; 3], v_fluid: [f64; 3]) -> [f64; 3] {
[
v_fluid[0] - v_mesh[0],
v_fluid[1] - v_mesh[1],
v_fluid[2] - v_mesh[2],
]
}
pub struct InterfaceCondition {
pub no_slip: bool,
pub pressure_continuity: bool,
pub traction_balance: bool,
}
impl InterfaceCondition {
pub fn new(no_slip: bool, pressure_continuity: bool, traction_balance: bool) -> Self {
Self {
no_slip,
pressure_continuity,
traction_balance,
}
}
pub fn standard() -> Self {
Self::new(true, true, true)
}
pub fn all_satisfied(&self) -> bool {
self.no_slip && self.pressure_continuity && self.traction_balance
}
}
pub fn ibm_force_density(target_vel: [f64; 3], fluid_vel: [f64; 3], k_ib: f64) -> [f64; 3] {
[
k_ib * (target_vel[0] - fluid_vel[0]),
k_ib * (target_vel[1] - fluid_vel[1]),
k_ib * (target_vel[2] - fluid_vel[2]),
]
}
pub fn sloshing_frequency(g: f64, h: f64, l: f64, n: usize) -> f64 {
if l.abs() < 1e-300 || n == 0 {
return 0.0;
}
let k = n as f64 * PI / l;
(k * g * (k * h).tanh()).sqrt()
}
#[cfg(test)]
mod fsi_new_tests {
use super::*;
#[test]
fn test_fluid_structure_fem_new() {
let fsi = FluidStructureFEM::new(100, 50, 20, 1000.0, 7800.0);
assert_eq!(fsi.fluid_nodes, 100);
assert_eq!(fsi.solid_nodes, 50);
assert_eq!(fsi.interface_nodes, 20);
}
#[test]
fn test_fluid_structure_fem_total_nodes() {
let fsi = FluidStructureFEM::new(100, 50, 20, 1000.0, 7800.0);
assert_eq!(fsi.total_nodes(), 150);
}
#[test]
fn test_fluid_structure_fem_density_ratio() {
let fsi = FluidStructureFEM::new(10, 10, 5, 1000.0, 2000.0);
assert!((fsi.density_ratio() - 0.5).abs() < 1e-12);
}
#[test]
fn test_fluid_structure_fem_zero_solid_density() {
let fsi = FluidStructureFEM::new(10, 10, 5, 1000.0, 0.0);
assert_eq!(fsi.density_ratio(), 0.0);
}
#[test]
fn test_added_mass_sphere() {
assert!((added_mass_coefficient("sphere") - 0.5).abs() < 1e-12);
}
#[test]
fn test_added_mass_cylinder() {
assert!((added_mass_coefficient("cylinder") - 1.0).abs() < 1e-12);
}
#[test]
fn test_added_mass_flat_plate() {
let expected = PI / 4.0;
assert!((added_mass_coefficient("flat_plate") - expected).abs() < 1e-12);
}
#[test]
fn test_added_mass_default() {
assert!((added_mass_coefficient("unknown") - 1.0).abs() < 1e-12);
}
#[test]
fn test_morison_force_pure_drag() {
let f = morison_force(1.0, 2.0, 1000.0, 0.5, 1.0, 0.0);
let expected = 0.5 * 1.0 * 1000.0 * 0.5 * 1.0;
assert!((f - expected).abs() < 1e-10);
}
#[test]
fn test_morison_force_pure_inertia() {
let f = morison_force(1.0, 2.0, 1000.0, 0.5, 0.0, 1.0);
let expected = 2.0 * 1000.0 * PI / 4.0 * 0.25 * 1.0;
assert!((f - expected).abs() < 1e-8);
}
#[test]
fn test_morison_force_negative_velocity() {
let f_pos = morison_force(1.0, 0.0, 1000.0, 0.5, 1.0, 0.0);
let f_neg = morison_force(1.0, 0.0, 1000.0, 0.5, -1.0, 0.0);
assert!(f_pos > 0.0);
assert!(f_neg < 0.0);
assert!((f_pos + f_neg).abs() < 1e-12);
}
#[test]
fn test_vortex_induced_vibration_frequency_formula() {
let f = vortex_induced_vibration_frequency(1.0, 0.5, 0.2);
assert!((f - 0.4).abs() < 1e-12);
}
#[test]
fn test_vortex_induced_vibration_frequency_zero_diameter() {
let f = vortex_induced_vibration_frequency(1.0, 0.0, 0.2);
assert_eq!(f, 0.0);
}
#[test]
fn test_vortex_induced_vibration_frequency_proportional_u() {
let f1 = vortex_induced_vibration_frequency(1.0, 0.5, 0.2);
let f2 = vortex_induced_vibration_frequency(2.0, 0.5, 0.2);
assert!((f2 - 2.0 * f1).abs() < 1e-12);
}
#[test]
fn test_lock_in_condition_true() {
assert!(lock_in_condition(5.0, 5.05, 0.1));
}
#[test]
fn test_lock_in_condition_false() {
assert!(!lock_in_condition(5.0, 6.0, 0.1));
}
#[test]
fn test_lock_in_condition_exact() {
assert!(lock_in_condition(5.0, 5.0, 0.0));
}
#[test]
fn test_coupling_factor_formula() {
let cf = fluid_structure_coupling_factor(1000.0, 7800.0, 1.0, 1.0);
assert!((cf - 1000.0 / 7800.0).abs() < 1e-10);
}
#[test]
fn test_coupling_factor_zero_solid_volume() {
let cf = fluid_structure_coupling_factor(1000.0, 7800.0, 1.0, 0.0);
assert_eq!(cf, 0.0);
}
#[test]
fn test_ale_velocity_zero_mesh() {
let c = arbitrary_lagrangian_eulerian_velocity([0.0; 3], [1.0, 2.0, 3.0]);
assert_eq!(c, [1.0, 2.0, 3.0]);
}
#[test]
fn test_ale_velocity_formula() {
let c = arbitrary_lagrangian_eulerian_velocity([1.0, 0.5, 0.25], [2.0, 1.5, 1.25]);
assert!((c[0] - 1.0).abs() < 1e-12);
assert!((c[1] - 1.0).abs() < 1e-12);
assert!((c[2] - 1.0).abs() < 1e-12);
}
#[test]
fn test_ale_velocity_mesh_faster_than_fluid() {
let c = arbitrary_lagrangian_eulerian_velocity([3.0, 0.0, 0.0], [1.0, 0.0, 0.0]);
assert!(c[0] < 0.0);
}
#[test]
fn test_interface_condition_standard() {
let ic = InterfaceCondition::standard();
assert!(ic.no_slip);
assert!(ic.pressure_continuity);
assert!(ic.traction_balance);
assert!(ic.all_satisfied());
}
#[test]
fn test_interface_condition_partial() {
let ic = InterfaceCondition::new(true, false, true);
assert!(!ic.all_satisfied());
}
#[test]
fn test_ibm_force_density_zero_diff() {
let f = ibm_force_density([1.0, 2.0, 3.0], [1.0, 2.0, 3.0], 1000.0);
assert_eq!(f, [0.0, 0.0, 0.0]);
}
#[test]
fn test_ibm_force_density_formula() {
let f = ibm_force_density([2.0, 0.0, 0.0], [1.0, 0.0, 0.0], 500.0);
assert!((f[0] - 500.0).abs() < 1e-10);
}
#[test]
fn test_ibm_force_density_scales_with_k() {
let f1 = ibm_force_density([2.0, 0.0, 0.0], [1.0, 0.0, 0.0], 100.0);
let f2 = ibm_force_density([2.0, 0.0, 0.0], [1.0, 0.0, 0.0], 200.0);
assert!((f2[0] - 2.0 * f1[0]).abs() < 1e-10);
}
#[test]
fn test_sloshing_frequency_n1() {
let g = 9.81;
let h = 1.0;
let l = 2.0;
let omega = sloshing_frequency(g, h, l, 1);
let k = PI / l;
let expected = (k * g * (k * h).tanh()).sqrt();
assert!((omega - expected).abs() < 1e-10);
}
#[test]
fn test_sloshing_frequency_zero_length() {
let omega = sloshing_frequency(9.81, 1.0, 0.0, 1);
assert_eq!(omega, 0.0);
}
#[test]
fn test_sloshing_frequency_zero_mode() {
let omega = sloshing_frequency(9.81, 1.0, 2.0, 0);
assert_eq!(omega, 0.0);
}
#[test]
fn test_sloshing_frequency_higher_mode_larger() {
let omega1 = sloshing_frequency(9.81, 1.0, 2.0, 1);
let omega2 = sloshing_frequency(9.81, 1.0, 2.0, 2);
assert!(omega2 > omega1);
}
#[test]
fn test_sloshing_frequency_positive() {
let omega = sloshing_frequency(9.81, 0.5, 1.0, 1);
assert!(omega > 0.0);
}
#[test]
fn test_morison_force_zero_all() {
let f = morison_force(1.0, 1.0, 1000.0, 0.5, 0.0, 0.0);
assert_eq!(f, 0.0);
}
}