Skip to main content

oxiphysics_materials/
tribology_ext.rs

1// Copyright 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Extended tribology — friction, wear, and lubrication.
5//!
6//! Provides Hertz contact mechanics, Stribeck lubrication curves, Archard
7//! wear model, Sommerfeld number, elasto-hydrodynamic film thickness,
8//! flash temperature, and hydrodynamic lift for tribological analysis.
9
10#![allow(dead_code)]
11#![allow(clippy::too_many_arguments)]
12
13use std::f64::consts::PI;
14
15// ─────────────────────────────────────────────────────────────────────────────
16// ContactInterface
17// ─────────────────────────────────────────────────────────────────────────────
18
19/// Tribological contact interface describing surface and material properties.
20#[derive(Debug, Clone, Copy)]
21pub struct ContactInterface {
22    /// Root-mean-square surface roughness \[m\].
23    pub roughness_rms: f64,
24    /// Young's modulus of the contact body \[Pa\].
25    pub young_modulus: f64,
26    /// Poisson's ratio of the contact body (dimensionless).
27    pub poisson_ratio: f64,
28    /// Vickers hardness \[Pa\].
29    pub hardness: f64,
30}
31
32impl ContactInterface {
33    /// Create a new [`ContactInterface`].
34    pub fn new(roughness_rms: f64, young_modulus: f64, poisson_ratio: f64, hardness: f64) -> Self {
35        Self {
36            roughness_rms,
37            young_modulus,
38            poisson_ratio,
39            hardness,
40        }
41    }
42}
43
44// ─────────────────────────────────────────────────────────────────────────────
45// Reduced modulus
46// ─────────────────────────────────────────────────────────────────────────────
47
48/// Compute the reduced (combined) elastic modulus E* for two contacting bodies.
49///
50/// E* = 1 / ((1−ν₁²)/E₁ + (1−ν₂²)/E₂)
51///
52/// # Arguments
53/// * `e1` – Young's modulus of body 1 \[Pa\].
54/// * `nu1` – Poisson's ratio of body 1.
55/// * `e2` – Young's modulus of body 2 \[Pa\].
56/// * `nu2` – Poisson's ratio of body 2.
57pub fn reduced_modulus(e1: f64, nu1: f64, e2: f64, nu2: f64) -> f64 {
58    1.0 / ((1.0 - nu1 * nu1) / e1 + (1.0 - nu2 * nu2) / e2)
59}
60
61// ─────────────────────────────────────────────────────────────────────────────
62// Hertz contact radius
63// ─────────────────────────────────────────────────────────────────────────────
64
65/// Compute the Hertz contact radius for two spheres in contact.
66///
67/// a = (3·F·R_eff / (4·E*))^(1/3)
68///
69/// where R_eff = R₁·R₂/(R₁+R₂) is the effective radius.
70///
71/// # Arguments
72/// * `r1` – Radius of sphere 1 \[m\].
73/// * `r2` – Radius of sphere 2 \[m\] (use a very large value for flat).
74/// * `e_star` – Reduced modulus E* \[Pa\].
75/// * `load` – Normal force F \[N\].
76pub fn hertz_contact_radius(r1: f64, r2: f64, e_star: f64, load: f64) -> f64 {
77    let r_eff = r1 * r2 / (r1 + r2);
78    (3.0 * load * r_eff / (4.0 * e_star)).cbrt()
79}
80
81// ─────────────────────────────────────────────────────────────────────────────
82// Hertz maximum pressure
83// ─────────────────────────────────────────────────────────────────────────────
84
85/// Compute the Hertz maximum contact pressure (peak Hertzian pressure).
86///
87/// p₀ = 3F / (2π·a²)
88///
89/// # Arguments
90/// * `load` – Normal force F \[N\].
91/// * `contact_radius` – Contact radius a \[m\].
92pub fn hertz_max_pressure(load: f64, contact_radius: f64) -> f64 {
93    3.0 * load / (2.0 * PI * contact_radius * contact_radius)
94}
95
96// ─────────────────────────────────────────────────────────────────────────────
97// Stribeck curve
98// ─────────────────────────────────────────────────────────────────────────────
99
100/// Compute the Stribeck number (dimensionless lubrication parameter).
101///
102/// S = η·v / (P·R)
103///
104/// The Stribeck number governs the lubrication regime transition from boundary
105/// to mixed to hydrodynamic lubrication.
106///
107/// # Arguments
108/// * `speed` – Sliding speed v \[m/s\].
109/// * `viscosity` – Dynamic viscosity η \[Pa·s\].
110/// * `pressure` – Mean contact pressure P \[Pa\].
111/// * `r` – Characteristic contact radius R \[m\].
112pub fn stribeck_curve(speed: f64, viscosity: f64, pressure: f64, r: f64) -> f64 {
113    viscosity * speed / (pressure * r)
114}
115
116// ─────────────────────────────────────────────────────────────────────────────
117// Hydrodynamic lift
118// ─────────────────────────────────────────────────────────────────────────────
119
120/// Estimate the hydrodynamic lift force from the Reynolds lubrication equation.
121///
122/// F ≈ η·v·L·W / h²  (simplified wedge-film approximation)
123///
124/// # Arguments
125/// * `viscosity` – Dynamic viscosity η \[Pa·s\].
126/// * `speed` – Surface speed v \[m/s\].
127/// * `gap` – Minimum film thickness h \[m\].
128/// * `length` – Bearing length L \[m\].
129/// * `width` – Bearing width W \[m\].
130pub fn hydrodynamic_lift(viscosity: f64, speed: f64, gap: f64, length: f64, width: f64) -> f64 {
131    viscosity * speed * length * width / (gap * gap)
132}
133
134// ─────────────────────────────────────────────────────────────────────────────
135// ArchardWear
136// ─────────────────────────────────────────────────────────────────────────────
137
138/// Archard adhesive wear model.
139///
140/// The Archard wear equation: V = k·W·s / H  where V is wear volume,
141/// k is the dimensionless wear coefficient, W is normal load, s is sliding
142/// distance, and H is hardness.
143#[derive(Debug, Clone, Copy)]
144pub struct ArchardWear {
145    /// Dimensionless Archard wear coefficient k.
146    pub k: f64,
147    /// Material hardness H \[Pa\].
148    pub hardness: f64,
149}
150
151impl ArchardWear {
152    /// Create a new [`ArchardWear`] model.
153    pub fn new(k: f64, hardness: f64) -> Self {
154        Self { k, hardness }
155    }
156
157    /// Compute wear volume \[m³\] for a given normal load and sliding distance.
158    ///
159    /// V = k·W·s / H
160    ///
161    /// # Arguments
162    /// * `load` – Normal force W \[N\].
163    /// * `sliding_dist` – Total sliding distance s \[m\].
164    pub fn wear_volume(&self, load: f64, sliding_dist: f64) -> f64 {
165        self.k * load * sliding_dist / self.hardness
166    }
167
168    /// Compute wear depth \[m\] for a given normal load, sliding distance, and
169    /// nominal contact area.
170    ///
171    /// d = V / A = k·W·s / (H·A)
172    ///
173    /// # Arguments
174    /// * `load` – Normal force W \[N\].
175    /// * `sliding_dist` – Total sliding distance s \[m\].
176    /// * `area` – Nominal contact area A \[m²\].
177    pub fn wear_depth(&self, load: f64, sliding_dist: f64, area: f64) -> f64 {
178        self.wear_volume(load, sliding_dist) / area
179    }
180}
181
182// ─────────────────────────────────────────────────────────────────────────────
183// Sommerfeld number
184// ─────────────────────────────────────────────────────────────────────────────
185
186/// Compute the Sommerfeld number for a journal bearing.
187///
188/// S = η·ω·R·L / (W·(c/R)²)
189///
190/// The Sommerfeld number characterises the operating conditions of a
191/// hydrodynamic journal bearing.
192///
193/// # Arguments
194/// * `viscosity` – Dynamic viscosity η \[Pa·s\].
195/// * `speed` – Rotational speed ω \[rad/s\].
196/// * `load` – Bearing load per unit projected area W \[N\].
197/// * `r` – Journal radius R \[m\].
198/// * `l` – Bearing length L \[m\].
199/// * `c` – Radial clearance c \[m\].
200pub fn sommerfeld_number(viscosity: f64, speed: f64, load: f64, r: f64, l: f64, c: f64) -> f64 {
201    let c_over_r = c / r;
202    viscosity * speed * r * l / (load * c_over_r * c_over_r)
203}
204
205// ─────────────────────────────────────────────────────────────────────────────
206// Elasto-hydrodynamic film thickness
207// ─────────────────────────────────────────────────────────────────────────────
208
209/// Estimate the minimum EHL (elasto-hydrodynamic lubrication) film thickness
210/// using a simplified Hamrock-Dowson formula.
211///
212/// h_min ≈ 2.69 · (η₀·u)^0.67 · R'^0.464 / (E'^0.073 · W^0.067)
213///
214/// where η₀ is the dynamic viscosity, u is the entraining velocity, R' is
215/// the effective radius, E' = 2·E* is the combined modulus, and W is the
216/// dimensionless load parameter W = load/(E'·R'^2).
217///
218/// # Arguments
219/// * `r_eff` – Effective radius R' \[m\].
220/// * `e_star` – Reduced modulus E* \[Pa\].
221/// * `u` – Entraining velocity u \[m/s\].
222/// * `eta` – Dynamic viscosity η₀ \[Pa·s\].
223/// * `load` – Normal force \[N\].
224pub fn elasto_hydrodynamic_film_thickness(
225    r_eff: f64,
226    e_star: f64,
227    u: f64,
228    eta: f64,
229    load: f64,
230) -> f64 {
231    // E' = 2 * E*
232    let e_prime = 2.0 * e_star;
233    // Dimensionless load W* = load / (E' * R'^2)
234    let w_star = load / (e_prime * r_eff * r_eff);
235    // Dimensionless speed U* = eta * u / (E' * R')
236    let u_star = eta * u / (e_prime * r_eff);
237    // Hamrock-Dowson: h_min/R' = 2.69 * U*^0.67 * W*^(-0.067) * (point contact)
238    r_eff * 2.69 * u_star.powf(0.67) * w_star.powf(-0.067)
239}
240
241// ─────────────────────────────────────────────────────────────────────────────
242// Friction power dissipation
243// ─────────────────────────────────────────────────────────────────────────────
244
245/// Compute the frictional power dissipation rate.
246///
247/// P = F_friction · v_sliding
248///
249/// # Arguments
250/// * `friction_force` – Friction force F \[N\].
251/// * `sliding_velocity` – Relative sliding velocity v \[m/s\].
252pub fn friction_power_dissipation(friction_force: f64, sliding_velocity: f64) -> f64 {
253    friction_force * sliding_velocity
254}
255
256// ─────────────────────────────────────────────────────────────────────────────
257// Flash temperature
258// ─────────────────────────────────────────────────────────────────────────────
259
260/// Compute the Blok flash temperature rise at the contact interface.
261///
262/// Δθ_flash ≈ q·r_contact / (kappa·sqrt(π·kappa_diff / (v·r_contact)))
263///
264/// Simplified form: Δθ ≈ 0.31 · q / (kappa · sqrt(r_c · v / κ))
265/// where κ = kappa/(rho·cp) is thermal diffusivity.
266///
267/// # Arguments
268/// * `q_friction` – Frictional heat flux Q \[W/m²\].
269/// * `r_contact` – Contact radius r_c \[m\].
270/// * `kappa` – Thermal conductivity κ \[W/(m·K)\].
271/// * `rho_cp` – Volumetric heat capacity ρ·cp \[J/(m³·K)\].
272/// * `v` – Sliding velocity v \[m/s\].
273pub fn flash_temperature(q_friction: f64, r_contact: f64, kappa: f64, rho_cp: f64, v: f64) -> f64 {
274    // Thermal diffusivity κ_diff = kappa / (rho * cp)
275    let kappa_diff = kappa / rho_cp;
276    // Blok flash temperature: ΔT = q * sqrt(π * r_c / (v * κ_diff)) / (2 * kappa)
277    let peclet_term = (PI * r_contact / (v * kappa_diff)).sqrt();
278    q_friction * peclet_term / (2.0 * kappa)
279}
280
281// ─────────────────────────────────────────────────────────────────────────────
282// Tests
283// ─────────────────────────────────────────────────────────────────────────────
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288
289    const EPS: f64 = 1e-9;
290
291    // 1. reduced_modulus is positive for valid inputs.
292    #[test]
293    fn test_reduced_modulus_positive() {
294        let e_star = reduced_modulus(200e9, 0.3, 200e9, 0.3);
295        assert!(
296            e_star > 0.0,
297            "Reduced modulus must be positive, got {e_star}"
298        );
299    }
300
301    // 2. reduced_modulus for equal materials equals E/(1-nu^2).
302    #[test]
303    fn test_reduced_modulus_equal_materials() {
304        let e = 200e9_f64;
305        let nu = 0.3_f64;
306        let e_star = reduced_modulus(e, nu, e, nu);
307        let expected = e / (2.0 * (1.0 - nu * nu));
308        assert!(
309            (e_star - expected).abs() / expected < 1e-9,
310            "Equal materials: E* = {e_star} vs expected {expected}"
311        );
312    }
313
314    // 3. reduced_modulus increases when one modulus increases.
315    #[test]
316    fn test_reduced_modulus_increases_with_e() {
317        let e1 = reduced_modulus(100e9, 0.3, 200e9, 0.3);
318        let e2 = reduced_modulus(200e9, 0.3, 200e9, 0.3);
319        assert!(e2 > e1, "Larger E should give larger E*");
320    }
321
322    // 4. hertz_contact_radius is positive.
323    #[test]
324    fn test_hertz_contact_radius_positive() {
325        let a = hertz_contact_radius(0.01, 0.01, 100e9, 100.0);
326        assert!(a > 0.0, "Contact radius must be positive, got {a}");
327    }
328
329    // 5. hertz_contact_radius increases with load.
330    #[test]
331    fn test_hertz_contact_radius_increases_with_load() {
332        let a1 = hertz_contact_radius(0.01, 0.01, 100e9, 100.0);
333        let a2 = hertz_contact_radius(0.01, 0.01, 100e9, 1000.0);
334        assert!(
335            a2 > a1,
336            "Contact radius should increase with load: a1={a1}, a2={a2}"
337        );
338    }
339
340    // 6. hertz_contact_radius scales as load^(1/3).
341    #[test]
342    fn test_hertz_contact_radius_scaling() {
343        let r1 = 0.01_f64;
344        let e_star = 100e9_f64;
345        let a1 = hertz_contact_radius(r1, r1, e_star, 100.0);
346        let a8 = hertz_contact_radius(r1, r1, e_star, 800.0);
347        // a scales as F^(1/3): a(8F)/a(F) = 2
348        let ratio = a8 / a1;
349        assert!(
350            (ratio - 2.0).abs() < 1e-6,
351            "Contact radius scales as F^1/3, got ratio={ratio}"
352        );
353    }
354
355    // 7. hertz_max_pressure formula: p0 = 3F/(2*pi*a^2).
356    #[test]
357    fn test_hertz_max_pressure_formula() {
358        let load = 100.0_f64;
359        let a = 1e-3_f64;
360        let p0 = hertz_max_pressure(load, a);
361        let expected = 3.0 * load / (2.0 * PI * a * a);
362        assert!(
363            (p0 - expected).abs() < EPS,
364            "Max pressure formula mismatch: {p0} vs {expected}"
365        );
366    }
367
368    // 8. hertz_max_pressure is positive.
369    #[test]
370    fn test_hertz_max_pressure_positive() {
371        let p0 = hertz_max_pressure(50.0, 5e-4);
372        assert!(p0 > 0.0, "Max pressure should be positive, got {p0}");
373    }
374
375    // 9. hertz_max_pressure increases with load.
376    #[test]
377    fn test_hertz_max_pressure_increases_with_load() {
378        let a = 1e-3_f64;
379        let p1 = hertz_max_pressure(100.0, a);
380        let p2 = hertz_max_pressure(200.0, a);
381        assert!(p2 > p1, "Higher load → higher max pressure");
382    }
383
384    // 10. stribeck_curve is positive.
385    #[test]
386    fn test_stribeck_positive() {
387        let s = stribeck_curve(1.0, 0.001, 1e6, 1e-3);
388        assert!(s > 0.0, "Stribeck number must be positive, got {s}");
389    }
390
391    // 11. stribeck_curve increases with speed.
392    #[test]
393    fn test_stribeck_increases_with_speed() {
394        let s1 = stribeck_curve(1.0, 0.001, 1e6, 1e-3);
395        let s2 = stribeck_curve(10.0, 0.001, 1e6, 1e-3);
396        assert!(s2 > s1, "Stribeck number should increase with speed");
397    }
398
399    // 12. stribeck_curve scales linearly with viscosity.
400    #[test]
401    fn test_stribeck_scales_with_viscosity() {
402        let s1 = stribeck_curve(1.0, 0.001, 1e6, 1e-3);
403        let s2 = stribeck_curve(1.0, 0.002, 1e6, 1e-3);
404        assert!(
405            (s2 / s1 - 2.0).abs() < EPS,
406            "Stribeck scales with viscosity: {s2} vs 2*{s1}"
407        );
408    }
409
410    // 13. hydrodynamic_lift is positive.
411    #[test]
412    fn test_hydro_lift_positive() {
413        let f = hydrodynamic_lift(0.001, 5.0, 1e-5, 0.1, 0.05);
414        assert!(f > 0.0, "Hydrodynamic lift must be positive, got {f}");
415    }
416
417    // 14. hydrodynamic_lift increases with speed.
418    #[test]
419    fn test_hydro_lift_increases_with_speed() {
420        let f1 = hydrodynamic_lift(0.001, 1.0, 1e-5, 0.1, 0.05);
421        let f2 = hydrodynamic_lift(0.001, 2.0, 1e-5, 0.1, 0.05);
422        assert!(f2 > f1, "Lift should increase with speed");
423    }
424
425    // 15. hydrodynamic_lift scales inversely with gap^2.
426    #[test]
427    fn test_hydro_lift_gap_scaling() {
428        let f1 = hydrodynamic_lift(0.001, 1.0, 1e-5, 0.1, 0.05);
429        let f2 = hydrodynamic_lift(0.001, 1.0, 2e-5, 0.1, 0.05);
430        // F ∝ 1/h^2, so doubling h gives 1/4 force
431        let ratio = f1 / f2;
432        assert!(
433            (ratio - 4.0).abs() < 1e-6,
434            "Lift scales as 1/h^2: got ratio={ratio}"
435        );
436    }
437
438    // 16. ArchardWear::wear_volume scales with load.
439    #[test]
440    fn test_archard_wear_scales_with_load() {
441        let wear = ArchardWear::new(1e-4, 1e9);
442        let v1 = wear.wear_volume(100.0, 1.0);
443        let v2 = wear.wear_volume(200.0, 1.0);
444        assert!(
445            (v2 / v1 - 2.0).abs() < EPS,
446            "Wear volume should scale with load: v1={v1}, v2={v2}"
447        );
448    }
449
450    // 17. ArchardWear::wear_volume scales with sliding distance.
451    #[test]
452    fn test_archard_wear_scales_with_distance() {
453        let wear = ArchardWear::new(1e-4, 1e9);
454        let v1 = wear.wear_volume(100.0, 1.0);
455        let v2 = wear.wear_volume(100.0, 5.0);
456        assert!(
457            (v2 / v1 - 5.0).abs() < EPS,
458            "Wear volume should scale with sliding distance: {v2} vs 5*{v1}"
459        );
460    }
461
462    // 18. ArchardWear::wear_volume is positive.
463    #[test]
464    fn test_archard_wear_positive() {
465        let wear = ArchardWear::new(1e-5, 5e9);
466        let v = wear.wear_volume(500.0, 10.0);
467        assert!(v > 0.0, "Wear volume must be positive, got {v}");
468    }
469
470    // 19. ArchardWear::wear_depth is consistent with wear_volume.
471    #[test]
472    fn test_archard_wear_depth_consistent() {
473        let wear = ArchardWear::new(1e-4, 1e9);
474        let area = 1e-4_f64; // 1 cm^2
475        let vol = wear.wear_volume(100.0, 0.5);
476        let depth = wear.wear_depth(100.0, 0.5, area);
477        assert!(
478            (depth - vol / area).abs() < EPS,
479            "Wear depth must be vol/area: {depth} vs {}",
480            vol / area
481        );
482    }
483
484    // 20. ArchardWear: harder material gives less wear.
485    #[test]
486    fn test_archard_harder_less_wear() {
487        let soft = ArchardWear::new(1e-4, 1e9);
488        let hard = ArchardWear::new(1e-4, 5e9);
489        let v_soft = soft.wear_volume(100.0, 1.0);
490        let v_hard = hard.wear_volume(100.0, 1.0);
491        assert!(v_hard < v_soft, "Harder material should wear less");
492    }
493
494    // 21. sommerfeld_number is positive.
495    #[test]
496    fn test_sommerfeld_positive() {
497        let s = sommerfeld_number(0.01, 100.0, 1000.0, 0.05, 0.05, 0.0001);
498        assert!(s > 0.0, "Sommerfeld number must be positive, got {s}");
499    }
500
501    // 22. sommerfeld_number increases with viscosity.
502    #[test]
503    fn test_sommerfeld_increases_with_viscosity() {
504        let s1 = sommerfeld_number(0.01, 100.0, 1000.0, 0.05, 0.05, 0.0001);
505        let s2 = sommerfeld_number(0.02, 100.0, 1000.0, 0.05, 0.05, 0.0001);
506        assert!(s2 > s1, "Sommerfeld increases with viscosity");
507    }
508
509    // 23. sommerfeld_number scales linearly with speed.
510    #[test]
511    fn test_sommerfeld_scales_with_speed() {
512        let s1 = sommerfeld_number(0.01, 100.0, 1000.0, 0.05, 0.05, 0.0001);
513        let s2 = sommerfeld_number(0.01, 200.0, 1000.0, 0.05, 0.05, 0.0001);
514        assert!(
515            (s2 / s1 - 2.0).abs() < EPS,
516            "Sommerfeld scales with speed: got ratio {}",
517            s2 / s1
518        );
519    }
520
521    // 24. elasto_hydrodynamic_film_thickness is positive.
522    #[test]
523    fn test_ehl_film_positive() {
524        let h = elasto_hydrodynamic_film_thickness(0.01, 100e9, 1.0, 0.01, 1000.0);
525        assert!(h > 0.0, "EHL film thickness must be positive, got {h}");
526    }
527
528    // 25. elasto_hydrodynamic_film_thickness increases with speed.
529    #[test]
530    fn test_ehl_film_increases_with_speed() {
531        let h1 = elasto_hydrodynamic_film_thickness(0.01, 100e9, 0.5, 0.01, 1000.0);
532        let h2 = elasto_hydrodynamic_film_thickness(0.01, 100e9, 1.0, 0.01, 1000.0);
533        assert!(h2 > h1, "EHL film increases with speed: h1={h1}, h2={h2}");
534    }
535
536    // 26. friction_power_dissipation is positive for positive inputs.
537    #[test]
538    fn test_friction_power_positive() {
539        let p = friction_power_dissipation(50.0, 2.0);
540        assert!(p > 0.0, "Friction power must be positive, got {p}");
541    }
542
543    // 27. friction_power_dissipation formula P = F * v.
544    #[test]
545    fn test_friction_power_formula() {
546        let f = 75.0_f64;
547        let v = 3.5_f64;
548        let p = friction_power_dissipation(f, v);
549        assert!(
550            (p - f * v).abs() < EPS,
551            "P = F*v: got {p}, expected {}",
552            f * v
553        );
554    }
555
556    // 28. friction_power_dissipation scales linearly with force.
557    #[test]
558    fn test_friction_power_scales_with_force() {
559        let p1 = friction_power_dissipation(10.0, 2.0);
560        let p2 = friction_power_dissipation(20.0, 2.0);
561        assert!(
562            (p2 / p1 - 2.0).abs() < EPS,
563            "Friction power scales with force"
564        );
565    }
566
567    // 29. flash_temperature is positive.
568    #[test]
569    fn test_flash_temperature_positive() {
570        let dt = flash_temperature(1e6, 1e-4, 50.0, 3e6, 1.0);
571        assert!(dt > 0.0, "Flash temperature must be positive, got {dt}");
572    }
573
574    // 30. flash_temperature increases with friction heat flux.
575    #[test]
576    fn test_flash_temperature_increases_with_flux() {
577        let dt1 = flash_temperature(1e6, 1e-4, 50.0, 3e6, 1.0);
578        let dt2 = flash_temperature(2e6, 1e-4, 50.0, 3e6, 1.0);
579        assert!(dt2 > dt1, "Higher heat flux → higher flash temperature");
580    }
581
582    // 31. flash_temperature decreases with higher thermal conductivity.
583    #[test]
584    fn test_flash_temperature_decreases_with_conductivity() {
585        let dt1 = flash_temperature(1e6, 1e-4, 50.0, 3e6, 1.0);
586        let dt2 = flash_temperature(1e6, 1e-4, 100.0, 3e6, 1.0);
587        assert!(dt2 < dt1, "Higher conductivity → lower flash temperature");
588    }
589
590    // 32. ContactInterface stores fields correctly.
591    #[test]
592    fn test_contact_interface_fields() {
593        let ci = ContactInterface::new(1e-6, 200e9, 0.3, 2e9);
594        assert!((ci.roughness_rms - 1e-6).abs() < EPS);
595        assert!((ci.young_modulus - 200e9).abs() < 1.0);
596        assert!((ci.poisson_ratio - 0.3).abs() < EPS);
597        assert!((ci.hardness - 2e9).abs() < 1.0);
598    }
599
600    // 33. hertz_contact_radius increases with larger radius.
601    #[test]
602    fn test_hertz_contact_radius_larger_sphere() {
603        let a1 = hertz_contact_radius(0.01, 0.01, 100e9, 100.0);
604        let a2 = hertz_contact_radius(0.02, 0.02, 100e9, 100.0);
605        assert!(a2 > a1, "Larger spheres → larger contact radius");
606    }
607
608    // 34. ArchardWear::new stores k and hardness.
609    #[test]
610    fn test_archard_new_fields() {
611        let w = ArchardWear::new(2e-4, 3e9);
612        assert!((w.k - 2e-4).abs() < EPS);
613        assert!((w.hardness - 3e9).abs() < 1.0);
614    }
615
616    // 35. stribeck_curve is zero when speed is zero.
617    #[test]
618    fn test_stribeck_zero_speed() {
619        let s = stribeck_curve(0.0, 0.001, 1e6, 1e-3);
620        assert_eq!(s, 0.0, "Stribeck = 0 at zero speed");
621    }
622
623    // 36. hertz_max_pressure decreases as contact radius grows.
624    #[test]
625    fn test_hertz_pressure_decreases_with_radius() {
626        let p1 = hertz_max_pressure(100.0, 1e-3);
627        let p2 = hertz_max_pressure(100.0, 2e-3);
628        assert!(
629            p2 < p1,
630            "Larger contact radius → lower pressure for same load"
631        );
632    }
633
634    // 37. sommerfeld_number decreases with higher load.
635    #[test]
636    fn test_sommerfeld_decreases_with_load() {
637        let s1 = sommerfeld_number(0.01, 100.0, 500.0, 0.05, 0.05, 0.0001);
638        let s2 = sommerfeld_number(0.01, 100.0, 1000.0, 0.05, 0.05, 0.0001);
639        assert!(s2 < s1, "Higher load → lower Sommerfeld number");
640    }
641
642    // 38. ehl_film_thickness increases with viscosity.
643    #[test]
644    fn test_ehl_film_increases_with_viscosity() {
645        let h1 = elasto_hydrodynamic_film_thickness(0.01, 100e9, 1.0, 0.01, 1000.0);
646        let h2 = elasto_hydrodynamic_film_thickness(0.01, 100e9, 1.0, 0.02, 1000.0);
647        assert!(h2 > h1, "Higher viscosity → thicker EHL film");
648    }
649}