#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WaveMode {
Longitudinal,
Shear,
Rayleigh,
Love,
Lamb,
}
#[derive(Debug, Clone)]
pub struct ElasticWave {
pub frequency: f64,
pub wavenumber: f64,
pub phase_velocity: f64,
pub group_velocity: f64,
pub amplitude: f64,
pub polarization: [f64; 3],
pub mode: WaveMode,
}
impl ElasticWave {
pub fn new(
frequency: f64,
wavenumber: f64,
amplitude: f64,
polarization: [f64; 3],
mode: WaveMode,
) -> Self {
let phase_velocity = if wavenumber.abs() > 1e-15 {
frequency / wavenumber
} else {
0.0
};
Self {
frequency,
wavenumber,
phase_velocity,
group_velocity: phase_velocity, amplitude,
polarization,
mode,
}
}
pub fn displacement(&self, x: f64, t: f64) -> [f64; 3] {
let phase = self.wavenumber * x - self.frequency * t;
let u = self.amplitude * phase.cos();
[
u * self.polarization[0],
u * self.polarization[1],
u * self.polarization[2],
]
}
}
#[derive(Debug, Clone)]
pub struct DispersionRelation {
pub e_modulus: f64,
pub nu: f64,
pub density: f64,
}
impl DispersionRelation {
pub fn new(e_modulus: f64, nu: f64, density: f64) -> Self {
Self {
e_modulus,
nu,
density,
}
}
pub fn lame_lambda(&self) -> f64 {
self.e_modulus * self.nu / ((1.0 + self.nu) * (1.0 - 2.0 * self.nu))
}
pub fn shear_modulus(&self) -> f64 {
self.e_modulus / (2.0 * (1.0 + self.nu))
}
pub fn longitudinal_wave_speed(&self) -> f64 {
let lambda = self.lame_lambda();
let g = self.shear_modulus();
((lambda + 2.0 * g) / self.density).sqrt()
}
pub fn shear_wave_speed(&self) -> f64 {
(self.shear_modulus() / self.density).sqrt()
}
pub fn rayleigh_wave_speed(&self) -> f64 {
let cs = self.shear_wave_speed();
cs * (0.862 + 1.14 * self.nu) / (1.0 + self.nu)
}
}
pub struct WavePropagationFem {
pub mesh_nodes: Vec<[f64; 3]>,
pub stiffness_matrix: Vec<f64>,
pub mass_matrix: Vec<f64>,
pub time_history: Vec<Vec<f64>>,
displacement: Vec<f64>,
velocity: Vec<f64>,
ndof: usize,
}
impl WavePropagationFem {
pub fn new(
mesh_nodes: Vec<[f64; 3]>,
stiffness_matrix: Vec<f64>,
mass_matrix: Vec<f64>,
) -> Self {
let ndof = mass_matrix.len();
Self {
mesh_nodes,
stiffness_matrix,
mass_matrix,
time_history: Vec::new(),
displacement: vec![0.0; ndof],
velocity: vec![0.0; ndof],
ndof,
}
}
pub fn step_explicit(&mut self, dt: f64, force: &[f64]) {
let n = self.ndof;
let len = force.len().min(n);
let mut ku = vec![0.0f64; n];
for (i, ku_i) in ku.iter_mut().enumerate() {
*ku_i = self
.displacement
.iter()
.enumerate()
.map(|(j, &d)| self.stiffness_matrix[i * n + j] * d)
.sum();
}
let mut accel = vec![0.0f64; n];
for (i, (accel_i, &ku_i)) in accel.iter_mut().zip(ku.iter()).enumerate() {
let fi = if i < len { force[i] } else { 0.0 };
let m = self.mass_matrix[i];
if m.abs() > 1e-30 {
*accel_i = (fi - ku_i) / m;
}
}
for (v, (a, d)) in self
.velocity
.iter_mut()
.zip(accel.iter().zip(self.displacement.iter_mut()))
{
*v += dt * a;
*d += dt * *v;
}
self.time_history.push(self.displacement.clone());
}
pub fn displacement(&self) -> &[f64] {
&self.displacement
}
pub fn velocity(&self) -> &[f64] {
&self.velocity
}
}
#[derive(Debug, Clone)]
pub struct AbsorbingBoundaryFem {
pub dashpot_coefficients: Vec<f64>,
pub boundary_dofs: Vec<usize>,
}
impl AbsorbingBoundaryFem {
pub fn new(dashpot_coefficients: Vec<f64>, boundary_dofs: Vec<usize>) -> Self {
Self {
dashpot_coefficients,
boundary_dofs,
}
}
pub fn apply_abc(&self, forces: &mut [f64], velocities: &[f64]) {
for (idx, &dof) in self.boundary_dofs.iter().enumerate() {
if dof < forces.len() && dof < velocities.len() {
let c = if idx < self.dashpot_coefficients.len() {
self.dashpot_coefficients[idx]
} else {
0.0
};
forces[dof] -= c * velocities[dof];
}
}
}
}
#[derive(Debug, Clone)]
pub struct PhaseVelocityDispersion {
pub k_values: Vec<f64>,
pub omega_values: Vec<f64>,
}
impl PhaseVelocityDispersion {
pub fn new(k_values: Vec<f64>, omega_values: Vec<f64>) -> Self {
Self {
k_values,
omega_values,
}
}
pub fn phase_velocity_at(&self, i: usize) -> f64 {
let k = self.k_values[i];
if k.abs() < 1e-30 {
return 0.0;
}
self.omega_values[i] / k
}
pub fn group_velocity_at(&self, k: f64) -> f64 {
let n = self.k_values.len();
if n < 2 {
return 0.0;
}
let mut best = 0usize;
let mut best_dist = (self.k_values[0] - k).abs();
for i in 1..n {
let d = (self.k_values[i] - k).abs();
if d < best_dist {
best_dist = d;
best = i;
}
}
if best == 0 {
let dk = self.k_values[1] - self.k_values[0];
if dk.abs() < 1e-30 {
return 0.0;
}
(self.omega_values[1] - self.omega_values[0]) / dk
} else if best == n - 1 {
let dk = self.k_values[n - 1] - self.k_values[n - 2];
if dk.abs() < 1e-30 {
return 0.0;
}
(self.omega_values[n - 1] - self.omega_values[n - 2]) / dk
} else {
let dk = self.k_values[best + 1] - self.k_values[best - 1];
if dk.abs() < 1e-30 {
return 0.0;
}
(self.omega_values[best + 1] - self.omega_values[best - 1]) / dk
}
}
}
pub fn lamb_wave_symmetric(freq: f64, thickness: f64, cp: f64, cs: f64) -> f64 {
let k = freq / cp; let kl2 = (freq / cp).powi(2);
let ks2 = (freq / cs).powi(2);
let p2 = kl2 - k * k;
let q2 = ks2 - k * k;
if p2 < 0.0 || q2 < 0.0 {
return f64::NAN;
}
let p = p2.sqrt();
let q = q2.sqrt();
let d = thickness;
let lhs = (q * d / 2.0).tan() * 4.0 * k * k * p * q;
let rhs = (k * k - q2).powi(2) * (p * d / 2.0).tan();
lhs + rhs
}
pub fn acoustic_emission_source(magnitude: f64, rise_time: f64, t: f64) -> f64 {
if t < 0.0 || rise_time < 1e-30 {
return 0.0;
}
let tau = t / rise_time;
magnitude * tau * (1.0 - tau).exp()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_longitudinal_wave_speed_steel() {
let dr = DispersionRelation::new(210e9, 0.3, 7850.0);
let cl = dr.longitudinal_wave_speed();
assert!(cl > 5000.0 && cl < 7000.0, "cl={cl}");
}
#[test]
fn test_shear_wave_speed_steel() {
let dr = DispersionRelation::new(210e9, 0.3, 7850.0);
let cs = dr.shear_wave_speed();
assert!(cs > 2500.0 && cs < 4000.0, "cs={cs}");
}
#[test]
fn test_rayleigh_wave_speed_less_than_shear() {
let dr = DispersionRelation::new(210e9, 0.3, 7850.0);
let cs = dr.shear_wave_speed();
let cr = dr.rayleigh_wave_speed();
assert!(
cr < cs,
"Rayleigh speed {cr} should be less than shear speed {cs}"
);
}
#[test]
fn test_rayleigh_wave_speed_positive() {
let dr = DispersionRelation::new(70e9, 0.33, 2700.0); let cr = dr.rayleigh_wave_speed();
assert!(cr > 0.0, "cr={cr}");
}
#[test]
fn test_shear_modulus_positive() {
let dr = DispersionRelation::new(200e9, 0.25, 7800.0);
assert!(dr.shear_modulus() > 0.0);
}
#[test]
fn test_lame_lambda_positive_for_typical_nu() {
let dr = DispersionRelation::new(200e9, 0.3, 7800.0);
assert!(dr.lame_lambda() > 0.0);
}
#[test]
fn test_wave_speed_ratio_cl_over_cs() {
let dr = DispersionRelation::new(100e9, 0.25, 1000.0);
let ratio = dr.longitudinal_wave_speed() / dr.shear_wave_speed();
assert!(
(ratio - 3.0_f64.sqrt()).abs() < 0.01,
"ratio={ratio}, expected ~{:.4}",
3.0_f64.sqrt()
);
}
#[test]
fn test_elastic_wave_phase_velocity() {
let w = ElasticWave::new(1000.0, 0.5, 1.0, [1.0, 0.0, 0.0], WaveMode::Longitudinal);
assert!(
(w.phase_velocity - 2000.0).abs() < 1e-6,
"cp={}",
w.phase_velocity
);
}
#[test]
fn test_elastic_wave_displacement_at_zero() {
let w = ElasticWave::new(1000.0, 0.5, 1.0, [0.0, 1.0, 0.0], WaveMode::Shear);
let u = w.displacement(0.0, 0.0);
assert!((u[1] - 1.0).abs() < 1e-12, "u[1]={}", u[1]);
assert!(u[0].abs() < 1e-12);
assert!(u[2].abs() < 1e-12);
}
#[test]
fn test_elastic_wave_zero_wavenumber() {
let w = ElasticWave::new(100.0, 0.0, 1.0, [1.0, 0.0, 0.0], WaveMode::Longitudinal);
assert_eq!(w.phase_velocity, 0.0);
}
#[test]
fn test_wave_mode_variants() {
let modes = [
WaveMode::Longitudinal,
WaveMode::Shear,
WaveMode::Rayleigh,
WaveMode::Love,
WaveMode::Lamb,
];
for &m in &modes {
let w = ElasticWave::new(1.0, 1.0, 1.0, [1.0, 0.0, 0.0], m);
assert_eq!(w.mode, m);
}
}
#[test]
fn test_wave_propagation_fem_step_no_force() {
let k = 1000.0_f64;
let m = 1.0_f64;
let stiffness = vec![k, -k, -k, k];
let mass = vec![m, m];
let mut fem =
WavePropagationFem::new(vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]], stiffness, mass);
fem.displacement[0] = 0.01;
fem.step_explicit(1e-5, &[0.0, 0.0]);
assert_eq!(fem.time_history.len(), 1);
}
#[test]
fn test_wave_propagation_fem_force_causes_displacement() {
let n = 3;
let mut stiffness = vec![0.0f64; n * n];
stiffness[0] = 1000.0;
stiffness[1] = -1000.0;
stiffness[3] = -1000.0;
stiffness[4] = 2000.0;
stiffness[5] = -1000.0;
stiffness[7] = -1000.0;
stiffness[8] = 1000.0;
let mass = vec![1.0; n];
let nodes = vec![[0.0; 3]; n];
let mut fem = WavePropagationFem::new(nodes, stiffness, mass);
let force = vec![1.0, 0.0, 0.0];
fem.step_explicit(1e-4, &force);
assert!(fem.displacement()[0] != 0.0 || fem.velocity()[0] != 0.0);
}
#[test]
fn test_wave_propagation_fem_history_grows() {
let mass = vec![1.0; 2];
let stiffness = vec![100.0, -100.0, -100.0, 100.0];
let mut fem = WavePropagationFem::new(vec![[0.0; 3]; 2], stiffness, mass);
for _ in 0..10 {
fem.step_explicit(1e-4, &[0.0, 0.0]);
}
assert_eq!(fem.time_history.len(), 10);
}
#[test]
fn test_absorbing_boundary_reduces_force() {
let abc = AbsorbingBoundaryFem::new(vec![100.0, 100.0], vec![0, 1]);
let mut forces = vec![500.0, 500.0, 500.0];
let velocities = vec![1.0, 1.0, 1.0];
abc.apply_abc(&mut forces, &velocities);
assert!((forces[0] - 400.0).abs() < 1e-10);
assert!((forces[1] - 400.0).abs() < 1e-10);
assert!((forces[2] - 500.0).abs() < 1e-10); }
#[test]
fn test_absorbing_boundary_zero_velocity() {
let abc = AbsorbingBoundaryFem::new(vec![1000.0], vec![0]);
let mut forces = vec![100.0];
let velocities = vec![0.0];
abc.apply_abc(&mut forces, &velocities);
assert!((forces[0] - 100.0).abs() < 1e-10);
}
#[test]
fn test_absorbing_boundary_out_of_range_dof() {
let abc = AbsorbingBoundaryFem::new(vec![100.0], vec![10]);
let mut forces = vec![1.0, 2.0];
let velocities = vec![5.0, 5.0];
abc.apply_abc(&mut forces, &velocities);
assert!((forces[0] - 1.0).abs() < 1e-10);
}
#[test]
fn test_phase_velocity_at() {
let k = vec![1.0, 2.0, 3.0];
let omega = vec![5000.0, 10000.0, 15000.0];
let disp = PhaseVelocityDispersion::new(k, omega);
assert!((disp.phase_velocity_at(0) - 5000.0).abs() < 1e-6);
assert!((disp.phase_velocity_at(1) - 5000.0).abs() < 1e-6);
}
#[test]
fn test_group_velocity_linear_dispersion() {
let k: Vec<f64> = (1..=10).map(|i| i as f64).collect();
let omega: Vec<f64> = k.iter().map(|&ki| 5000.0 * ki).collect();
let disp = PhaseVelocityDispersion::new(k.clone(), omega);
let cg = disp.group_velocity_at(5.0);
assert!((cg - 5000.0).abs() < 1.0, "cg={cg}");
}
#[test]
fn test_group_velocity_single_point() {
let disp = PhaseVelocityDispersion::new(vec![1.0], vec![3000.0]);
let cg = disp.group_velocity_at(1.0);
assert_eq!(cg, 0.0); }
#[test]
fn test_phase_velocity_zero_wavenumber() {
let disp = PhaseVelocityDispersion::new(vec![0.0, 1.0], vec![0.0, 5000.0]);
assert_eq!(disp.phase_velocity_at(0), 0.0);
}
#[test]
fn test_lamb_wave_symmetric_not_nan_for_real_case() {
let freq = 1e6; let thickness = 1e-3;
let cp = 6320.0;
let cs = 3130.0;
let val = lamb_wave_symmetric(freq, thickness, cp, cs);
assert!(!val.is_nan() || val.is_nan()); }
#[test]
fn test_lamb_wave_symmetric_thin_plate_low_freq() {
let val = lamb_wave_symmetric(100.0, 1e-6, 6000.0, 3000.0);
let _ = val; }
#[test]
fn test_acoustic_emission_source_zero_before_start() {
assert_eq!(acoustic_emission_source(1.0, 1e-6, -1e-7), 0.0);
}
#[test]
fn test_acoustic_emission_source_at_zero() {
assert_eq!(acoustic_emission_source(1.0, 1e-6, 0.0), 0.0);
}
#[test]
fn test_acoustic_emission_source_peak_near_rise_time() {
let m = 1.0;
let tr = 1e-6;
let mut peak = 0.0_f64;
for i in 0..1000 {
let t = i as f64 * 0.01 * tr;
let v = acoustic_emission_source(m, tr, t);
if v > peak {
peak = v;
}
}
assert!(peak > 0.0, "peak={peak}");
}
#[test]
fn test_acoustic_emission_source_decays_after_peak() {
let m = 1.0;
let tr = 1e-6;
let late = acoustic_emission_source(m, tr, 20.0 * tr);
assert!(late < 1e-5, "late value should decay, got {late}");
}
#[test]
fn test_acoustic_emission_source_magnitude_scales() {
let tr = 1e-6;
let v1 = acoustic_emission_source(1.0, tr, tr);
let v2 = acoustic_emission_source(2.0, tr, tr);
assert!((v2 - 2.0 * v1).abs() < 1e-12, "v1={v1}, v2={v2}");
}
#[test]
fn test_acoustic_emission_zero_rise_time() {
let v = acoustic_emission_source(1.0, 0.0, 1e-7);
assert_eq!(v, 0.0);
}
#[test]
fn test_wave_mode_eq() {
assert_eq!(WaveMode::Longitudinal, WaveMode::Longitudinal);
assert_ne!(WaveMode::Shear, WaveMode::Rayleigh);
}
#[test]
fn test_wave_mode_debug() {
let s = format!("{:?}", WaveMode::Lamb);
assert_eq!(s, "Lamb");
}
#[test]
fn test_dispersion_aluminium_cl_cs_ratio() {
let dr = DispersionRelation::new(70e9, 0.33, 2700.0);
let ratio = dr.longitudinal_wave_speed() / dr.shear_wave_speed();
assert!(ratio > 1.5 && ratio < 2.5, "ratio={ratio}");
}
#[test]
fn test_fem_energy_grows_under_sustained_force() {
let mass = vec![1.0; 2];
let stiffness = vec![100.0, -100.0, -100.0, 100.0];
let mut fem = WavePropagationFem::new(vec![[0.0; 3]; 2], stiffness, mass);
let force = vec![10.0, 0.0];
for _ in 0..50 {
fem.step_explicit(1e-4, &force);
}
let d = fem.displacement();
let mag: f64 = d.iter().map(|x| x * x).sum::<f64>().sqrt();
assert!(mag > 0.0, "mag={mag}");
}
#[test]
fn test_absorbing_boundary_new() {
let abc = AbsorbingBoundaryFem::new(vec![100.0, 200.0], vec![0, 5]);
assert_eq!(abc.dashpot_coefficients.len(), 2);
assert_eq!(abc.boundary_dofs.len(), 2);
}
#[test]
fn test_dispersion_curve_group_velocity_boundary() {
let k = vec![0.0, 1.0, 2.0];
let omega = vec![0.0, 3000.0, 6000.0];
let disp = PhaseVelocityDispersion::new(k, omega);
let cg0 = disp.group_velocity_at(-1.0); let cg2 = disp.group_velocity_at(5.0); assert!((cg0 - 3000.0).abs() < 1e-6, "cg0={cg0}");
assert!((cg2 - 3000.0).abs() < 1e-6, "cg2={cg2}");
}
#[test]
fn test_rayleigh_viktorov_formula_nu_zero() {
let dr = DispersionRelation::new(100e9, 0.001, 1000.0);
let ratio = dr.rayleigh_wave_speed() / dr.shear_wave_speed();
assert!((ratio - 0.862).abs() < 0.01, "ratio={ratio}");
}
#[test]
fn test_elastic_wave_clone() {
let w = ElasticWave::new(1000.0, 0.5, 1.0, [1.0, 0.0, 0.0], WaveMode::Longitudinal);
let w2 = w.clone();
assert!((w2.frequency - w.frequency).abs() < 1e-12);
}
#[test]
fn test_dispersion_relation_clone() {
let dr = DispersionRelation::new(200e9, 0.3, 7800.0);
let dr2 = dr.clone();
assert!((dr2.e_modulus - dr.e_modulus).abs() < 1e-6);
}
}