Skip to main content

oxiphysics_materials/tribology/
types.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use super::functions::erfc;
6#[allow(unused_imports)]
7use super::functions::*;
8use std::f64::consts::PI;
9
10/// Archard's wear model: Q = K * W / H * s (volume loss).
11#[derive(Debug, Clone, Copy)]
12pub struct ArchardWear {
13    /// Wear coefficient K (dimensionless, typically 1e-3 to 1e-8).
14    pub k: f64,
15    /// Hardness of softer material \[Pa\].
16    pub hardness: f64,
17}
18impl ArchardWear {
19    /// Create an Archard wear model.
20    pub fn new(k: f64, hardness: f64) -> Self {
21        Self { k, hardness }
22    }
23    /// Compute wear rate \[m³/m\] = K * P / H.
24    /// `pressure` is normal contact pressure \[Pa\].
25    pub fn wear_rate(&self, pressure: f64) -> f64 {
26        self.k * pressure / self.hardness
27    }
28    /// Compute volume loss \[m³\] over sliding distance `s` \[m\] under normal load `w` \[N\].
29    pub fn volume_loss(&self, w: f64, s: f64) -> f64 {
30        self.k * w * s / self.hardness
31    }
32    /// Compute linear wear depth \[m\] over area `a` \[m²\] sliding distance `s`.
33    pub fn depth_loss(&self, w: f64, s: f64, area: f64) -> f64 {
34        if area < 1e-30 {
35            return 0.0;
36        }
37        self.volume_loss(w, s) / area
38    }
39}
40/// Scuffing risk assessment based on contact flash temperature criterion (Blok).
41#[derive(Debug, Clone, Copy)]
42pub struct ScuffingModel {
43    /// Friction coefficient μ.
44    pub mu: f64,
45    /// Thermal conductivity body 1 k1 \[W/(m·K)\].
46    pub k1: f64,
47    /// Thermal conductivity body 2 k2 \[W/(m·K)\].
48    pub k2: f64,
49    /// Density * specific heat product ρcp1 \[J/(m³·K)\].
50    pub rho_cp1: f64,
51    /// Density * specific heat product ρcp2 \[J/(m³·K)\].
52    pub rho_cp2: f64,
53    /// Critical scuffing temperature Tc \[K\].
54    pub t_critical: f64,
55    /// Bulk temperature T_bulk \[K\].
56    pub t_bulk: f64,
57}
58impl ScuffingModel {
59    /// Create a typical steel-on-steel scuffing model.
60    pub fn steel_steel() -> Self {
61        ScuffingModel {
62            mu: 0.1,
63            k1: 50.0,
64            k2: 50.0,
65            rho_cp1: 3.75e6,
66            rho_cp2: 3.75e6,
67            t_critical: 700.0 + 273.15,
68            t_bulk: 80.0 + 273.15,
69        }
70    }
71    /// Thermal diffusivity κ \[m²/s\] for body 1.
72    pub fn thermal_diffusivity_1(&self) -> f64 {
73        self.k1 / self.rho_cp1
74    }
75    /// Thermal diffusivity κ for body 2.
76    pub fn thermal_diffusivity_2(&self) -> f64 {
77        self.k2 / self.rho_cp2
78    }
79    /// Flash temperature rise ΔTf \[K\] using Blok's contact temperature formula.
80    /// `q` = heat flux at contact \[W/m²\], `a` = contact radius \[m\], `v` = velocity \[m/s\].
81    pub fn flash_temperature_rise(&self, q: f64, a: f64, v: f64) -> f64 {
82        if a < 1e-30 || v < 1e-10 {
83            return 0.0;
84        }
85        let beta1 = (self.rho_cp1 * self.k1).sqrt();
86        let beta2 = (self.rho_cp2 * self.k2).sqrt();
87        1.11 * q * (a / v).sqrt() / (beta1 + beta2)
88    }
89    /// Contact temperature Tc = T_bulk + ΔTf.
90    pub fn contact_temperature(&self, q: f64, a: f64, v: f64) -> f64 {
91        self.t_bulk + self.flash_temperature_rise(q, a, v)
92    }
93    /// Scuffing risk: returns true if contact temperature exceeds critical.
94    pub fn scuffing_risk(&self, q: f64, a: f64, v: f64) -> bool {
95        self.contact_temperature(q, a, v) > self.t_critical
96    }
97    /// Safety factor against scuffing (Tc_critical / T_contact).
98    pub fn safety_factor(&self, q: f64, a: f64, v: f64) -> f64 {
99        let tc = self.contact_temperature(q, a, v);
100        if tc < 1.0 {
101            return f64::INFINITY;
102        }
103        self.t_critical / tc
104    }
105}
106/// Rolling element bearing life analysis per ISO 281.
107#[derive(Debug, Clone, Copy)]
108pub struct BearingLife {
109    /// Basic dynamic load rating C \[N\].
110    pub c: f64,
111    /// Applied equivalent dynamic load P \[N\].
112    pub p: f64,
113    /// Life exponent p (3 for ball bearings, 10/3 for roller bearings).
114    pub life_exponent: f64,
115    /// Lubrication factor aISO (1.0 = reference).
116    pub a_iso: f64,
117    /// Material/contamination factor (0.1–4.0).
118    pub a_skf: f64,
119}
120impl BearingLife {
121    /// Create a ball bearing life model.
122    pub fn ball_bearing(c: f64, p: f64) -> Self {
123        BearingLife {
124            c,
125            p,
126            life_exponent: 3.0,
127            a_iso: 1.0,
128            a_skf: 1.0,
129        }
130    }
131    /// Create a roller bearing life model.
132    pub fn roller_bearing(c: f64, p: f64) -> Self {
133        BearingLife {
134            c,
135            p,
136            life_exponent: 10.0 / 3.0,
137            a_iso: 1.0,
138            a_skf: 1.0,
139        }
140    }
141    /// Basic L10 life in millions of revolutions.
142    pub fn l10_mrv(&self) -> f64 {
143        if self.p < 1e-10 {
144            return f64::INFINITY;
145        }
146        (self.c / self.p).powf(self.life_exponent)
147    }
148    /// L10 life in hours given shaft speed n \[rpm\].
149    pub fn l10_hours(&self, n_rpm: f64) -> f64 {
150        if n_rpm < 1e-10 {
151            return f64::INFINITY;
152        }
153        self.l10_mrv() * 1e6 / (60.0 * n_rpm)
154    }
155    /// Modified L10m life (SKF): Lnm = a1 * a_ISO * a_SKF * L10.
156    /// `a1` = reliability factor (1.0 for 90% reliability).
157    pub fn l10m_hours(&self, n_rpm: f64, a1: f64) -> f64 {
158        a1 * self.a_iso * self.a_skf * self.l10_hours(n_rpm)
159    }
160    /// Weibull reliability at life t \[hours\], given L10 (90% reliability).
161    /// R(t) = exp\[-(t/L50)^e\] where e ≈ 1.5 for bearings.
162    pub fn reliability_at(&self, t_hours: f64, n_rpm: f64) -> f64 {
163        let l10 = self.l10_hours(n_rpm);
164        if l10 < 1e-10 {
165            return 0.0;
166        }
167        let l50 = 5.0 * l10;
168        let weibull_slope = 1.5_f64;
169        (-(t_hours / l50).powf(weibull_slope)).exp()
170    }
171    /// Equivalent dynamic load for combined radial Fr and axial Fa loads.
172    /// P = X * Fr + Y * Fa (simplified, X=1, Y=0 for pure radial).
173    pub fn equivalent_load(fr: f64, fa: f64, x: f64, y: f64) -> f64 {
174        x * fr + y * fa
175    }
176    /// Speed factor fn = (33.3/n)^(1/3) \[rpm\].
177    pub fn speed_factor(&self, n_rpm: f64) -> f64 {
178        if n_rpm < 1e-10 {
179            return 0.0;
180        }
181        (33.3 / n_rpm).powf(1.0 / 3.0)
182    }
183}
184/// Results of a Hertz contact calculation.
185#[derive(Debug, Clone, Copy)]
186pub struct HertzResult {
187    /// Contact radius \[m\].
188    pub contact_radius: f64,
189    /// Maximum contact pressure \[Pa\].
190    pub max_pressure: f64,
191    /// Contact depth (approach) \[m\].
192    pub depth: f64,
193    /// Normal force \[N\].
194    pub force: f64,
195}
196/// Thermal wear model including frictional heating and oxidative wear.
197#[derive(Debug, Clone, Copy)]
198pub struct ThermalWear {
199    /// Friction coefficient.
200    pub friction_coeff: f64,
201    /// Thermal conductivity of material 1 \[W/(m·K)\].
202    pub k1: f64,
203    /// Thermal conductivity of material 2 \[W/(m·K)\].
204    pub k2: f64,
205    /// Ambient temperature \[K\].
206    pub t_ambient: f64,
207    /// Oxidative wear activation energy Q \[J/mol\].
208    pub activation_energy: f64,
209    /// Pre-exponential wear factor A₀.
210    pub wear_factor_a0: f64,
211}
212impl ThermalWear {
213    /// Create a thermal wear model.
214    pub fn new(
215        friction_coeff: f64,
216        k1: f64,
217        k2: f64,
218        t_ambient: f64,
219        activation_energy: f64,
220        wear_factor_a0: f64,
221    ) -> Self {
222        Self {
223            friction_coeff,
224            k1,
225            k2,
226            t_ambient,
227            activation_energy,
228            wear_factor_a0,
229        }
230    }
231    /// Flash temperature rise at contact (Blok's equation), simplified.
232    /// `f` = friction force \[N\], `v` = sliding speed \[m/s\], `a` = contact radius \[m\].
233    pub fn flash_temperature(&self, f: f64, v: f64, a: f64) -> f64 {
234        if a < 1e-30 {
235            return self.t_ambient;
236        }
237        let heat_rate = self.friction_coeff * f * v;
238        let delta_t = heat_rate / (PI * a * (self.k1 + self.k2));
239        self.t_ambient + delta_t
240    }
241    /// Oxidative wear rate at temperature T (Arrhenius-type).
242    /// Returns wear rate \[m/s\].
243    pub fn oxidative_wear_rate(&self, temperature: f64) -> f64 {
244        const R_GAS: f64 = 8.314;
245        self.wear_factor_a0 * (-(self.activation_energy / (R_GAS * temperature))).exp()
246    }
247    /// Total wear volume \[m³\] over time `t` \[s\] at contact conditions.
248    pub fn total_wear_volume(&self, f: f64, v: f64, a: f64, t: f64) -> f64 {
249        let temp = self.flash_temperature(f, v, a);
250        let rate = self.oxidative_wear_rate(temp);
251        rate * a * a * t
252    }
253}
254/// Two-body and three-body abrasive wear model.
255#[derive(Debug, Clone, Copy)]
256pub struct AbrasiveWear {
257    /// Abrasion wear coefficient Ka (dimensionless).
258    pub ka: f64,
259    /// Hardness of abraded material \[Pa\].
260    pub hardness: f64,
261    /// Abrasive particle average size dp \[m\].
262    pub particle_size: f64,
263    /// Fraction of particles in contact (efficiency factor, 0–1).
264    pub efficiency: f64,
265}
266impl AbrasiveWear {
267    /// Create an abrasive wear model.
268    pub fn new(ka: f64, hardness: f64, particle_size: f64, efficiency: f64) -> Self {
269        AbrasiveWear {
270            ka,
271            hardness,
272            particle_size,
273            efficiency,
274        }
275    }
276    /// Two-body abrasive wear volume \[m³\] over distance s \[m\] under load W \[N\].
277    pub fn two_body_volume(&self, w: f64, s: f64) -> f64 {
278        self.ka * w * s / self.hardness
279    }
280    /// Three-body abrasive wear volume \[m³\] (factor ~3× lower than two-body).
281    pub fn three_body_volume(&self, w: f64, s: f64) -> f64 {
282        self.two_body_volume(w, s) / 3.0
283    }
284    /// Scratch depth per asperity pass \[m\] (Bowden-Tabor model).
285    pub fn scratch_depth(&self, w_per_asperity: f64) -> f64 {
286        (2.0 * w_per_asperity / (PI * self.hardness)).sqrt()
287    }
288    /// Volume per scratch \[m³\] for scratch length L \[m\].
289    pub fn volume_per_scratch(&self, w_per_asperity: f64, scratch_length: f64) -> f64 {
290        let depth = self.scratch_depth(w_per_asperity);
291        0.5 * PI * depth.powi(2) * scratch_length
292    }
293    /// Specific wear rate \[m³/(N·m)\] = Ka/H.
294    pub fn specific_wear_rate(&self) -> f64 {
295        self.ka / self.hardness
296    }
297}
298/// Elliptical Hertz contact parameters for general curved bodies.
299/// Based on Hertz contact theory for two bodies with different principal radii.
300#[derive(Debug, Clone, Copy)]
301pub struct HertzElliptical {
302    /// Effective modulus E* \[Pa\].
303    pub effective_modulus: f64,
304    /// Sum of principal curvatures (1/R1a + 1/R1b + 1/R2a + 1/R2b) \[1/m\].
305    pub sum_curvature: f64,
306    /// Difference parameter for ellipticity.
307    pub curvature_difference: f64,
308}
309impl HertzElliptical {
310    /// Create elliptical contact from two bodies' principal radii.
311    /// R1a, R1b = principal radii of body 1; R2a, R2b = principal radii of body 2.
312    #[allow(clippy::too_many_arguments)]
313    pub fn new(
314        e1: f64,
315        nu1: f64,
316        e2: f64,
317        nu2: f64,
318        r1a: f64,
319        r1b: f64,
320        r2a: f64,
321        r2b: f64,
322    ) -> Self {
323        let e_star = 1.0 / ((1.0 - nu1 * nu1) / e1 + (1.0 - nu2 * nu2) / e2);
324        let sum_k = 1.0 / r1a + 1.0 / r1b + 1.0 / r2a + 1.0 / r2b;
325        let diff_k = (1.0 / r1a - 1.0 / r1b + 1.0 / r2a - 1.0 / r2b)
326            .powi(2)
327            .sqrt();
328        HertzElliptical {
329            effective_modulus: e_star,
330            sum_curvature: sum_k,
331            curvature_difference: diff_k,
332        }
333    }
334    /// Ellipticity parameter k (semi-axis ratio b/a, ≤ 1).
335    /// Uses Hamrock approximation: k = (B/A)^(2/3) where A = (Σ-δ)/2, B = (Σ+δ)/2.
336    pub fn ellipticity_ratio(&self) -> f64 {
337        let cap_b = (self.sum_curvature + self.curvature_difference) / 2.0;
338        let cap_a = (self.sum_curvature - self.curvature_difference) / 2.0;
339        if cap_a < 1e-30 {
340            return 1.0;
341        }
342        (cap_b / cap_a).powf(2.0 / 3.0).min(1.0)
343    }
344    /// Contact semi-axis a \[m\] (major) under load F \[N\].
345    pub fn semi_axis_a(&self, force: f64) -> f64 {
346        let r_eff = 2.0 / self.sum_curvature;
347        let h = HertzContact {
348            effective_modulus: self.effective_modulus,
349            effective_radius: r_eff,
350        };
351        h.compute(force).contact_radius
352    }
353    /// Contact semi-axis b \[m\] (minor) = k * a.
354    pub fn semi_axis_b(&self, force: f64) -> f64 {
355        self.ellipticity_ratio() * self.semi_axis_a(force)
356    }
357    /// Maximum Hertz pressure p0 \[Pa\] for elliptical contact.
358    pub fn max_pressure(&self, force: f64) -> f64 {
359        let a = self.semi_axis_a(force);
360        let b = self.semi_axis_b(force);
361        if a < 1e-30 || b < 1e-30 {
362            return 0.0;
363        }
364        3.0 * force / (2.0 * PI * a * b)
365    }
366    /// Contact area (ellipse) \[m²\].
367    pub fn contact_area(&self, force: f64) -> f64 {
368        PI * self.semi_axis_a(force) * self.semi_axis_b(force)
369    }
370}
371/// Gear tooth tribology: pitch line velocity, sliding ratio, contact stress.
372#[derive(Debug, Clone)]
373pub struct GearTooth {
374    /// Module m \[mm\].
375    pub module: f64,
376    /// Number of teeth on gear 1.
377    pub z1: u32,
378    /// Number of teeth on gear 2.
379    pub z2: u32,
380    /// Pressure angle φ \[degrees\].
381    pub pressure_angle: f64,
382    /// Face width b \[mm\].
383    pub face_width: f64,
384    /// Transmitted tangential force Wt \[N\].
385    pub wt: f64,
386    /// Effective elastic coefficient ZE \[(N/mm²)^0.5\].
387    pub ze: f64,
388    /// Speed of gear 1 n1 \[rpm\].
389    pub n1: f64,
390}
391impl GearTooth {
392    /// Create a spur gear pair.
393    pub fn spur_pair(
394        module: f64,
395        z1: u32,
396        z2: u32,
397        pressure_angle: f64,
398        face_width: f64,
399        wt: f64,
400        n1: f64,
401    ) -> Self {
402        GearTooth {
403            module,
404            z1,
405            z2,
406            pressure_angle,
407            face_width,
408            wt,
409            ze: 191.0,
410            n1,
411        }
412    }
413    /// Pitch diameter of gear 1 d1 \[mm\].
414    pub fn d1(&self) -> f64 {
415        self.module * self.z1 as f64
416    }
417    /// Pitch diameter of gear 2 d2 \[mm\].
418    pub fn d2(&self) -> f64 {
419        self.module * self.z2 as f64
420    }
421    /// Pitch line velocity Vp \[m/s\].
422    pub fn pitch_line_velocity(&self) -> f64 {
423        PI * self.d1() * self.n1 / (60.0 * 1000.0)
424    }
425    /// Gear ratio i = z2/z1.
426    pub fn gear_ratio(&self) -> f64 {
427        self.z2 as f64 / self.z1 as f64
428    }
429    /// Contact ratio εα (profile contact ratio) for spur gear.
430    pub fn contact_ratio(&self) -> f64 {
431        let phi = self.pressure_angle.to_radians();
432        let r1 = self.d1() / 2.0;
433        let r2 = self.d2() / 2.0;
434        let ra1 = r1 + self.module;
435        let ra2 = r2 + self.module;
436        let cp = (self.d1() + self.d2()) / 2.0 * phi.sin();
437        let term1 = (ra1.powi(2) - (r1 * phi.cos()).powi(2)).sqrt();
438        let term2 = (ra2.powi(2) - (r2 * phi.cos()).powi(2)).sqrt();
439        (term1 + term2 - cp) / (PI * self.module * phi.cos())
440    }
441    /// Hertz contact stress at pitch point σH \[N/mm²\] (AGMA/ISO simplified).
442    pub fn hertz_contact_stress(&self) -> f64 {
443        let phi = self.pressure_angle.to_radians();
444        let d1 = self.d1();
445        let i = self.gear_ratio();
446        let term = self.wt / (d1 * self.face_width) * (i + 1.0) / i / phi.cos();
447        if term < 0.0 {
448            return 0.0;
449        }
450        self.ze * term.sqrt()
451    }
452    /// Sliding velocity at pitch point offset s \[mm\] from pitch point.
453    pub fn sliding_velocity_at(&self, s_mm: f64) -> f64 {
454        let _vp = self.pitch_line_velocity() * 1000.0;
455        let omega1 = 2.0 * PI * self.n1 / 60.0;
456        let i = self.gear_ratio();
457        omega1 * s_mm * (1.0 + 1.0 / i) / 1000.0
458    }
459    /// Specific sliding ratio at arbitrary contact point (simplified AGMA).
460    pub fn sliding_ratio(&self) -> f64 {
461        let phi = self.pressure_angle.to_radians();
462        2.0 * PI * (1.0 / self.z1 as f64 + 1.0 / self.z2 as f64) / (PI * phi.cos())
463            * self.contact_ratio()
464    }
465    /// Power loss from friction \[W\], given friction coefficient μ.
466    pub fn friction_power_loss(&self, mu: f64) -> f64 {
467        let vs = self.sliding_velocity_at(self.module);
468        mu * self.wt * vs.abs()
469    }
470}
471/// Brake pad wear and thermal analysis for disc brakes.
472#[derive(Debug, Clone)]
473pub struct BrakePad {
474    /// Normal force on pad Fn \[N\].
475    pub fn_force: f64,
476    /// Friction coefficient μ.
477    pub mu: f64,
478    /// Rotor radius (effective rubbing radius) r \[m\].
479    pub rotor_radius: f64,
480    /// Pad area A \[m²\].
481    pub pad_area: f64,
482    /// Thermal conductivity of pad kp \[W/(m·K)\].
483    pub k_pad: f64,
484    /// Heat partition factor for pad βp (fraction of heat to pad, 0–1).
485    pub beta_pad: f64,
486    /// Pad wear coefficient Kw \[m²/N\].
487    pub kw: f64,
488    /// Pad thickness tp \[m\].
489    pub tp: f64,
490    /// Specific heat of pad cp \[J/(kg·K)\].
491    pub cp_pad: f64,
492    /// Density of pad \[kg/m³\].
493    pub density_pad: f64,
494}
495impl BrakePad {
496    /// Create a typical semi-metallic brake pad.
497    pub fn semi_metallic() -> Self {
498        BrakePad {
499            fn_force: 3000.0,
500            mu: 0.40,
501            rotor_radius: 0.12,
502            pad_area: 50e-4,
503            k_pad: 2.0,
504            beta_pad: 0.05,
505            kw: 1e-14,
506            tp: 0.012,
507            cp_pad: 900.0,
508            density_pad: 2500.0,
509        }
510    }
511    /// Braking torque Tb \[N·m\].
512    pub fn braking_torque(&self) -> f64 {
513        self.mu * self.fn_force * self.rotor_radius
514    }
515    /// Frictional power dissipation Q \[W\] at sliding speed v \[m/s\].
516    pub fn frictional_power(&self, v: f64) -> f64 {
517        self.mu * self.fn_force * v
518    }
519    /// Average interface temperature rise ΔT \[K\] (steady-state).
520    pub fn temperature_rise_steady(&self, v: f64) -> f64 {
521        let q = self.frictional_power(v) * self.beta_pad;
522        if self.k_pad < 1e-10 || self.pad_area < 1e-10 {
523            return 0.0;
524        }
525        q * self.tp / (self.k_pad * self.pad_area)
526    }
527    /// Transient temperature rise \[K\] after time t \[s\], simplified lumped capacity.
528    pub fn temperature_rise_transient(&self, v: f64, t: f64) -> f64 {
529        let q = self.frictional_power(v) * self.beta_pad;
530        let mass = self.density_pad * self.pad_area * self.tp;
531        if mass < 1e-10 {
532            return 0.0;
533        }
534        q * t / (mass * self.cp_pad)
535    }
536    /// Pad wear depth \[m\] over sliding distance s \[m\].
537    pub fn wear_depth(&self, s: f64) -> f64 {
538        self.kw * self.fn_force * s / self.pad_area
539    }
540    /// Remaining pad life \[m of sliding distance\] from initial thickness tp.
541    pub fn remaining_life(&self, current_thickness: f64, min_thickness: f64) -> f64 {
542        let wear_allowed = current_thickness - min_thickness;
543        if wear_allowed <= 0.0 {
544            return 0.0;
545        }
546        wear_allowed * self.pad_area / (self.kw * self.fn_force)
547    }
548    /// Contact pressure p \[Pa\] = Fn / A.
549    pub fn contact_pressure(&self) -> f64 {
550        self.fn_force / self.pad_area
551    }
552}
553/// Lubrication regime based on the Stribeck curve and lambda ratio.
554#[derive(Debug, Clone, Copy, PartialEq)]
555pub enum LubricationRegimeKind {
556    /// Boundary lubrication (λ < 1).
557    Boundary,
558    /// Mixed lubrication (1 ≤ λ < 3).
559    Mixed,
560    /// Elastohydrodynamic lubrication (3 ≤ λ < 10).
561    Elastohydrodynamic,
562    /// Full-film hydrodynamic lubrication (λ ≥ 10).
563    FullFilm,
564}
565/// Surface roughness parameters from ISO 4287 / ASME B46.1.
566#[derive(Debug, Clone)]
567pub struct SurfaceRoughness {
568    /// Arithmetic mean deviation Ra \[m\].
569    pub ra: f64,
570    /// Root-mean-square roughness Rq \[m\].
571    pub rq: f64,
572    /// Maximum height of profile Rz \[m\].
573    pub rz: f64,
574    /// Skewness Rsk (dimensionless).
575    pub rsk: f64,
576    /// Kurtosis Rku (dimensionless; 3.0 for Gaussian).
577    pub rku: f64,
578    /// Mean spacing of profile irregularities RSm \[m\].
579    pub rsm: f64,
580}
581impl SurfaceRoughness {
582    /// Create a surface roughness profile.
583    pub fn new(ra: f64, rq: f64, rz: f64, rsk: f64, rku: f64, rsm: f64) -> Self {
584        SurfaceRoughness {
585            ra,
586            rq,
587            rz,
588            rsk,
589            rku,
590            rsm,
591        }
592    }
593    /// Create a ground steel surface (typical Ra = 0.8 µm).
594    pub fn ground_steel() -> Self {
595        SurfaceRoughness {
596            ra: 0.8e-6,
597            rq: 1.0e-6,
598            rz: 4.0e-6,
599            rsk: 0.0,
600            rku: 3.0,
601            rsm: 80.0e-6,
602        }
603    }
604    /// Create a lapped/polished surface (Ra ≈ 0.1 µm).
605    pub fn lapped() -> Self {
606        SurfaceRoughness {
607            ra: 0.1e-6,
608            rq: 0.12e-6,
609            rz: 0.6e-6,
610            rsk: -0.2,
611            rku: 3.5,
612            rsm: 20.0e-6,
613        }
614    }
615    /// Combined roughness of two surfaces (root-sum-square).
616    pub fn combined_rq(&self, other: &SurfaceRoughness) -> f64 {
617        (self.rq.powi(2) + other.rq.powi(2)).sqrt()
618    }
619    /// Rq / Ra ratio (should be ~1.25 for Gaussian surface).
620    pub fn rq_ra_ratio(&self) -> f64 {
621        if self.ra < 1e-30 {
622            return 0.0;
623        }
624        self.rq / self.ra
625    }
626    /// Surface texture index STI = Rz / Ra (bearing capacity quality indicator).
627    pub fn surface_texture_index(&self) -> f64 {
628        if self.ra < 1e-30 {
629            return 0.0;
630        }
631        self.rz / self.ra
632    }
633    /// Abbott-Firestone bearing area curve at normalised depth z (0–1).
634    /// Returns fraction of surface in contact (0–1).
635    pub fn bearing_area(&self, z_norm: f64) -> f64 {
636        let threshold = self.rz * (1.0 - z_norm) - self.ra;
637        let z = threshold / (self.rq * 2.0_f64.sqrt());
638        0.5 * erfc(z)
639    }
640    /// Valley depth Rv (estimated as Rz * Rsk factor).
641    pub fn valley_depth(&self) -> f64 {
642        self.rz * if self.rsk < 0.0 { 0.6 } else { 0.4 }
643    }
644}
645/// Run-in friction evolution: exponential decrease from initial to steady-state friction.
646#[derive(Debug, Clone, Copy)]
647pub struct FrictionEvolution {
648    /// Initial friction coefficient (start of run-in).
649    pub mu_initial: f64,
650    /// Steady-state friction coefficient.
651    pub mu_steady: f64,
652    /// Run-in time constant τ \[s or m\].
653    pub time_constant: f64,
654    /// Current time/distance state.
655    pub time: f64,
656}
657impl FrictionEvolution {
658    /// Create a friction evolution model.
659    pub fn new(mu_initial: f64, mu_steady: f64, time_constant: f64) -> Self {
660        Self {
661            mu_initial,
662            mu_steady,
663            time_constant,
664            time: 0.0,
665        }
666    }
667    /// Get friction coefficient at current time `t`.
668    pub fn friction_at(&self, t: f64) -> f64 {
669        self.mu_steady + (self.mu_initial - self.mu_steady) * (-(t / self.time_constant)).exp()
670    }
671    /// Step simulation by `dt`.
672    pub fn step(&mut self, dt: f64) {
673        self.time += dt;
674    }
675    /// Current friction coefficient.
676    pub fn current_friction(&self) -> f64 {
677        self.friction_at(self.time)
678    }
679    /// Time to reach within `epsilon` of steady state.
680    pub fn convergence_time(&self, epsilon: f64) -> f64 {
681        let range = (self.mu_initial - self.mu_steady).abs();
682        if range < 1e-12 {
683            return 0.0;
684        }
685        -self.time_constant * (epsilon / range).ln()
686    }
687}
688/// Lubricant viscosity models including Reynolds and Walther equations.
689#[derive(Debug, Clone, Copy)]
690pub struct LubricantViscosity {
691    /// Dynamic viscosity at reference temperature T0 \[K\]: η0 \[Pa·s\].
692    pub eta0: f64,
693    /// Reference temperature T0 \[K\].
694    pub t0: f64,
695    /// Reynolds temperature coefficient β \[1/K\].
696    pub beta: f64,
697    /// Walther equation constants: log(log(ν+c)) = A - B*log(T).
698    pub walther_a: f64,
699    /// Walther B constant.
700    pub walther_b: f64,
701    /// Density at reference temperature ρ0 \[kg/m³\].
702    pub density: f64,
703}
704impl LubricantViscosity {
705    /// Create a viscosity model for SAE 10W-40 motor oil (approximate).
706    pub fn sae10w40() -> Self {
707        LubricantViscosity {
708            eta0: 0.1,
709            t0: 313.15,
710            beta: 0.028,
711            walther_a: 10.5,
712            walther_b: 3.5,
713            density: 870.0,
714        }
715    }
716    /// Create a model for ISO VG 46 hydraulic oil.
717    pub fn iso_vg46() -> Self {
718        LubricantViscosity {
719            eta0: 0.046,
720            t0: 313.15,
721            beta: 0.025,
722            walther_a: 9.8,
723            walther_b: 3.3,
724            density: 875.0,
725        }
726    }
727    /// Dynamic viscosity at temperature T \[K\] using Reynolds equation.
728    pub fn eta_reynolds(&self, t: f64) -> f64 {
729        self.eta0 * (-(self.beta * (t - self.t0))).exp()
730    }
731    /// Kinematic viscosity \[m²/s\] at temperature T \[K\].
732    pub fn nu_kinematic(&self, t: f64) -> f64 {
733        self.eta_reynolds(t) / self.density
734    }
735    /// Barus equation for pressure-dependent viscosity η(p) \[Pa·s\].
736    pub fn eta_barus(&self, t: f64, alpha_p: f64, pressure: f64) -> f64 {
737        self.eta_reynolds(t) * (alpha_p * pressure).exp()
738    }
739    /// Viscosity index (VI) — higher = less temperature sensitivity.
740    /// Simplified: VI = (η40 - η100) / (L - H) where L,H are reference viscosities.
741    pub fn viscosity_index(&self) -> f64 {
742        let eta40 = self.eta_reynolds(313.15);
743        let eta100 = self.eta_reynolds(373.15);
744        if eta100 < 1e-10 {
745            return 0.0;
746        }
747        100.0 * (eta40 - eta100) / eta100
748    }
749    /// Pour point estimate \[K\] below which oil loses flowability.
750    pub fn pour_point_estimate(&self) -> f64 {
751        let target_eta = 3.0;
752        if self.eta0 <= target_eta {
753            return self.t0;
754        }
755        self.t0 - (target_eta / self.eta0).ln() / self.beta
756    }
757}
758/// Fretting fatigue model: stick-slip regimes, fretting loop, fretting fatigue life.
759#[derive(Debug, Clone)]
760pub struct FrettingFatigue {
761    /// Normal load Q \[N\] on contact.
762    pub normal_load: f64,
763    /// Friction coefficient μ (Coulomb).
764    pub mu: f64,
765    /// Tangential amplitude δ \[m\].
766    pub amplitude: f64,
767    /// Contact stiffness kt \[N/m\].
768    pub contact_stiffness: f64,
769    /// Fatigue strength of material σ_fat \[Pa\].
770    pub fatigue_strength: f64,
771    /// Paris law exponent m for fretting crack propagation.
772    pub paris_m: f64,
773    /// Paris law coefficient C \[m/cycle / (MPa√m)^m\].
774    pub paris_c: f64,
775    /// Initial crack half-length a0 \[m\].
776    pub a0: f64,
777}
778impl FrettingFatigue {
779    /// Create a fretting fatigue model.
780    pub fn new(
781        normal_load: f64,
782        mu: f64,
783        amplitude: f64,
784        contact_stiffness: f64,
785        fatigue_strength: f64,
786    ) -> Self {
787        FrettingFatigue {
788            normal_load,
789            mu,
790            amplitude,
791            contact_stiffness,
792            fatigue_strength,
793            paris_m: 3.0,
794            paris_c: 1e-11,
795            a0: 10e-6,
796        }
797    }
798    /// Maximum tangential (friction) force in the fretting loop Qmax \[N\].
799    pub fn max_tangential_force(&self) -> f64 {
800        self.mu * self.normal_load
801    }
802    /// Stick-slip threshold displacement δs \[m\].
803    pub fn stick_slip_threshold(&self) -> f64 {
804        self.max_tangential_force() / self.contact_stiffness
805    }
806    /// Determine if gross slip occurs (δ > δs).
807    pub fn is_gross_slip(&self) -> bool {
808        self.amplitude > self.stick_slip_threshold()
809    }
810    /// Fretting loop energy dissipated per cycle W \[J\].
811    pub fn energy_per_cycle(&self) -> f64 {
812        if self.is_gross_slip() {
813            let delta_s = self.stick_slip_threshold();
814            4.0 * self.max_tangential_force() * (self.amplitude - delta_s)
815        } else {
816            let ratio = self.amplitude / self.stick_slip_threshold();
817            let w_stick = 4.0 * self.contact_stiffness * self.amplitude.powi(2);
818            w_stick * (1.0 - (1.0 - ratio).powi(3)) / 3.0
819        }
820    }
821    /// Stress intensity factor range ΔK \[Pa√m\] for crack size a \[m\].
822    pub fn stress_intensity_range(&self, stress_range: f64, a: f64) -> f64 {
823        stress_range * (PI * a).sqrt()
824    }
825    /// Cycles to grow crack from a0 to ac \[m\] (Paris law integration).
826    pub fn cycles_to_crack(&self, stress_range: f64, a_critical: f64) -> f64 {
827        let m = self.paris_m;
828        let c = self.paris_c;
829        let f_sig = stress_range * PI.sqrt();
830        let exponent = 1.0 - m / 2.0;
831        if exponent.abs() < 1e-10 {
832            return (a_critical / self.a0).ln() / (c * f_sig.powi(2));
833        }
834        let denom = c * f_sig.powf(m) * exponent;
835        if denom.abs() < 1e-30 {
836            return f64::INFINITY;
837        }
838        (a_critical.powf(exponent) - self.a0.powf(exponent)) / denom
839    }
840    /// Fretting fatigue factor (stress concentration effect), simplified.
841    pub fn fretting_factor(&self) -> f64 {
842        let q_max = self.max_tangential_force();
843        1.0 + q_max / self.fatigue_strength.max(1.0)
844    }
845}
846/// Hertz contact mechanics for elastic bodies.
847#[derive(Debug, Clone, Copy)]
848pub struct HertzContact {
849    /// Effective modulus E* \[Pa\].
850    pub effective_modulus: f64,
851    /// Effective radius R* \[m\].
852    pub effective_radius: f64,
853}
854impl HertzContact {
855    /// Create from two sphere moduli, Poisson ratios, and radii.
856    /// Computes E* = 1 / ((1-ν₁²)/E₁ + (1-ν₂²)/E₂) and R* = R₁*R₂/(R₁+R₂).
857    pub fn sphere_sphere(e1: f64, nu1: f64, r1: f64, e2: f64, nu2: f64, r2: f64) -> Self {
858        let e_star = 1.0 / ((1.0 - nu1 * nu1) / e1 + (1.0 - nu2 * nu2) / e2);
859        let r_star = r1 * r2 / (r1 + r2);
860        Self {
861            effective_modulus: e_star,
862            effective_radius: r_star,
863        }
864    }
865    /// Create for sphere on a flat surface (R₂ → ∞).
866    pub fn sphere_flat(e1: f64, nu1: f64, r: f64, e2: f64, nu2: f64) -> Self {
867        let e_star = 1.0 / ((1.0 - nu1 * nu1) / e1 + (1.0 - nu2 * nu2) / e2);
868        Self {
869            effective_modulus: e_star,
870            effective_radius: r,
871        }
872    }
873    /// Create for two parallel cylinders of length L (per unit length analysis).
874    /// Uses 2D Hertz: a = sqrt(4*F*R*/(π*E*)).
875    pub fn cylinder_cylinder(e1: f64, nu1: f64, r1: f64, e2: f64, nu2: f64, r2: f64) -> Self {
876        let e_star = 1.0 / ((1.0 - nu1 * nu1) / e1 + (1.0 - nu2 * nu2) / e2);
877        let r_star = r1 * r2 / (r1 + r2);
878        Self {
879            effective_modulus: e_star,
880            effective_radius: r_star,
881        }
882    }
883    /// Compute Hertz contact parameters for a given normal force \[N\].
884    pub fn compute(&self, force: f64) -> HertzResult {
885        let a = ((3.0 * force * self.effective_radius) / (4.0 * self.effective_modulus))
886            .powf(1.0 / 3.0);
887        let p0 = if a > 1e-30 {
888            3.0 * force / (2.0 * PI * a * a)
889        } else {
890            0.0
891        };
892        let depth = a * a / self.effective_radius;
893        HertzResult {
894            contact_radius: a,
895            max_pressure: p0,
896            depth,
897            force,
898        }
899    }
900    /// Compute required force for a given contact radius.
901    pub fn force_for_radius(&self, a: f64) -> f64 {
902        4.0 * self.effective_modulus * a * a * a / (3.0 * self.effective_radius)
903    }
904}
905/// Greenwood-Williamson multi-asperity contact model.
906#[derive(Debug, Clone)]
907pub struct AsperityModel {
908    /// Asperity density \[1/m²\].
909    pub asperity_density: f64,
910    /// RMS surface roughness σ \[m\].
911    pub rms_roughness: f64,
912    /// Asperity tip radius β \[m\].
913    pub tip_radius: f64,
914    /// Effective elastic modulus \[Pa\].
915    pub effective_modulus: f64,
916}
917impl AsperityModel {
918    /// Create a Greenwood-Williamson model.
919    pub fn new(
920        asperity_density: f64,
921        rms_roughness: f64,
922        tip_radius: f64,
923        effective_modulus: f64,
924    ) -> Self {
925        Self {
926            asperity_density,
927            rms_roughness,
928            tip_radius,
929            effective_modulus,
930        }
931    }
932    /// Plasticity index: Ψ = (E*/H) * sqrt(σ/β).
933    pub fn plasticity_index(&self, hardness: f64) -> f64 {
934        (self.effective_modulus / hardness) * (self.rms_roughness / self.tip_radius).sqrt()
935    }
936    /// Expected number of asperities in contact for separation `d` \[m\].
937    /// Uses Gaussian distribution of asperity heights.
938    pub fn n_contacts(&self, area: f64, separation: f64) -> f64 {
939        let z = separation / (2.0_f64.sqrt() * self.rms_roughness);
940        let p = 0.5 * erfc(z);
941        self.asperity_density * area * p
942    }
943    /// Bearing area (ratio of real to apparent contact area).
944    pub fn bearing_area(&self, separation: f64) -> f64 {
945        let z = separation / (2.0_f64.sqrt() * self.rms_roughness);
946        0.5 * erfc(z)
947    }
948    /// Total normal load for given separation d over nominal area A.
949    pub fn normal_load(&self, area: f64, separation: f64) -> f64 {
950        let n = self.n_contacts(area, separation);
951        let avg_deform = self.rms_roughness;
952        let f_per =
953            (4.0 / 3.0) * self.effective_modulus * self.tip_radius.sqrt() * avg_deform.powf(1.5);
954        n * f_per
955    }
956}
957/// Extended Stribeck curve with boundary, mixed, and hydrodynamic regimes.
958#[derive(Debug, Clone)]
959pub struct StribeckCurve {
960    /// Boundary friction coefficient μb.
961    pub mu_boundary: f64,
962    /// Hydrodynamic (full film) friction coefficient μf.
963    pub mu_fullfilm: f64,
964    /// Stribeck constant Sc \[Pa·s · m/s / Pa\] = \[m\].
965    pub sc: f64,
966    /// Exponent controlling sharpness of the Stribeck minimum (typically 0.5–2.0).
967    pub exponent: f64,
968    /// Minimum friction coefficient (Stribeck minimum), typically between μb and μf.
969    pub mu_min: f64,
970    /// Viscosity-speed-load number at Stribeck minimum.
971    pub sn_min: f64,
972}
973impl StribeckCurve {
974    /// Create a Stribeck curve model.
975    pub fn new(mu_boundary: f64, mu_fullfilm: f64, sc: f64) -> Self {
976        StribeckCurve {
977            mu_boundary,
978            mu_fullfilm,
979            sc,
980            exponent: 1.0,
981            mu_min: (mu_boundary + mu_fullfilm) / 2.0,
982            sn_min: sc,
983        }
984    }
985    /// Hersey number (Stribeck number) Hs = η * N / P \[m\].
986    pub fn hersey_number(&self, eta: f64, n: f64, p: f64) -> f64 {
987        if p < 1e-10 {
988            return f64::INFINITY;
989        }
990        eta * n / p
991    }
992    /// Friction coefficient at Hersey number Hs.
993    /// Uses Houpert's analytical Stribeck model.
994    pub fn mu_at_hersey(&self, hs: f64) -> f64 {
995        let transition_hs = self.sc / 10.0;
996        if hs <= transition_hs {
997            self.mu_boundary
998        } else {
999            let x = (hs / self.sc).ln();
1000            self.mu_min
1001                + (self.mu_boundary - self.mu_min) * (-x.powi(2) / 2.0).exp()
1002                + (self.mu_fullfilm - self.mu_min) * (1.0 - (-hs / self.sc).exp())
1003        }
1004    }
1005    /// Regime classification.
1006    pub fn classify(&self, lambda: f64) -> LubricationRegimeKind {
1007        if lambda < 1.0 {
1008            LubricationRegimeKind::Boundary
1009        } else if lambda < 3.0 {
1010            LubricationRegimeKind::Mixed
1011        } else if lambda < 10.0 {
1012            LubricationRegimeKind::Elastohydrodynamic
1013        } else {
1014            LubricationRegimeKind::FullFilm
1015        }
1016    }
1017    /// Viscous shear stress in full-film regime \[Pa\].
1018    pub fn viscous_shear_stress(&self, eta: f64, v: f64, h: f64) -> f64 {
1019        if h < 1e-30 {
1020            return 0.0;
1021        }
1022        eta * v / h
1023    }
1024}
1025/// Substrate + coating system with mechanical properties.
1026#[derive(Debug, Clone)]
1027pub struct CoatingMaterial {
1028    /// Substrate Young's modulus \[Pa\].
1029    pub substrate_modulus: f64,
1030    /// Coating Young's modulus \[Pa\].
1031    pub coating_modulus: f64,
1032    /// Coating thickness \[m\].
1033    pub coating_thickness: f64,
1034    /// Vickers hardness of coating \[Pa\].
1035    pub vickers_hardness: f64,
1036    /// Adhesion strength (critical load for delamination) \[N\].
1037    pub adhesion_strength: f64,
1038    /// Coating density \[kg/m³\].
1039    pub density: f64,
1040}
1041impl CoatingMaterial {
1042    /// Create a coating system.
1043    pub fn new(
1044        substrate_modulus: f64,
1045        coating_modulus: f64,
1046        coating_thickness: f64,
1047        vickers_hardness: f64,
1048        adhesion_strength: f64,
1049        density: f64,
1050    ) -> Self {
1051        Self {
1052            substrate_modulus,
1053            coating_modulus,
1054            coating_thickness,
1055            vickers_hardness,
1056            adhesion_strength,
1057            density,
1058        }
1059    }
1060    /// Effective composite hardness (depth-dependent, simplified).
1061    /// Uses Bückle's rule: H_eff decreases from coating toward substrate beyond ~10% thickness.
1062    pub fn effective_hardness(&self, indent_depth: f64) -> f64 {
1063        let ratio = indent_depth / self.coating_thickness;
1064        if ratio < 0.1 {
1065            self.vickers_hardness
1066        } else {
1067            let h_sub = 0.3 * self.vickers_hardness;
1068            let t = ((ratio - 0.1) / 0.9).min(1.0);
1069            self.vickers_hardness * (1.0 - t) + h_sub * t
1070        }
1071    }
1072    /// Check if a given scratch load exceeds adhesion limit (delamination risk).
1073    pub fn will_delaminate(&self, scratch_load: f64) -> bool {
1074        scratch_load > self.adhesion_strength
1075    }
1076    /// Modulus ratio (coating / substrate).
1077    pub fn modulus_ratio(&self) -> f64 {
1078        self.coating_modulus / self.substrate_modulus
1079    }
1080}
1081/// Bowden-Tabor adhesion model for friction coefficient.
1082#[derive(Debug, Clone, Copy)]
1083pub struct BowdenTaborFriction {
1084    /// Shear strength of interfacial film s \[Pa\].
1085    pub shear_strength: f64,
1086    /// Hardness of softer surface H \[Pa\].
1087    pub hardness: f64,
1088    /// Ploughing term contribution (dimensionless, typically 0.01–0.1).
1089    pub ploughing: f64,
1090}
1091impl BowdenTaborFriction {
1092    /// Create a Bowden-Tabor friction model.
1093    pub fn new(shear_strength: f64, hardness: f64, ploughing: f64) -> Self {
1094        BowdenTaborFriction {
1095            shear_strength,
1096            hardness,
1097            ploughing,
1098        }
1099    }
1100    /// Adhesion component of friction coefficient: μ_adh = s / H.
1101    pub fn mu_adhesion(&self) -> f64 {
1102        self.shear_strength / self.hardness
1103    }
1104    /// Total friction coefficient: μ = μ_adh + μ_plough.
1105    pub fn mu_total(&self) -> f64 {
1106        self.mu_adhesion() + self.ploughing
1107    }
1108    /// Real contact area fraction: Ar/An = P / H.
1109    pub fn real_contact_fraction(&self, pressure: f64) -> f64 {
1110        pressure / self.hardness
1111    }
1112    /// Friction force \[N\] for given normal load \[N\].
1113    pub fn friction_force(&self, normal_load: f64) -> f64 {
1114        self.mu_total() * normal_load
1115    }
1116}
1117/// Hamrock-Dowson EHL point contact film thickness model.
1118#[derive(Debug, Clone, Copy)]
1119pub struct HamrockDowson {
1120    /// Lubricant dynamic viscosity η0 \[Pa·s\].
1121    pub eta0: f64,
1122    /// Pressure-viscosity coefficient α \[1/Pa\].
1123    pub alpha_visc: f64,
1124    /// Effective elastic modulus E' \[Pa\].
1125    pub e_prime: f64,
1126    /// Effective radius in x-direction Rx \[m\].
1127    pub rx: f64,
1128    /// Effective radius in y-direction Ry \[m\].
1129    pub ry: f64,
1130}
1131impl HamrockDowson {
1132    /// Create a Hamrock-Dowson EHL model.
1133    pub fn new(eta0: f64, alpha_visc: f64, e_prime: f64, rx: f64, ry: f64) -> Self {
1134        HamrockDowson {
1135            eta0,
1136            alpha_visc,
1137            e_prime,
1138            rx,
1139            ry,
1140        }
1141    }
1142    /// Effective radius Re = sqrt(Rx * Ry).
1143    pub fn re(&self) -> f64 {
1144        (self.rx * self.ry).sqrt()
1145    }
1146    /// Ellipticity ratio k = Rx/Ry (or Ry/Rx, take >1).
1147    pub fn ellipticity(&self) -> f64 {
1148        (self.rx / self.ry).max(self.ry / self.rx)
1149    }
1150    /// Dimensionless speed parameter U = η0*u / (E' * Re).
1151    pub fn speed_param(&self, u: f64) -> f64 {
1152        self.eta0 * u / (self.e_prime * self.re())
1153    }
1154    /// Dimensionless load parameter W = F / (E' * Re²).
1155    pub fn load_param(&self, force: f64) -> f64 {
1156        force / (self.e_prime * self.re().powi(2))
1157    }
1158    /// Dimensionless material parameter G = α * E'.
1159    pub fn material_param(&self) -> f64 {
1160        self.alpha_visc * self.e_prime
1161    }
1162    /// Central film thickness hc \[m\] using Hamrock-Dowson (1977).
1163    /// hc/Re = 2.69 * U^0.67 * G^0.53 * W^(-0.067) * (1 - 0.61*exp(-0.73*k)).
1164    pub fn central_film_thickness(&self, u: f64, force: f64) -> f64 {
1165        let uu = self.speed_param(u);
1166        let ww = self.load_param(force);
1167        let gg = self.material_param();
1168        let k = self.ellipticity();
1169        if ww < 1e-30 {
1170            return 0.0;
1171        }
1172        let hc_re = 2.69
1173            * uu.powf(0.67)
1174            * gg.powf(0.53)
1175            * ww.powf(-0.067)
1176            * (1.0 - 0.61 * (-0.73 * k).exp());
1177        hc_re * self.re()
1178    }
1179    /// Minimum film thickness hmin \[m\] using Hamrock-Dowson.
1180    /// hmin/Re = 3.63 * U^0.68 * G^0.49 * W^(-0.073) * (1 - exp(-0.68*k)).
1181    pub fn minimum_film_thickness(&self, u: f64, force: f64) -> f64 {
1182        let uu = self.speed_param(u);
1183        let ww = self.load_param(force);
1184        let gg = self.material_param();
1185        let k = self.ellipticity();
1186        if ww < 1e-30 {
1187            return 0.0;
1188        }
1189        let hmin_re =
1190            3.63 * uu.powf(0.68) * gg.powf(0.49) * ww.powf(-0.073) * (1.0 - (-0.68 * k).exp());
1191        hmin_re * self.re()
1192    }
1193    /// Lambda ratio Λ = hmin / σ_combined.
1194    pub fn lambda_ratio(&self, u: f64, force: f64, roughness: f64) -> f64 {
1195        let hmin = self.minimum_film_thickness(u, force);
1196        if roughness < 1e-30 {
1197            return f64::INFINITY;
1198        }
1199        hmin / roughness
1200    }
1201}
1202/// Elastohydrodynamic (EHL) film thickness model (Dowson-Higginson).
1203///
1204/// Computes the central and minimum film thickness for a lubricated
1205/// point/line contact using Dowson-Higginson parameters.
1206#[derive(Debug, Clone, Copy)]
1207pub struct ElastohydrodynamicFilm {
1208    /// Ambient dynamic viscosity η₀ \[Pa·s\].
1209    pub eta0: f64,
1210    /// Pressure-viscosity coefficient α \[1/Pa\].
1211    pub alpha: f64,
1212    /// Entrainment speed u \[m/s\].
1213    pub u: f64,
1214    /// Effective radius R* \[m\].
1215    pub r: f64,
1216    /// Reduced (plane-strain) modulus E* \[Pa\].
1217    pub e_star: f64,
1218}
1219impl ElastohydrodynamicFilm {
1220    /// Create an `ElastohydrodynamicFilm`.
1221    pub fn new(eta0: f64, alpha: f64, u: f64, r: f64, e_star: f64) -> Self {
1222        Self {
1223            eta0,
1224            alpha,
1225            u,
1226            r,
1227            e_star,
1228        }
1229    }
1230    /// Dimensionless speed parameter U = η₀·u / (E*·R).
1231    fn speed_param(&self) -> f64 {
1232        self.eta0 * self.u / (self.e_star * self.r)
1233    }
1234    /// Dimensionless materials parameter G = α·E*.
1235    fn material_param(&self) -> f64 {
1236        self.alpha * self.e_star
1237    }
1238    /// Dimensionless load parameter W = F / (E*·R²).
1239    fn load_param(&self, force: f64) -> f64 {
1240        force / (self.e_star * self.r * self.r)
1241    }
1242    /// Central film thickness \[m\] using Dowson-Higginson formula.
1243    ///
1244    /// Hc = 2.69 · U^0.67 · G^0.53 · W^(-0.067) · R
1245    pub fn central_film_thickness(&self, force: f64) -> f64 {
1246        let u_dim = self.speed_param();
1247        let g_dim = self.material_param();
1248        let w_dim = self.load_param(force).max(1e-30);
1249        2.69 * u_dim.powf(0.67) * g_dim.powf(0.53) * w_dim.powf(-0.067) * self.r
1250    }
1251    /// Minimum film thickness \[m\]; approximately 75 % of central.
1252    ///
1253    /// Hmin = 0.75 · Hc  (empirical approximation)
1254    pub fn minimum_film_thickness(&self, force: f64) -> f64 {
1255        0.75 * self.central_film_thickness(force)
1256    }
1257    /// Specific film thickness (lambda ratio) Λ = h_min / σ_composite.
1258    ///
1259    /// `composite_roughness` σ = √(σ₁² + σ₂²) \[m\].
1260    pub fn lambda_ratio(&self, composite_roughness: f64, force: f64) -> f64 {
1261        let sigma = composite_roughness.max(1e-30);
1262        self.minimum_film_thickness(force) / sigma
1263    }
1264}
1265/// DMT adhesive contact model for stiff, hard materials.
1266#[derive(Debug, Clone, Copy)]
1267pub struct DerjaguinMullerToporov {
1268    /// Effective modulus E* \[Pa\].
1269    pub effective_modulus: f64,
1270    /// Effective radius R* \[m\].
1271    pub effective_radius: f64,
1272    /// Work of adhesion w \[J/m²\].
1273    pub work_of_adhesion: f64,
1274}
1275impl DerjaguinMullerToporov {
1276    /// Create a DMT model.
1277    pub fn new(effective_modulus: f64, effective_radius: f64, work_of_adhesion: f64) -> Self {
1278        Self {
1279            effective_modulus,
1280            effective_radius,
1281            work_of_adhesion,
1282        }
1283    }
1284    /// Pull-off force in DMT model: F_pull = -2 * π * w * R*.
1285    pub fn pull_off_force(&self) -> f64 {
1286        -2.0 * PI * self.work_of_adhesion * self.effective_radius
1287    }
1288    /// Contact radius: same as Hertz with effective load F + |F_pull|.
1289    pub fn contact_radius(&self, force: f64) -> f64 {
1290        let f_pull = self.pull_off_force().abs();
1291        let f_eff = force + f_pull;
1292        if f_eff <= 0.0 {
1293            return 0.0;
1294        }
1295        let h = HertzContact {
1296            effective_modulus: self.effective_modulus,
1297            effective_radius: self.effective_radius,
1298        };
1299        h.compute(f_eff).contact_radius
1300    }
1301    /// Tabor parameter (determines JKR vs DMT regime).
1302    /// μ = (R* * w² / (E*² * z₀³))^(1/3), z₀ = atomic cutoff ≈ 0.3 nm.
1303    pub fn tabor_parameter(&self, z0: f64) -> f64 {
1304        let num = self.effective_radius * self.work_of_adhesion * self.work_of_adhesion;
1305        let den = self.effective_modulus * self.effective_modulus * z0 * z0 * z0;
1306        (num / den).powf(1.0 / 3.0)
1307    }
1308}
1309/// Archard's wear law model.
1310///
1311/// Volume worn Q = k_archard · F · s / H, where F is normal force \[N\],
1312/// s is sliding distance \[m\], and H is hardness \[Pa\].
1313#[derive(Debug, Clone, Copy)]
1314pub struct WearModel {
1315    /// Dimensionless Archard wear coefficient k \[–\].
1316    pub k_archard: f64,
1317    /// Hardness of the softer material \[Pa\].
1318    pub hardness: f64,
1319}
1320impl WearModel {
1321    /// Create a `WearModel`.
1322    pub fn new(k_archard: f64, hardness: f64) -> Self {
1323        Self {
1324            k_archard,
1325            hardness,
1326        }
1327    }
1328    /// Wear volume \[m³\] = k · F · s / H.
1329    pub fn wear_volume(&self, normal_force: f64, sliding_distance: f64) -> f64 {
1330        self.k_archard * normal_force * sliding_distance / self.hardness
1331    }
1332    /// Wear rate \[m³/s\] = k · F · v / H.
1333    pub fn wear_rate(&self, normal_force: f64, sliding_speed: f64) -> f64 {
1334        self.k_archard * normal_force * sliding_speed / self.hardness
1335    }
1336}
1337/// Holm's wear model (electrical contact inspired, generalised to tribology).
1338/// Volume per unit distance = Z * W (Holm equation).
1339#[derive(Debug, Clone, Copy)]
1340pub struct HolmWear {
1341    /// Holm wear coefficient Z \[m²/N\] (= K/H in Archard terms).
1342    pub z: f64,
1343}
1344impl HolmWear {
1345    /// Create a Holm wear model with given Z coefficient.
1346    pub fn new(z: f64) -> Self {
1347        HolmWear { z }
1348    }
1349    /// Wear volume rate \[m³/m\] = Z * W.
1350    pub fn volume_rate(&self, normal_load: f64) -> f64 {
1351        self.z * normal_load
1352    }
1353    /// Volume loss \[m³\] over sliding distance s \[m\].
1354    pub fn volume_loss(&self, normal_load: f64, s: f64) -> f64 {
1355        self.z * normal_load * s
1356    }
1357    /// Wear depth \[m\] over area A \[m²\] and distance s \[m\].
1358    pub fn wear_depth(&self, normal_load: f64, s: f64, area: f64) -> f64 {
1359        if area < 1e-30 {
1360            return 0.0;
1361        }
1362        self.volume_loss(normal_load, s) / area
1363    }
1364}
1365/// 2D Hertz contact for two parallel cylinders (line contact).
1366#[derive(Debug, Clone, Copy)]
1367pub struct CylinderContact {
1368    /// Effective modulus E* \[Pa\].
1369    pub effective_modulus: f64,
1370    /// Effective radius R* \[m\].
1371    pub effective_radius: f64,
1372    /// Contact half-width a \[m\] per unit length.
1373    pub contact_half_width: f64,
1374}
1375impl CylinderContact {
1376    /// Create a cylinder-on-cylinder contact (per unit length).
1377    pub fn new(e1: f64, nu1: f64, r1: f64, e2: f64, nu2: f64, r2: f64) -> Self {
1378        let e_star = 1.0 / ((1.0 - nu1 * nu1) / e1 + (1.0 - nu2 * nu2) / e2);
1379        let r_star = r1 * r2 / (r1 + r2);
1380        CylinderContact {
1381            effective_modulus: e_star,
1382            effective_radius: r_star,
1383            contact_half_width: 0.0,
1384        }
1385    }
1386    /// Contact half-width a \[m\] per unit length under load w \[N/m\].
1387    pub fn half_width(&self, w: f64) -> f64 {
1388        (4.0 * w * self.effective_radius / (PI * self.effective_modulus)).sqrt()
1389    }
1390    /// Maximum pressure p0 \[Pa\] per unit length.
1391    pub fn max_pressure(&self, w: f64) -> f64 {
1392        let a = self.half_width(w);
1393        if a < 1e-30 {
1394            return 0.0;
1395        }
1396        2.0 * w / (PI * a)
1397    }
1398    /// Subsurface shear stress τmax \[Pa\] at depth z = 0.786a below surface.
1399    pub fn max_subsurface_shear(&self, w: f64) -> f64 {
1400        0.300 * self.max_pressure(w)
1401    }
1402    /// Contact pressure distribution at position x from centre \[Pa\].
1403    pub fn pressure_at(&self, w: f64, x: f64) -> f64 {
1404        let a = self.half_width(w);
1405        let p0 = self.max_pressure(w);
1406        if x.abs() >= a {
1407            return 0.0;
1408        }
1409        p0 * (1.0 - (x / a).powi(2)).sqrt()
1410    }
1411    /// Depth of maximum shear stress z_max \[m\].
1412    pub fn depth_max_shear(&self, w: f64) -> f64 {
1413        0.786 * self.half_width(w)
1414    }
1415}
1416/// State of a tribological system at a given operating point.
1417#[derive(Debug, Clone)]
1418pub struct TribologicalState {
1419    /// Sliding speed v \[m/s\].
1420    pub velocity: f64,
1421    /// Normal load W \[N\].
1422    pub load: f64,
1423    /// Contact temperature T \[K\].
1424    pub temperature: f64,
1425    /// Film thickness h \[m\].
1426    pub film_thickness: f64,
1427    /// Lambda ratio (film parameter) Λ.
1428    pub lambda: f64,
1429    /// Current friction coefficient μ.
1430    pub mu: f64,
1431    /// Accumulated wear volume Vtotal \[m³\].
1432    pub wear_volume: f64,
1433    /// Lubrication regime.
1434    pub regime: LubricationRegimeKind,
1435}
1436impl TribologicalState {
1437    /// Create an initial tribological state.
1438    pub fn new(velocity: f64, load: f64) -> Self {
1439        TribologicalState {
1440            velocity,
1441            load,
1442            temperature: 293.15,
1443            film_thickness: 0.0,
1444            lambda: 0.0,
1445            mu: 0.15,
1446            wear_volume: 0.0,
1447            regime: LubricationRegimeKind::Boundary,
1448        }
1449    }
1450    /// Update state with new film thickness and roughness.
1451    pub fn update_film(&mut self, h: f64, roughness: f64) {
1452        self.film_thickness = h;
1453        self.lambda = if roughness > 1e-30 {
1454            h / roughness
1455        } else {
1456            f64::INFINITY
1457        };
1458        self.regime = if self.lambda < 1.0 {
1459            LubricationRegimeKind::Boundary
1460        } else if self.lambda < 3.0 {
1461            LubricationRegimeKind::Mixed
1462        } else if self.lambda < 10.0 {
1463            LubricationRegimeKind::Elastohydrodynamic
1464        } else {
1465            LubricationRegimeKind::FullFilm
1466        };
1467    }
1468    /// Accumulate wear volume over time step dt \[s\] using Archard model.
1469    pub fn accumulate_wear(&mut self, k: f64, hardness: f64, dt: f64) {
1470        let rate = k * self.load * self.velocity / hardness;
1471        self.wear_volume += rate * dt;
1472    }
1473    /// Update friction coefficient based on current lambda.
1474    pub fn update_friction(&mut self, mu_boundary: f64, mu_full: f64) {
1475        let t = ((self.lambda - 1.0) / 9.0).clamp(0.0, 1.0);
1476        self.mu = mu_boundary * (1.0 - t) + mu_full * t;
1477    }
1478    /// Power dissipation \[W\].
1479    pub fn power_dissipation(&self) -> f64 {
1480        self.mu * self.load * self.velocity
1481    }
1482}
1483/// Stick-slip friction oscillator (spring-mass with Coulomb friction on a
1484/// moving belt).
1485///
1486/// The mass is connected to a fixed spring (`stiffness`) and rests on a belt
1487/// moving at speed `v_drive`.  Friction alternates between static
1488/// (`mu_s`) and kinetic (`mu_k`) regimes.
1489#[derive(Debug, Clone)]
1490pub struct FrictionOscillator {
1491    /// Mass \[kg\].
1492    pub mass: f64,
1493    /// Spring stiffness \[N/m\].
1494    pub stiffness: f64,
1495    /// Static friction coefficient.
1496    pub mu_s: f64,
1497    /// Kinetic friction coefficient.
1498    pub mu_k: f64,
1499}
1500impl FrictionOscillator {
1501    /// Create a `FrictionOscillator`.
1502    pub fn new(mass: f64, stiffness: f64, mu_s: f64, mu_k: f64) -> Self {
1503        Self {
1504            mass,
1505            stiffness,
1506            mu_s,
1507            mu_k,
1508        }
1509    }
1510    /// Returns `true` when the mass is sticking to the belt.
1511    ///
1512    /// The mass sticks when its velocity equals the belt drive speed.
1513    pub fn is_sticking(&self, v: f64, v_drive: f64) -> bool {
1514        (v - v_drive).abs() < 1e-9
1515    }
1516    /// Advance one time step using semi-implicit Euler.
1517    ///
1518    /// `x` is the current displacement \[m\], `v` is the current velocity
1519    /// \[m/s\], `v_drive` is the belt speed \[m/s\], `dt` is the time step \[s\].
1520    ///
1521    /// Returns `(new_x, new_v)`.
1522    pub fn step(&mut self, x: f64, v: f64, v_drive: f64, dt: f64) -> (f64, f64) {
1523        let g = 9.81;
1524        let f_normal = self.mass * g;
1525        let f_spring = -self.stiffness * x;
1526        let relative_v = v - v_drive;
1527        let f_friction = if relative_v.abs() < 1e-9 {
1528            let f_static_max = self.mu_s * f_normal;
1529            (-f_spring).clamp(-f_static_max, f_static_max)
1530        } else {
1531            -relative_v.signum() * self.mu_k * f_normal
1532        };
1533        let a = (f_spring + f_friction) / self.mass;
1534        let new_v = v + a * dt;
1535        let new_x = x + new_v * dt;
1536        (new_x, new_v)
1537    }
1538}
1539/// JKR adhesive contact model for soft, compliant materials.
1540#[derive(Debug, Clone, Copy)]
1541pub struct JohnsonKendallRoberts {
1542    /// Effective elastic modulus E* \[Pa\].
1543    pub effective_modulus: f64,
1544    /// Effective radius R* \[m\].
1545    pub effective_radius: f64,
1546    /// Work of adhesion w \[J/m²\].
1547    pub work_of_adhesion: f64,
1548}
1549impl JohnsonKendallRoberts {
1550    /// Create a JKR model.
1551    pub fn new(effective_modulus: f64, effective_radius: f64, work_of_adhesion: f64) -> Self {
1552        Self {
1553            effective_modulus,
1554            effective_radius,
1555            work_of_adhesion,
1556        }
1557    }
1558    /// Pull-off force: F_pull = -1.5 * π * w * R*.
1559    pub fn pull_off_force(&self) -> f64 {
1560        -1.5 * PI * self.work_of_adhesion * self.effective_radius
1561    }
1562    /// Contact radius under JKR theory for applied load F.
1563    pub fn contact_radius(&self, force: f64) -> f64 {
1564        let w = self.work_of_adhesion;
1565        let r = self.effective_radius;
1566        let e = self.effective_modulus;
1567        let term1 = 3.0 * PI * w * r;
1568        let discriminant = 6.0 * PI * w * r * force + term1 * term1;
1569        if discriminant < 0.0 {
1570            return 0.0;
1571        }
1572        let inner = force + term1 + discriminant.sqrt();
1573        let a3 = r / e * inner;
1574        a3.powf(1.0 / 3.0)
1575    }
1576    /// Load for a given contact radius (inverse JKR).
1577    pub fn load_for_radius(&self, a: f64) -> f64 {
1578        let e = self.effective_modulus;
1579        let r = self.effective_radius;
1580        let w = self.work_of_adhesion;
1581        let hertz_term = 4.0 * e * a * a * a / (3.0 * r);
1582        let adhesion_term = (8.0 * PI * e * w * a * a * a).sqrt();
1583        hertz_term - adhesion_term
1584    }
1585}
1586/// Elastohydrodynamic lubrication (EHL) film thickness using Dowson-Higginson formula.
1587#[derive(Debug, Clone, Copy)]
1588pub struct EhlFilm {
1589    /// Ambient viscosity η₀ \[Pa·s\].
1590    pub viscosity: f64,
1591    /// Pressure-viscosity coefficient α \[1/Pa\].
1592    pub pressure_viscosity_coeff: f64,
1593    /// Effective elastic modulus E' \[Pa\].
1594    pub effective_modulus: f64,
1595    /// Effective radius R' \[m\].
1596    pub effective_radius: f64,
1597}
1598impl EhlFilm {
1599    /// Create an EHL film model.
1600    pub fn new(
1601        viscosity: f64,
1602        pressure_viscosity_coeff: f64,
1603        effective_modulus: f64,
1604        effective_radius: f64,
1605    ) -> Self {
1606        Self {
1607            viscosity,
1608            pressure_viscosity_coeff,
1609            effective_modulus,
1610            effective_radius,
1611        }
1612    }
1613    /// Compute central film thickness h_c using Dowson-Higginson (1977).
1614    /// `u` = entrainment velocity \[m/s\], `w` = load per unit length \[N/m\] for line contact.
1615    pub fn central_film_thickness_line(&self, u: f64, w: f64) -> f64 {
1616        let ue = (self.viscosity * u) / (self.effective_modulus * self.effective_radius);
1617        let we = w / (self.effective_modulus * self.effective_radius);
1618        let ge = self.pressure_viscosity_coeff * self.effective_modulus;
1619        if we < 1e-30 {
1620            return 0.0;
1621        }
1622        let h_c_r = 2.65 * ge.powf(0.54) * ue.powf(0.7) / we.powf(0.13);
1623        h_c_r * self.effective_radius
1624    }
1625    /// Compute minimum film thickness h_min for point contact (Hamrock-Dowson).
1626    /// `u` = entrainment velocity \[m/s\], `w` = normal load \[N\].
1627    pub fn minimum_film_thickness_point(&self, u: f64, w: f64) -> f64 {
1628        let ue = (self.viscosity * u) / (self.effective_modulus * self.effective_radius);
1629        let we = w / (self.effective_modulus * self.effective_radius * self.effective_radius);
1630        let ge = self.pressure_viscosity_coeff * self.effective_modulus;
1631        if we < 1e-30 {
1632            return 0.0;
1633        }
1634        let h_min_r = 3.63 * ue.powf(0.68) * ge.powf(0.49) * we.powf(-0.073);
1635        h_min_r * self.effective_radius
1636    }
1637    /// Lambda ratio (film parameter): Λ = h_min / σ.
1638    pub fn lambda_ratio(&self, u: f64, w: f64, roughness: f64) -> f64 {
1639        let h_min = self.minimum_film_thickness_point(u, w);
1640        if roughness < 1e-30 {
1641            f64::INFINITY
1642        } else {
1643            h_min / roughness
1644        }
1645    }
1646}
1647/// Hertz contact mechanics parameterised by the individual sphere/surface data.
1648///
1649/// Stores the raw material and geometry parameters (radii, moduli, Poisson
1650/// ratios) and derives the reduced quantities on demand.
1651#[derive(Debug, Clone, Copy)]
1652pub struct HertzContactParams {
1653    /// Radius of body 1 \[m\] (use `f64::INFINITY` for a flat).
1654    pub r1: f64,
1655    /// Radius of body 2 \[m\] (use `f64::INFINITY` for a flat).
1656    pub r2: f64,
1657    /// Young's modulus of body 1 \[Pa\].
1658    pub e1: f64,
1659    /// Poisson's ratio of body 1 \[–\].
1660    pub nu1: f64,
1661    /// Young's modulus of body 2 \[Pa\].
1662    pub e2: f64,
1663    /// Poisson's ratio of body 2 \[–\].
1664    pub nu2: f64,
1665}
1666impl HertzContactParams {
1667    /// Create a `HertzContactParams` from individual surface parameters.
1668    pub fn new(r1: f64, r2: f64, e1: f64, nu1: f64, e2: f64, nu2: f64) -> Self {
1669        Self {
1670            r1,
1671            r2,
1672            e1,
1673            nu1,
1674            e2,
1675            nu2,
1676        }
1677    }
1678    /// Reduced (plane-strain) modulus E* = 1/\[(1−ν₁²)/E₁ + (1−ν₂²)/E₂\].
1679    pub fn reduced_modulus(&self) -> f64 {
1680        1.0 / ((1.0 - self.nu1 * self.nu1) / self.e1 + (1.0 - self.nu2 * self.nu2) / self.e2)
1681    }
1682    /// Reduced radius R* = R₁·R₂ / (R₁ + R₂).
1683    ///
1684    /// When one body is flat (`r = ∞`) this correctly returns the other radius.
1685    pub fn reduced_radius(&self) -> f64 {
1686        if self.r1.is_infinite() {
1687            return self.r2;
1688        }
1689        if self.r2.is_infinite() {
1690            return self.r1;
1691        }
1692        self.r1 * self.r2 / (self.r1 + self.r2)
1693    }
1694    /// Contact radius a = (3·F·R* / (4·E*))^(1/3) \[m\].
1695    pub fn contact_radius(&self, force: f64) -> f64 {
1696        let e_star = self.reduced_modulus();
1697        let r_star = self.reduced_radius();
1698        ((3.0 * force * r_star) / (4.0 * e_star)).powf(1.0 / 3.0)
1699    }
1700    /// Maximum Hertz pressure p₀ = 3F / (2π·a²) \[Pa\].
1701    pub fn max_pressure(&self, force: f64) -> f64 {
1702        let a = self.contact_radius(force);
1703        if a < 1e-30 {
1704            return 0.0;
1705        }
1706        3.0 * force / (2.0 * PI * a * a)
1707    }
1708    /// Contact stiffness k = dF/dδ = 2·E*·a \[N/m\].
1709    pub fn contact_stiffness(&self, force: f64) -> f64 {
1710        let e_star = self.reduced_modulus();
1711        let a = self.contact_radius(force);
1712        2.0 * e_star * a
1713    }
1714    /// Maximum sub-surface shear stress τ_max ≈ 0.31·p₀ at depth z ≈ 0.48·a.
1715    pub fn max_shear_stress(&self, force: f64) -> f64 {
1716        0.31 * self.max_pressure(force)
1717    }
1718}
1719/// Stribeck curve model for lubrication regime determination.
1720#[derive(Debug, Clone)]
1721pub struct LubricationRegime {
1722    /// Boundary friction coefficient μ_b.
1723    pub mu_boundary: f64,
1724    /// Full-film friction coefficient μ_f.
1725    pub mu_fullfilm: f64,
1726    /// Stribeck parameter: characteristic viscosity-speed-load number.
1727    pub stribeck_constant: f64,
1728}
1729impl LubricationRegime {
1730    /// Create a Stribeck model.
1731    pub fn new(mu_boundary: f64, mu_fullfilm: f64, stribeck_constant: f64) -> Self {
1732        Self {
1733            mu_boundary,
1734            mu_fullfilm,
1735            stribeck_constant,
1736        }
1737    }
1738    /// Classify regime from lambda ratio.
1739    pub fn classify(&self, lambda: f64) -> LubricationRegimeKind {
1740        if lambda < 1.0 {
1741            LubricationRegimeKind::Boundary
1742        } else if lambda < 3.0 {
1743            LubricationRegimeKind::Mixed
1744        } else if lambda < 10.0 {
1745            LubricationRegimeKind::Elastohydrodynamic
1746        } else {
1747            LubricationRegimeKind::FullFilm
1748        }
1749    }
1750    /// Compute effective friction coefficient from Stribeck number S = η*v/P.
1751    /// Uses exponential transition model.
1752    pub fn friction_coefficient(&self, eta: f64, velocity: f64, pressure: f64) -> f64 {
1753        if pressure < 1e-10 {
1754            return self.mu_boundary;
1755        }
1756        let stribeck_num = eta * velocity / pressure;
1757        self.mu_fullfilm
1758            + (self.mu_boundary - self.mu_fullfilm)
1759                * (-(stribeck_num / self.stribeck_constant)).exp()
1760    }
1761}