use std::f64::consts::PI;
#[allow(dead_code)]
fn v3_add(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[0] + b[0], a[1] + b[1], a[2] + b[2]]
}
#[allow(dead_code)]
fn v3_sub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
#[allow(dead_code)]
fn v3_scale(a: [f64; 3], s: f64) -> [f64; 3] {
[a[0] * s, a[1] * s, a[2] * s]
}
#[allow(dead_code)]
fn v3_dot(a: [f64; 3], b: [f64; 3]) -> f64 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
#[allow(dead_code)]
fn v3_norm(a: [f64; 3]) -> f64 {
v3_dot(a, a).sqrt()
}
#[derive(Debug, Clone)]
pub struct ArchardWear {
pub wear_coefficient: f64,
pub hardness: f64,
}
impl ArchardWear {
pub fn new(wear_coefficient: f64, hardness: f64) -> Self {
Self {
wear_coefficient,
hardness,
}
}
pub fn wear_volume(&self, normal_load: f64, sliding_distance: f64) -> f64 {
self.wear_coefficient * normal_load * sliding_distance / self.hardness
}
pub fn wear_rate(&self, normal_load: f64) -> f64 {
self.wear_coefficient * normal_load / self.hardness
}
pub fn specific_wear_rate(&self) -> f64 {
self.wear_coefficient / self.hardness
}
}
#[derive(Debug, Clone)]
pub struct HertzContact {
pub effective_modulus: f64,
pub effective_radius: f64,
}
impl HertzContact {
pub fn new(e1: f64, nu1: f64, e2: f64, nu2: f64, r1: f64, r2: f64) -> Self {
let inv_e_star = (1.0 - nu1 * nu1) / e1 + (1.0 - nu2 * nu2) / e2;
let effective_modulus = 1.0 / inv_e_star;
let inv_r_star = 1.0 / r1 + 1.0 / r2;
let effective_radius = 1.0 / inv_r_star;
Self {
effective_modulus,
effective_radius,
}
}
pub fn contact_radius(&self, normal_load: f64) -> f64 {
let arg = 3.0 * normal_load * self.effective_radius / (4.0 * self.effective_modulus);
arg.cbrt()
}
pub fn max_pressure(&self, normal_load: f64) -> f64 {
let a = self.contact_radius(normal_load);
3.0 * normal_load / (2.0 * PI * a * a)
}
pub fn mean_pressure(&self, normal_load: f64) -> f64 {
let a = self.contact_radius(normal_load);
normal_load / (PI * a * a)
}
pub fn indentation_depth(&self, normal_load: f64) -> f64 {
let a = self.contact_radius(normal_load);
a * a / self.effective_radius
}
pub fn contact_stiffness(&self, normal_load: f64) -> f64 {
let a = self.contact_radius(normal_load);
2.0 * self.effective_modulus * a
}
}
#[derive(Debug, Clone)]
pub struct FlashTemperature {
pub lambda1: f64,
pub lambda2: f64,
pub alpha1: f64,
pub alpha2: f64,
}
impl FlashTemperature {
pub fn new(lambda1: f64, alpha1: f64, lambda2: f64, alpha2: f64) -> Self {
Self {
lambda1,
lambda2,
alpha1,
alpha2,
}
}
pub fn blok_flash_temperature(
&self,
friction_coeff: f64,
mean_pressure: f64,
sliding_velocity: f64,
contact_radius: f64,
) -> f64 {
let heat_flux = friction_coeff * mean_pressure * sliding_velocity;
0.308 * heat_flux * contact_radius / (self.lambda1 + self.lambda2)
}
pub fn jaeger_flash_temperature(
&self,
friction_coeff: f64,
mean_pressure: f64,
sliding_velocity: f64,
contact_radius: f64,
) -> f64 {
let heat_flux = friction_coeff * mean_pressure * sliding_velocity;
heat_flux * contact_radius / (PI * (self.lambda1 + self.lambda2))
}
pub fn peclet_number(&self, sliding_velocity: f64, contact_radius: f64) -> f64 {
let alpha_avg = 0.5 * (self.alpha1 + self.alpha2);
sliding_velocity * contact_radius / (2.0 * alpha_avg)
}
}
#[derive(Debug, Clone)]
pub struct SurfaceRoughness {
pub profile: Vec<f64>,
pub sampling_length: f64,
}
impl SurfaceRoughness {
pub fn new(profile: Vec<f64>, sampling_length: f64) -> Self {
Self {
profile,
sampling_length,
}
}
pub fn ra(&self) -> f64 {
if self.profile.is_empty() {
return 0.0;
}
let sum: f64 = self.profile.iter().map(|z| z.abs()).sum();
sum / self.profile.len() as f64
}
pub fn rq(&self) -> f64 {
if self.profile.is_empty() {
return 0.0;
}
let sum_sq: f64 = self.profile.iter().map(|z| z * z).sum();
(sum_sq / self.profile.len() as f64).sqrt()
}
pub fn rz(&self) -> f64 {
if self.profile.is_empty() {
return 0.0;
}
let max_z = self
.profile
.iter()
.cloned()
.fold(f64::NEG_INFINITY, f64::max);
let min_z = self.profile.iter().cloned().fold(f64::INFINITY, f64::min);
max_z - min_z
}
pub fn rsk(&self) -> f64 {
if self.profile.is_empty() {
return 0.0;
}
let rq = self.rq();
if rq < 1e-30 {
return 0.0;
}
let n = self.profile.len() as f64;
let sum_cube: f64 = self.profile.iter().map(|z| z * z * z).sum();
sum_cube / (n * rq * rq * rq)
}
pub fn rku(&self) -> f64 {
if self.profile.is_empty() {
return 0.0;
}
let rq = self.rq();
if rq < 1e-30 {
return 0.0;
}
let n = self.profile.len() as f64;
let sum_4th: f64 = self.profile.iter().map(|z| z * z * z * z).sum();
sum_4th / (n * rq * rq * rq * rq)
}
}
#[derive(Debug, Clone)]
pub struct GreenwoodWilliamson {
pub asperity_density: f64,
pub asperity_radius: f64,
pub height_std: f64,
pub effective_modulus: f64,
}
impl GreenwoodWilliamson {
pub fn new(
asperity_density: f64,
asperity_radius: f64,
height_std: f64,
effective_modulus: f64,
) -> Self {
Self {
asperity_density,
asperity_radius,
height_std,
effective_modulus,
}
}
pub fn plasticity_index(&self, hardness: f64) -> f64 {
(self.effective_modulus / hardness) * (self.height_std / self.asperity_radius).sqrt()
}
pub fn real_contact_area(&self, separation: f64, nominal_area: f64) -> f64 {
let n_steps = 200usize;
let z_max = 6.0 * self.height_std;
let dz = (z_max - separation) / n_steps as f64;
if dz <= 0.0 {
return 0.0;
}
let mut integral = 0.0;
for i in 0..n_steps {
let z = separation + (i as f64 + 0.5) * dz;
let phi = gaussian_pdf(z, 0.0, self.height_std);
let delta = z - separation;
integral += PI * self.asperity_radius * delta * phi * dz;
}
self.asperity_density * nominal_area * integral
}
pub fn total_load(&self, separation: f64, nominal_area: f64) -> f64 {
let n_steps = 200usize;
let z_max = 6.0 * self.height_std;
let dz = (z_max - separation) / n_steps as f64;
if dz <= 0.0 {
return 0.0;
}
let e_star = self.effective_modulus;
let beta = self.asperity_radius;
let mut integral = 0.0;
for i in 0..n_steps {
let z = separation + (i as f64 + 0.5) * dz;
let phi = gaussian_pdf(z, 0.0, self.height_std);
let delta = z - separation;
let f_asperity = (4.0 / 3.0) * e_star * beta.sqrt() * delta.powf(1.5);
integral += f_asperity * phi * dz;
}
self.asperity_density * nominal_area * integral
}
}
#[allow(dead_code)]
fn gaussian_pdf(x: f64, mean: f64, std: f64) -> f64 {
let z = (x - mean) / std;
(-0.5 * z * z).exp() / (std * (2.0 * PI).sqrt())
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum LubricationRegime {
Boundary,
Mixed,
FullFilm,
}
pub fn lambda_ratio(h_min: f64, rq1: f64, rq2: f64) -> f64 {
let composite_roughness = (rq1 * rq1 + rq2 * rq2).sqrt();
if composite_roughness < 1e-30 {
return f64::INFINITY;
}
h_min / composite_roughness
}
pub fn classify_lubrication_regime(lambda: f64) -> LubricationRegime {
if lambda < 1.0 {
LubricationRegime::Boundary
} else if lambda < 3.0 {
LubricationRegime::Mixed
} else {
LubricationRegime::FullFilm
}
}
pub fn boundary_lubrication_friction(interfacial_shear_strength: f64, mean_pressure: f64) -> f64 {
if mean_pressure < 1e-20 {
return 0.0;
}
interfacial_shear_strength / mean_pressure
}
#[derive(Debug, Clone)]
pub struct EhlFilmThickness {
pub viscosity_0: f64,
pub pressure_viscosity_coeff: f64,
pub effective_modulus: f64,
pub effective_radius: f64,
}
impl EhlFilmThickness {
pub fn new(
viscosity_0: f64,
pressure_viscosity_coeff: f64,
effective_modulus: f64,
effective_radius: f64,
) -> Self {
Self {
viscosity_0,
pressure_viscosity_coeff,
effective_modulus,
effective_radius,
}
}
pub fn hamrock_dowson_hmin(&self, entrainment_velocity: f64, normal_load: f64) -> f64 {
let r = self.effective_radius;
let e_star = self.effective_modulus;
let eta0 = self.viscosity_0;
let alpha = self.pressure_viscosity_coeff;
let u_nd = eta0 * entrainment_velocity / (e_star * r); let g_nd = alpha * e_star; let w_nd = normal_load / (e_star * r * r);
let ellipticity_factor = 1.0 - (-0.68_f64).exp();
3.63 * r * u_nd.powf(0.68) * g_nd.powf(0.49) * w_nd.powf(-0.073) * ellipticity_factor
}
pub fn barus_viscosity(&self, pressure: f64) -> f64 {
self.viscosity_0 * (self.pressure_viscosity_coeff * pressure).exp()
}
pub fn dowson_higginson_hmin(&self, entrainment_velocity: f64, load_per_width: f64) -> f64 {
let alpha = self.pressure_viscosity_coeff;
let eta0 = self.viscosity_0;
let r = self.effective_radius;
let e_star = self.effective_modulus;
2.65 * alpha.powf(0.54) * (eta0 * entrainment_velocity).powf(0.70) * r.powf(0.43)
/ (e_star.powf(0.03) * load_per_width.powf(0.13))
}
}
#[derive(Debug, Clone)]
pub struct CoulombFriction {
pub static_coeff: f64,
pub kinetic_coeff: f64,
}
impl CoulombFriction {
pub fn new(static_coeff: f64, kinetic_coeff: f64) -> Self {
Self {
static_coeff,
kinetic_coeff,
}
}
pub fn friction_force(&self, normal_load: f64, sliding: bool) -> f64 {
let mu = if sliding {
self.kinetic_coeff
} else {
self.static_coeff
};
mu * normal_load.abs()
}
}
#[derive(Debug, Clone)]
pub struct ViscousFriction {
pub damping: f64,
}
impl ViscousFriction {
pub fn new(damping: f64) -> Self {
Self { damping }
}
pub fn friction_force(&self, velocity: f64) -> f64 {
self.damping * velocity
}
}
#[derive(Debug, Clone)]
pub struct StribeckFriction {
pub static_coeff: f64,
pub kinetic_coeff: f64,
pub stribeck_velocity: f64,
pub shape_exponent: f64,
pub viscous_coeff: f64,
}
impl StribeckFriction {
pub fn new(
static_coeff: f64,
kinetic_coeff: f64,
stribeck_velocity: f64,
shape_exponent: f64,
viscous_coeff: f64,
) -> Self {
Self {
static_coeff,
kinetic_coeff,
stribeck_velocity,
shape_exponent,
viscous_coeff,
}
}
pub fn friction_coeff(&self, velocity: f64) -> f64 {
let v = velocity.abs();
let stribeck = (self.static_coeff - self.kinetic_coeff)
* (-(v / self.stribeck_velocity).powf(self.shape_exponent)).exp();
self.kinetic_coeff + stribeck + self.viscous_coeff * v
}
pub fn friction_force(&self, normal_load: f64, velocity: f64) -> f64 {
self.friction_coeff(velocity) * normal_load.abs()
}
}
#[derive(Debug, Clone)]
pub struct FrettingWear {
pub wear_coefficient: f64,
pub contact_area: f64,
}
impl FrettingWear {
pub fn new(wear_coefficient: f64, contact_area: f64) -> Self {
Self {
wear_coefficient,
contact_area,
}
}
pub fn dissipated_energy_per_cycle(
&self,
tangential_force_amplitude: f64,
slip_amplitude: f64,
) -> f64 {
4.0 * tangential_force_amplitude * slip_amplitude
}
pub fn wear_volume_per_cycle(
&self,
tangential_force_amplitude: f64,
slip_amplitude: f64,
) -> f64 {
self.wear_coefficient
* self.dissipated_energy_per_cycle(tangential_force_amplitude, slip_amplitude)
}
pub fn fatigue_reduction_factor(
&self,
max_pressure: f64,
fatigue_limit: f64,
fretting_factor: f64,
) -> f64 {
let reduction = fretting_factor * max_pressure / fatigue_limit;
(1.0 - reduction).max(0.0)
}
}
#[derive(Debug, Clone)]
pub struct WearDebris {
pub particle_radius: f64,
pub particle_density: f64,
pub trapping_ratio: f64,
}
impl WearDebris {
pub fn new(particle_radius: f64, particle_density: f64, trapping_ratio: f64) -> Self {
Self {
particle_radius,
particle_density,
trapping_ratio,
}
}
pub fn particle_mass(&self) -> f64 {
(4.0 / 3.0) * PI * self.particle_radius.powi(3) * self.particle_density
}
pub fn particle_count(&self, worn_volume: f64) -> f64 {
let vol_particle = (4.0 / 3.0) * PI * self.particle_radius.powi(3);
worn_volume / vol_particle
}
pub fn friction_increase(&self, nominal_gap: f64, abrasive_friction_coeff: f64) -> f64 {
if nominal_gap < 1e-30 {
return 0.0;
}
self.trapping_ratio * self.particle_radius / nominal_gap * abrasive_friction_coeff
}
}
#[derive(Debug, Clone)]
pub struct Tribocorrosion {
pub mechanical_wear_rate: f64,
pub corrosion_rate_0: f64,
pub synergy_factor: f64,
}
impl Tribocorrosion {
pub fn new(mechanical_wear_rate: f64, corrosion_rate_0: f64, synergy_factor: f64) -> Self {
Self {
mechanical_wear_rate,
corrosion_rate_0,
synergy_factor,
}
}
pub fn total_rate(&self) -> f64 {
let w_chem_enhanced = self.corrosion_rate_0 * (1.0 + self.synergy_factor);
self.mechanical_wear_rate + w_chem_enhanced
}
pub fn synergy_rate(&self) -> f64 {
self.corrosion_rate_0 * self.synergy_factor
}
pub fn volume_loss(&self, time: f64) -> f64 {
self.total_rate() * time
}
}
#[derive(Debug, Clone)]
pub struct SolidLubricant {
pub coating_type: CoatingType,
pub thickness: f64,
pub friction_coeff_air: f64,
pub friction_coeff_vacuum: f64,
pub hardness_gpa: f64,
pub specific_wear_rate: f64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum CoatingType {
MoS2,
Dlc,
Ptfe,
WS2,
}
impl SolidLubricant {
pub fn mos2(thickness: f64) -> Self {
Self {
coating_type: CoatingType::MoS2,
thickness,
friction_coeff_air: 0.15,
friction_coeff_vacuum: 0.01,
hardness_gpa: 0.3,
specific_wear_rate: 1e-16,
}
}
pub fn dlc(thickness: f64) -> Self {
Self {
coating_type: CoatingType::Dlc,
thickness,
friction_coeff_air: 0.05,
friction_coeff_vacuum: 0.08,
hardness_gpa: 40.0,
specific_wear_rate: 1e-18,
}
}
pub fn coating_lifetime(&self, mean_pressure: f64) -> f64 {
if mean_pressure < 1e-20 {
return f64::INFINITY;
}
self.thickness / (self.specific_wear_rate * mean_pressure)
}
pub fn friction_coeff(&self, vacuum: bool) -> f64 {
if vacuum {
self.friction_coeff_vacuum
} else {
self.friction_coeff_air
}
}
}
#[derive(Debug, Clone)]
pub struct RollingContactFatigue {
pub dynamic_load_rating: f64,
pub weibull_slope: f64,
pub life_modification_factor: f64,
}
impl RollingContactFatigue {
pub fn new(
dynamic_load_rating: f64,
weibull_slope: f64,
life_modification_factor: f64,
) -> Self {
Self {
dynamic_load_rating,
weibull_slope,
life_modification_factor,
}
}
pub fn l10_life(&self, equivalent_load: f64) -> f64 {
if equivalent_load < 1e-20 {
return f64::INFINITY;
}
(self.dynamic_load_rating / equivalent_load).powf(self.weibull_slope)
}
pub fn lnm_life(&self, equivalent_load: f64) -> f64 {
self.life_modification_factor * self.l10_life(equivalent_load)
}
pub fn fatigue_limit_load(&self) -> f64 {
0.5 * self.dynamic_load_rating
}
pub fn survival_probability(&self, life_ratio: f64) -> f64 {
let ln_10_9: f64 = (10.0_f64 / 9.0_f64).ln();
(-ln_10_9 * life_ratio.powf(self.weibull_slope)).exp()
}
}
#[derive(Debug, Clone)]
pub struct MixedLubrication {
pub gw: GreenwoodWilliamson,
pub ehl: EhlFilmThickness,
pub nominal_area: f64,
}
impl MixedLubrication {
pub fn new(gw: GreenwoodWilliamson, ehl: EhlFilmThickness, nominal_area: f64) -> Self {
Self {
gw,
ehl,
nominal_area,
}
}
pub fn asperity_load_fraction(&self, separation: f64, total_load: f64) -> f64 {
if total_load < 1e-20 {
return 0.0;
}
let w_asp = self.gw.total_load(separation, self.nominal_area);
(w_asp / total_load).min(1.0)
}
pub fn effective_friction(
&self,
separation: f64,
total_load: f64,
boundary_mu: f64,
fluid_mu: f64,
) -> f64 {
let chi = self.asperity_load_fraction(separation, total_load);
chi * boundary_mu + (1.0 - chi) * fluid_mu
}
}
#[derive(Debug, Clone)]
pub struct AbrasiveWear {
pub archard: ArchardWear,
pub abrasive_hardness: f64,
pub abrasive_angle: f64,
}
impl AbrasiveWear {
pub fn new(
wear_coefficient: f64,
surface_hardness: f64,
abrasive_hardness: f64,
abrasive_angle: f64,
) -> Self {
Self {
archard: ArchardWear::new(wear_coefficient, surface_hardness),
abrasive_hardness,
abrasive_angle,
}
}
pub fn two_body_wear_rate(&self, normal_load: f64) -> f64 {
(2.0 / PI) * self.abrasive_angle.tan() * normal_load / self.archard.hardness
}
pub fn hardness_ratio(&self) -> f64 {
self.archard.hardness / self.abrasive_hardness
}
pub fn three_body_severity(&self) -> f64 {
0.05 }
}
#[derive(Debug, Clone)]
pub struct AdhesiveWear {
pub archard: ArchardWear,
pub work_of_adhesion: f64,
}
impl AdhesiveWear {
pub fn new(wear_coefficient: f64, hardness: f64, work_of_adhesion: f64) -> Self {
Self {
archard: ArchardWear::new(wear_coefficient, hardness),
work_of_adhesion,
}
}
pub fn pull_off_force(&self, contact_radius: f64) -> f64 {
2.0 * PI * self.work_of_adhesion * contact_radius * contact_radius
}
pub fn wear_volume(&self, normal_load: f64, sliding_distance: f64) -> f64 {
self.archard.wear_volume(normal_load, sliding_distance)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_archard_wear_volume_basic() {
let wear = ArchardWear::new(0.001, 1e9);
let v = wear.wear_volume(100.0, 1.0);
assert!((v - 1e-10).abs() < 1e-22, "Expected 1e-10, got {v:.6e}");
}
#[test]
fn test_archard_wear_rate_proportional_to_load() {
let wear = ArchardWear::new(0.01, 1e9);
let r1 = wear.wear_rate(100.0);
let r2 = wear.wear_rate(200.0);
assert!(
(r2 / r1 - 2.0).abs() < 1e-10,
"Wear rate should double with load"
);
}
#[test]
fn test_archard_specific_wear_rate() {
let k = 1e-3;
let h = 2e9;
let wear = ArchardWear::new(k, h);
let expected = k / h;
assert!(
(wear.specific_wear_rate() - expected).abs() < 1e-30,
"Specific wear rate mismatch"
);
}
#[test]
fn test_hertz_contact_radius_positive() {
let hertz = HertzContact::new(200e9, 0.3, 200e9, 0.3, 0.01, f64::INFINITY);
let a = hertz.contact_radius(1000.0);
assert!(a > 0.0, "Contact radius must be positive");
}
#[test]
fn test_hertz_max_pressure_greater_than_mean() {
let hertz = HertzContact::new(200e9, 0.3, 200e9, 0.3, 0.01, f64::INFINITY);
let p_max = hertz.max_pressure(1000.0);
let p_mean = hertz.mean_pressure(1000.0);
assert!(
p_max > p_mean,
"Max pressure {p_max:.4e} should exceed mean {p_mean:.4e}"
);
}
#[test]
fn test_hertz_max_to_mean_pressure_ratio() {
let hertz = HertzContact::new(200e9, 0.3, 200e9, 0.3, 0.005, f64::INFINITY);
let p_max = hertz.max_pressure(500.0);
let p_mean = hertz.mean_pressure(500.0);
assert!(
(p_max / p_mean - 1.5).abs() < 1e-6,
"Ratio p_max/p_mean should be 3/2, got {}",
p_max / p_mean
);
}
#[test]
fn test_blok_flash_temperature_positive() {
let ft = FlashTemperature::new(50.0, 1.2e-5, 50.0, 1.2e-5);
let dt = ft.blok_flash_temperature(0.1, 1e9, 1.0, 1e-4);
assert!(dt > 0.0, "Flash temperature must be positive");
}
#[test]
fn test_peclet_number_consistency() {
let ft = FlashTemperature::new(50.0, 1.2e-5, 50.0, 1.2e-5);
let pe = ft.peclet_number(1.0, 1e-4);
assert!(
(pe - 4.166_666).abs() < 1e-4,
"Peclet number expected ~4.17, got {pe:.6}"
);
}
#[test]
fn test_jaeger_less_than_blok_at_high_pe() {
let ft = FlashTemperature::new(50.0, 1.2e-5, 50.0, 1.2e-5);
let blok = ft.blok_flash_temperature(0.1, 1e9, 10.0, 1e-3);
let jaeger = ft.jaeger_flash_temperature(0.1, 1e9, 10.0, 1e-3);
assert!(
blok > 0.0 && jaeger > 0.0,
"Both flash temps must be positive"
);
}
#[test]
fn test_roughness_ra_simple() {
let profile = vec![1.0, -1.0, 1.0, -1.0];
let sr = SurfaceRoughness::new(profile, 4e-6);
assert!(
(sr.ra() - 1.0).abs() < 1e-10,
"Ra should be 1.0, got {}",
sr.ra()
);
}
#[test]
fn test_roughness_rq_greater_than_or_equal_ra() {
let profile: Vec<f64> = (0..100).map(|i| (i as f64 * 0.1).sin()).collect();
let sr = SurfaceRoughness::new(profile, 10e-6);
assert!(sr.rq() >= sr.ra() - 1e-12, "Rq must be >= Ra");
}
#[test]
fn test_roughness_rz_max_minus_min() {
let profile = vec![-2.0, 0.0, 3.0, 1.0, -1.0];
let sr = SurfaceRoughness::new(profile, 5e-6);
assert!((sr.rz() - 5.0).abs() < 1e-10, "Rz should be 5.0 (max-min)");
}
#[test]
fn test_roughness_empty_profile() {
let sr = SurfaceRoughness::new(vec![], 1e-3);
assert_eq!(sr.ra(), 0.0);
assert_eq!(sr.rq(), 0.0);
assert_eq!(sr.rz(), 0.0);
}
#[test]
fn test_gw_plasticity_index() {
let gw = GreenwoodWilliamson::new(1e12, 5e-6, 0.5e-6, 200e9);
let psi = gw.plasticity_index(7e9);
assert!(
psi > 1.0,
"Plasticity index should indicate plastic contact"
);
}
#[test]
fn test_gw_real_contact_area_positive() {
let gw = GreenwoodWilliamson::new(1e12, 1e-6, 1e-7, 100e9);
let a_r = gw.real_contact_area(0.0, 1e-4);
assert!(a_r > 0.0, "Real contact area must be positive");
}
#[test]
fn test_gw_total_load_increases_with_separation_decrease() {
let gw = GreenwoodWilliamson::new(1e12, 1e-6, 1e-7, 100e9);
let w1 = gw.total_load(5e-8, 1e-4); let w2 = gw.total_load(2e-7, 1e-4); assert!(w1 > w2, "Load should increase as separation decreases");
}
#[test]
fn test_lambda_ratio_classification() {
assert_eq!(
classify_lubrication_regime(0.5),
LubricationRegime::Boundary
);
assert_eq!(classify_lubrication_regime(2.0), LubricationRegime::Mixed);
assert_eq!(
classify_lubrication_regime(5.0),
LubricationRegime::FullFilm
);
}
#[test]
fn test_lambda_ratio_computation() {
let lam = lambda_ratio(1e-6, 0.5e-6, 0.5e-6);
let expected = 1e-6 / (2.0_f64.sqrt() * 0.5e-6);
assert!((lam - expected).abs() < 1e-10, "Lambda ratio mismatch");
}
#[test]
fn test_ehl_hmin_positive() {
let ehl = EhlFilmThickness::new(0.02, 2e-8, 220e9, 0.01);
let h = ehl.hamrock_dowson_hmin(1.0, 1000.0);
assert!(h > 0.0, "EHL film thickness must be positive");
}
#[test]
fn test_barus_viscosity_increases_with_pressure() {
let ehl = EhlFilmThickness::new(0.02, 2e-8, 220e9, 0.01);
let v0 = ehl.barus_viscosity(0.0);
let v1 = ehl.barus_viscosity(1e8);
assert!(v1 > v0, "Barus viscosity must increase with pressure");
assert!((v0 - 0.02).abs() < 1e-15, "At p=0, viscosity equals η_0");
}
#[test]
fn test_coulomb_friction_sliding_vs_static() {
let cf = CoulombFriction::new(0.5, 0.35);
let f_static = cf.friction_force(100.0, false);
let f_kinetic = cf.friction_force(100.0, true);
assert!(f_static > f_kinetic, "Static friction must exceed kinetic");
}
#[test]
fn test_stribeck_friction_coeff_decreases_from_zero_velocity() {
let sf = StribeckFriction::new(0.5, 0.2, 0.1, 2.0, 0.001);
let mu_slow = sf.friction_coeff(1e-4);
let mu_medium = sf.friction_coeff(0.1);
assert!(
mu_slow > mu_medium - 0.3,
"Stribeck curve should be bounded"
);
assert!(
mu_slow > 0.0 && mu_medium > 0.0,
"Friction must be positive"
);
}
#[test]
fn test_viscous_friction_linear() {
let vf = ViscousFriction::new(5.0);
let f1 = vf.friction_force(1.0);
let f2 = vf.friction_force(2.0);
assert!(
(f2 / f1 - 2.0).abs() < 1e-10,
"Viscous friction should be linear"
);
}
#[test]
fn test_fretting_wear_volume_per_cycle() {
let fw = FrettingWear::new(1e-8, 1e-6);
let v = fw.wear_volume_per_cycle(50.0, 1e-5);
assert!(v > 0.0, "Fretting wear volume must be positive");
}
#[test]
fn test_fretting_fatigue_reduction_factor_bounded() {
let fw = FrettingWear::new(1e-8, 1e-6);
let factor = fw.fatigue_reduction_factor(500e6, 300e6, 0.3);
assert!(
(0.0..=1.0).contains(&factor),
"Reduction factor must be in [0,1]"
);
}
#[test]
fn test_wear_debris_particle_count() {
let wd = WearDebris::new(1e-6, 7800.0, 0.5);
let n = wd.particle_count(1e-12);
let vol_p = (4.0 / 3.0) * PI * 1e-18;
let expected = 1e-12 / vol_p;
assert!(
(n - expected).abs() / expected < 1e-10,
"Particle count mismatch"
);
}
#[test]
fn test_tribocorrosion_total_exceeds_components() {
let tc = Tribocorrosion::new(1e-9, 5e-10, 2.0);
let total = tc.total_rate();
assert!(
total > tc.mechanical_wear_rate,
"Total rate must exceed mechanical wear rate"
);
assert!(
total > tc.corrosion_rate_0,
"Total rate must exceed bare corrosion"
);
}
#[test]
fn test_mos2_friction_lower_in_vacuum() {
let mos2 = SolidLubricant::mos2(1e-6);
assert!(
mos2.friction_coeff(true) < mos2.friction_coeff(false),
"MoS2 friction lower in vacuum"
);
}
#[test]
fn test_dlc_hardness_greater_than_mos2() {
let mos2 = SolidLubricant::mos2(1e-6);
let dlc = SolidLubricant::dlc(1e-6);
assert!(
dlc.hardness_gpa > mos2.hardness_gpa,
"DLC should be harder than MoS2"
);
}
#[test]
fn test_coating_lifetime_finite_under_load() {
let dlc = SolidLubricant::dlc(2e-6);
let life = dlc.coating_lifetime(1e9);
assert!(
life.is_finite() && life > 0.0,
"Lifetime should be finite and positive"
);
}
#[test]
fn test_rcf_l10_life_decreases_with_load() {
let rcf = RollingContactFatigue::new(10000.0, 10.0 / 3.0, 1.0);
let l1 = rcf.l10_life(2000.0);
let l2 = rcf.l10_life(5000.0);
assert!(l1 > l2, "L10 life must decrease as load increases");
}
#[test]
fn test_rcf_survival_probability_at_l10() {
let rcf = RollingContactFatigue::new(10000.0, 10.0 / 3.0, 1.0);
let s = rcf.survival_probability(1.0); assert!(
(s - 0.9).abs() < 1e-6,
"Survival at L10 should be 0.9, got {s:.6}"
);
}
#[test]
fn test_rcf_lnm_scaled_by_factor() {
let rcf = RollingContactFatigue::new(10000.0, 10.0 / 3.0, 2.0);
let base = RollingContactFatigue::new(10000.0, 10.0 / 3.0, 1.0);
let load = 3000.0;
assert!(
(rcf.lnm_life(load) - 2.0 * base.l10_life(load)).abs() < 1e-8,
"Lnm should equal a_ISO * L10"
);
}
}