Skip to main content

oxiphysics_materials/
ceramic_materials.rs

1// Copyright 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Ceramic material models: property database, fracture mechanics, thermal shock,
5//! creep, sintering densification, hardness conversion, Weibull failure statistics,
6//! piezoelectric ceramics (PZT), ferroelectric hysteresis, zirconia phase
7//! transformation toughening, alumina properties, thermal conductivity models,
8//! Hasselman parameters, and grain growth kinetics.
9//!
10//! All functions use SI units unless otherwise stated.
11
12#![allow(dead_code)]
13#![allow(clippy::too_many_arguments)]
14
15use std::f64::consts::PI;
16
17/// Universal gas constant (J/(mol K)).
18const R_GAS: f64 = 8.314;
19
20// ---------------------------------------------------------------------------
21// CeramicMaterial
22// ---------------------------------------------------------------------------
23
24/// Structural and thermal properties of a ceramic material.
25#[derive(Debug, Clone)]
26pub struct CeramicMaterial {
27    /// Young's modulus (Pa).
28    pub youngs_modulus: f64,
29    /// Poisson's ratio (dimensionless).
30    pub poissons_ratio: f64,
31    /// Mode-I fracture toughness KIC (Pa sqrt(m)).
32    pub fracture_toughness_kic: f64,
33    /// Vickers hardness (HV).
34    pub hardness_vickers: f64,
35    /// Thermal conductivity (W/(m K)).
36    pub thermal_conductivity: f64,
37    /// Coefficient of thermal expansion (1/K).
38    pub thermal_expansion: f64,
39    /// Melting / decomposition point (K).
40    pub melting_point: f64,
41    /// Average grain size (um).
42    pub grain_size_um: f64,
43}
44
45// ---------------------------------------------------------------------------
46// CeramicType
47// ---------------------------------------------------------------------------
48
49/// Enumeration of common engineering ceramics supported by the preset database.
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum CeramicType {
52    /// Aluminium oxide (Al2O3) -- general-purpose structural ceramic.
53    Alumina,
54    /// Yttria-stabilised zirconia (3Y-TZP) -- high-toughness, biomedical use.
55    ZirconiaYSZ,
56    /// Silicon carbide (SiC) -- extreme hardness and thermal stability.
57    SiliconCarbide,
58    /// Silicon nitride (Si3N4) -- excellent fatigue and wear resistance.
59    SiliconNitride,
60    /// Boron carbide (B4C) -- one of the hardest known materials; armour ceramic.
61    BoronCarbide,
62    /// Hydroxyapatite (Ca10(PO4)6(OH)2) -- bone-like biocompatible ceramic.
63    HydroxyApatite,
64}
65
66// ---------------------------------------------------------------------------
67// Preset database
68// ---------------------------------------------------------------------------
69
70/// Return a [`CeramicMaterial`] with representative published properties for
71/// the requested [`CeramicType`].
72///
73/// Sources: Engineered Materials Handbook Vol. 4 (ASM); CRC Handbook; Munz & Fett.
74pub fn ceramic_preset(ct: CeramicType) -> CeramicMaterial {
75    match ct {
76        CeramicType::Alumina => CeramicMaterial {
77            youngs_modulus: 380.0e9,
78            poissons_ratio: 0.22,
79            fracture_toughness_kic: 3.5e6,
80            hardness_vickers: 1600.0,
81            thermal_conductivity: 25.0,
82            thermal_expansion: 8.1e-6,
83            melting_point: 2323.0,
84            grain_size_um: 3.0,
85        },
86        CeramicType::ZirconiaYSZ => CeramicMaterial {
87            youngs_modulus: 210.0e9,
88            poissons_ratio: 0.31,
89            fracture_toughness_kic: 8.0e6,
90            hardness_vickers: 1200.0,
91            thermal_conductivity: 2.2,
92            thermal_expansion: 10.5e-6,
93            melting_point: 2988.0,
94            grain_size_um: 0.5,
95        },
96        CeramicType::SiliconCarbide => CeramicMaterial {
97            youngs_modulus: 410.0e9,
98            poissons_ratio: 0.14,
99            fracture_toughness_kic: 3.0e6,
100            hardness_vickers: 2500.0,
101            thermal_conductivity: 120.0,
102            thermal_expansion: 4.5e-6,
103            melting_point: 3003.0,
104            grain_size_um: 5.0,
105        },
106        CeramicType::SiliconNitride => CeramicMaterial {
107            youngs_modulus: 310.0e9,
108            poissons_ratio: 0.27,
109            fracture_toughness_kic: 7.0e6,
110            hardness_vickers: 1600.0,
111            thermal_conductivity: 30.0,
112            thermal_expansion: 3.2e-6,
113            melting_point: 2173.0,
114            grain_size_um: 1.0,
115        },
116        CeramicType::BoronCarbide => CeramicMaterial {
117            youngs_modulus: 460.0e9,
118            poissons_ratio: 0.17,
119            fracture_toughness_kic: 2.5e6,
120            hardness_vickers: 3000.0,
121            thermal_conductivity: 35.0,
122            thermal_expansion: 5.0e-6,
123            melting_point: 2618.0,
124            grain_size_um: 10.0,
125        },
126        CeramicType::HydroxyApatite => CeramicMaterial {
127            youngs_modulus: 80.0e9,
128            poissons_ratio: 0.27,
129            fracture_toughness_kic: 0.9e6,
130            hardness_vickers: 500.0,
131            thermal_conductivity: 1.3,
132            thermal_expansion: 14.0e-6,
133            melting_point: 1943.0,
134            grain_size_um: 2.0,
135        },
136    }
137}
138
139// ---------------------------------------------------------------------------
140// Hall-Petch strength
141// ---------------------------------------------------------------------------
142
143/// Hall-Petch grain-boundary strengthening: sigma = sigma_0 + k_hp / sqrt(d).
144///
145/// # Arguments
146/// * `k_hp` -- Hall-Petch coefficient (Pa m^0.5).
147/// * `sigma_0` -- friction stress / lattice resistance (Pa).
148/// * `grain_size` -- grain size d (m).
149///
150/// Returns yield/fracture stress (Pa).
151pub fn hall_petch_strength(k_hp: f64, sigma_0: f64, grain_size: f64) -> f64 {
152    sigma_0 + k_hp / grain_size.sqrt()
153}
154
155// ---------------------------------------------------------------------------
156// Griffith critical flaw size
157// ---------------------------------------------------------------------------
158
159/// Griffith / Irwin criterion -- critical flaw radius *a* that drives unstable fracture.
160///
161/// `a_c = (1/pi) * (K_IC / (Y * sigma))^2`
162///
163/// # Arguments
164/// * `kic` -- fracture toughness (Pa sqrt(m)).
165/// * `stress` -- applied tensile stress (Pa).
166/// * `geometry_factor` -- dimensionless stress-intensity shape factor Y.
167///
168/// Returns the critical half-crack length (m).
169pub fn critical_flaw_size(kic: f64, stress: f64, geometry_factor: f64) -> f64 {
170    let ratio = kic / (geometry_factor * stress);
171    ratio * ratio / PI
172}
173
174// ---------------------------------------------------------------------------
175// Thermal shock resistance
176// ---------------------------------------------------------------------------
177
178/// Kingery first thermal-shock resistance parameter R.
179///
180/// `R = sigma_f * (1 - nu) / (E * alpha)`
181///
182/// A higher R indicates better thermal-shock resistance.
183///
184/// # Arguments
185/// * `ceramic` -- material properties.
186/// * `delta_t` -- temperature difference (K).
187///
188/// Returns the R parameter (K).
189pub fn thermal_shock_resistance(ceramic: &CeramicMaterial, delta_t: f64) -> f64 {
190    let grain_m = ceramic.grain_size_um * 1e-6 / 2.0;
191    let sigma_f = ceramic.fracture_toughness_kic / (PI * grain_m).sqrt();
192    let r = sigma_f * (1.0 - ceramic.poissons_ratio)
193        / (ceramic.youngs_modulus * ceramic.thermal_expansion);
194    r * delta_t
195}
196
197// ---------------------------------------------------------------------------
198// Hasselman thermal shock parameters
199// ---------------------------------------------------------------------------
200
201/// Hasselman thermal shock parameters for ceramic design.
202///
203/// Contains the four Hasselman figures of merit: R, R', R''', R''''.
204#[derive(Debug, Clone)]
205pub struct HasselmanParameters {
206    /// First parameter R (K) -- resistance to fracture initiation.
207    pub r_first: f64,
208    /// Second parameter R' (W/m) -- R scaled by thermal conductivity.
209    pub r_second: f64,
210    /// Third parameter R''' (m^-1) -- resistance to crack propagation.
211    pub r_third: f64,
212    /// Fourth parameter R'''' (m Pa) -- damage resistance.
213    pub r_fourth: f64,
214}
215
216/// Compute all four Hasselman thermal shock resistance parameters.
217///
218/// # Arguments
219/// * `strength` -- tensile/flexural strength sigma_f (Pa).
220/// * `youngs_modulus` -- E (Pa).
221/// * `poissons_ratio` -- nu (dimensionless).
222/// * `cte` -- coefficient of thermal expansion alpha (1/K).
223/// * `thermal_conductivity` -- k (W/(m K)).
224/// * `fracture_toughness` -- K_IC (Pa sqrt(m)).
225/// * `surface_energy` -- gamma_f (J/m^2).
226///
227/// Returns [`HasselmanParameters`].
228pub fn hasselman_parameters(
229    strength: f64,
230    youngs_modulus: f64,
231    poissons_ratio: f64,
232    cte: f64,
233    thermal_conductivity: f64,
234    fracture_toughness: f64,
235    surface_energy: f64,
236) -> HasselmanParameters {
237    // R = sigma_f (1 - nu) / (E alpha)
238    let r_first = strength * (1.0 - poissons_ratio) / (youngs_modulus * cte);
239    // R' = k * R
240    let r_second = thermal_conductivity * r_first;
241    // R''' = E / (K_IC^2 (1 - nu^2))  -- resistance to crack propagation
242    let r_third = youngs_modulus
243        / (fracture_toughness * fracture_toughness * (1.0 - poissons_ratio * poissons_ratio));
244    // R'''' = E * gamma_f / (sigma_f^2 (1 - nu^2))  -- damage resistance
245    let r_fourth = youngs_modulus * surface_energy
246        / (strength * strength * (1.0 - poissons_ratio * poissons_ratio));
247    HasselmanParameters {
248        r_first,
249        r_second,
250        r_third,
251        r_fourth,
252    }
253}
254
255// ---------------------------------------------------------------------------
256// Nabarro-Herring creep
257// ---------------------------------------------------------------------------
258
259/// Nabarro-Herring diffusional creep rate for fine-grained ceramics.
260///
261/// # Arguments
262/// * `temp` -- absolute temperature (K).
263/// * `stress` -- applied stress (Pa).
264/// * `grain_size` -- grain diameter (m).
265/// * `activation_energy` -- activation energy for grain-boundary diffusion (J/mol).
266///
267/// Returns creep strain rate (1/s, scaled).
268pub fn creep_rate_ceramic(temp: f64, stress: f64, grain_size: f64, activation_energy: f64) -> f64 {
269    let exponent = -activation_energy / (R_GAS * temp);
270    let a = 1e-20_f64;
271    a * stress * exponent.exp() / (grain_size * grain_size)
272}
273
274// ---------------------------------------------------------------------------
275// Sintering densification
276// ---------------------------------------------------------------------------
277
278/// Simplified sintering densification kinetics (Coble-type).
279///
280/// # Arguments
281/// * `temp` -- sintering temperature (K).
282/// * `time` -- sintering time (s).
283/// * `initial_density` -- green density as fraction of theoretical (0-1).
284///
285/// Returns instantaneous relative density (fraction, clamped to 1.0).
286pub fn sintering_densification(temp: f64, time: f64, initial_density: f64) -> f64 {
287    const Q: f64 = 300_000.0;
288    let tau = (-Q / (R_GAS * temp)).exp();
289    let rho_inf = 1.0_f64;
290    let rho = rho_inf - (rho_inf - initial_density) * (-time * tau).exp();
291    rho.min(1.0)
292}
293
294// ---------------------------------------------------------------------------
295// Grain growth kinetics
296// ---------------------------------------------------------------------------
297
298/// Normal grain growth model: d^n - d0^n = K * t * exp(-Q/(R*T)).
299///
300/// # Arguments
301/// * `d0` -- initial grain size (m).
302/// * `exponent_n` -- grain growth exponent (typically 2-4).
303/// * `prefactor_k` -- pre-exponential growth constant (m^n / s).
304/// * `activation_energy` -- activation energy Q (J/mol).
305/// * `temp` -- absolute temperature (K).
306/// * `time` -- annealing time (s).
307///
308/// Returns final grain size (m).
309pub fn grain_growth_size(
310    d0: f64,
311    exponent_n: f64,
312    prefactor_k: f64,
313    activation_energy: f64,
314    temp: f64,
315    time: f64,
316) -> f64 {
317    let d0n = d0.powf(exponent_n);
318    let kt = prefactor_k * time * (-activation_energy / (R_GAS * temp)).exp();
319    (d0n + kt).powf(1.0 / exponent_n)
320}
321
322/// Grain growth rate dd/dt at current grain size d and temperature.
323///
324/// # Arguments
325/// * `d` -- current grain size (m).
326/// * `exponent_n` -- grain growth exponent.
327/// * `prefactor_k` -- pre-exponential growth constant (m^n / s).
328/// * `activation_energy` -- activation energy Q (J/mol).
329/// * `temp` -- absolute temperature (K).
330///
331/// Returns growth rate (m/s).
332pub fn grain_growth_rate(
333    d: f64,
334    exponent_n: f64,
335    prefactor_k: f64,
336    activation_energy: f64,
337    temp: f64,
338) -> f64 {
339    let k_eff = prefactor_k * (-activation_energy / (R_GAS * temp)).exp();
340    k_eff / (exponent_n * d.powf(exponent_n - 1.0))
341}
342
343// ---------------------------------------------------------------------------
344// Vickers hardness conversion
345// ---------------------------------------------------------------------------
346
347/// Convert Vickers hardness number to equivalent pressure in MPa.
348///
349/// `HV [MPa] = HV * 9.807`
350pub fn vickers_hardness_to_mpa(hv: f64) -> f64 {
351    hv * 9.807
352}
353
354// ---------------------------------------------------------------------------
355// Weibull fracture probability
356// ---------------------------------------------------------------------------
357
358/// Two-parameter Weibull failure probability under uniaxial tension.
359///
360/// `P_f = 1 - exp(-(sigma / sigma_0)^m)`
361///
362/// # Arguments
363/// * `stress` -- applied stress (Pa).
364/// * `modulus_of_rupture` -- Weibull scale parameter sigma_0 (Pa).
365/// * `weibull_m` -- Weibull shape / modulus m (dimensionless).
366///
367/// Returns failure probability (0-1).
368pub fn ceramic_fracture_probability(stress: f64, modulus_of_rupture: f64, weibull_m: f64) -> f64 {
369    let ratio = stress / modulus_of_rupture;
370    1.0 - (-(ratio.powf(weibull_m))).exp()
371}
372
373/// Weibull strength at a specified failure probability.
374///
375/// `sigma = sigma_0 * (-ln(1 - P_f))^(1/m)`
376///
377/// # Arguments
378/// * `prob_failure` -- target failure probability (0 < P_f < 1).
379/// * `modulus_of_rupture` -- scale parameter sigma_0 (Pa).
380/// * `weibull_m` -- shape parameter m.
381///
382/// Returns stress (Pa).
383pub fn weibull_strength_at_probability(
384    prob_failure: f64,
385    modulus_of_rupture: f64,
386    weibull_m: f64,
387) -> f64 {
388    let ln_term = -(1.0 - prob_failure).ln();
389    modulus_of_rupture * ln_term.powf(1.0 / weibull_m)
390}
391
392/// Weibull effective volume scaling: sigma_2 = sigma_1 * (V1/V2)^(1/m).
393///
394/// Scales strength from volume V1 to volume V2 via the weakest-link model.
395///
396/// # Arguments
397/// * `sigma_1` -- strength at reference volume V1 (Pa).
398/// * `v1` -- reference volume (m^3).
399/// * `v2` -- target volume (m^3).
400/// * `weibull_m` -- Weibull modulus m.
401///
402/// Returns scaled strength (Pa).
403pub fn weibull_volume_scaling(sigma_1: f64, v1: f64, v2: f64, weibull_m: f64) -> f64 {
404    sigma_1 * (v1 / v2).powf(1.0 / weibull_m)
405}
406
407// ---------------------------------------------------------------------------
408// Thermal conductivity models
409// ---------------------------------------------------------------------------
410
411/// Debye thermal conductivity model for crystalline ceramics.
412///
413/// `k = (1/3) * C_v * v_s * l_mfp`
414///
415/// # Arguments
416/// * `heat_capacity_vol` -- volumetric heat capacity C_v (J/(m^3 K)).
417/// * `sound_velocity` -- average phonon velocity v_s (m/s).
418/// * `mean_free_path` -- phonon mean free path l (m).
419///
420/// Returns thermal conductivity (W/(m K)).
421pub fn debye_thermal_conductivity(
422    heat_capacity_vol: f64,
423    sound_velocity: f64,
424    mean_free_path: f64,
425) -> f64 {
426    heat_capacity_vol * sound_velocity * mean_free_path / 3.0
427}
428
429/// Klemens point-defect phonon scattering model.
430///
431/// Gives the reciprocal mean free path due to point defects:
432/// `1/l_pd = A * omega^4 / v_s`
433///
434/// where A is a scattering parameter and omega is phonon frequency.
435///
436/// # Arguments
437/// * `scatter_param` -- point-defect scattering parameter A (s^3/m).
438/// * `omega` -- phonon angular frequency (rad/s).
439/// * `sound_velocity` -- v_s (m/s).
440///
441/// Returns reciprocal mean free path (1/m).
442pub fn klemens_point_defect_scattering(scatter_param: f64, omega: f64, sound_velocity: f64) -> f64 {
443    scatter_param * omega.powi(4) / sound_velocity
444}
445
446/// Effective medium thermal conductivity for porous ceramics (Maxwell-Eucken).
447///
448/// `k_eff = k_solid * (1 - porosity) / (1 + porosity / 2)`
449///
450/// # Arguments
451/// * `k_solid` -- thermal conductivity of dense ceramic (W/(m K)).
452/// * `porosity` -- volume fraction of pores (0-1).
453///
454/// Returns effective thermal conductivity (W/(m K)).
455pub fn porous_thermal_conductivity(k_solid: f64, porosity: f64) -> f64 {
456    k_solid * (1.0 - porosity) / (1.0 + porosity / 2.0)
457}
458
459/// Temperature-dependent thermal conductivity via 1/T Umklapp model.
460///
461/// `k(T) = k_ref * (T_ref / T)`
462///
463/// Valid above the Debye temperature where Umklapp scattering dominates.
464///
465/// # Arguments
466/// * `k_ref` -- conductivity at reference temperature (W/(m K)).
467/// * `t_ref` -- reference temperature (K).
468/// * `t` -- target temperature (K).
469///
470/// Returns thermal conductivity at temperature T (W/(m K)).
471pub fn umklapp_conductivity(k_ref: f64, t_ref: f64, t: f64) -> f64 {
472    k_ref * t_ref / t
473}
474
475// ---------------------------------------------------------------------------
476// Piezoelectric ceramics (PZT)
477// ---------------------------------------------------------------------------
478
479/// Properties of a piezoelectric ceramic (e.g. PZT).
480#[derive(Debug, Clone)]
481pub struct PiezoelectricCeramic {
482    /// Piezoelectric charge coefficient d33 (C/N or m/V).
483    pub d33: f64,
484    /// Piezoelectric charge coefficient d31 (C/N).
485    pub d31: f64,
486    /// Piezoelectric voltage coefficient g33 (V m/N).
487    pub g33: f64,
488    /// Relative permittivity (dielectric constant) epsilon_r.
489    pub epsilon_r: f64,
490    /// Young's modulus (Pa).
491    pub youngs_modulus: f64,
492    /// Electromechanical coupling factor k33 (dimensionless).
493    pub k33: f64,
494    /// Curie temperature (K).
495    pub curie_temperature: f64,
496    /// Mechanical quality factor Q_m (dimensionless).
497    pub quality_factor: f64,
498}
499
500/// Vacuum permittivity (F/m).
501const EPS_0: f64 = 8.854_187_817e-12;
502
503/// PZT-4 (Navy Type I) hard PZT preset.
504pub fn pzt4_preset() -> PiezoelectricCeramic {
505    PiezoelectricCeramic {
506        d33: 289.0e-12,
507        d31: -123.0e-12,
508        g33: 26.1e-3,
509        epsilon_r: 1300.0,
510        youngs_modulus: 66.0e9,
511        k33: 0.70,
512        curie_temperature: 601.0,
513        quality_factor: 500.0,
514    }
515}
516
517/// PZT-5A (Navy Type II) soft PZT preset.
518pub fn pzt5a_preset() -> PiezoelectricCeramic {
519    PiezoelectricCeramic {
520        d33: 374.0e-12,
521        d31: -171.0e-12,
522        g33: 24.8e-3,
523        epsilon_r: 1700.0,
524        youngs_modulus: 52.0e9,
525        k33: 0.72,
526        curie_temperature: 638.0,
527        quality_factor: 75.0,
528    }
529}
530
531/// PZT-5H (Navy Type VI) soft PZT preset.
532pub fn pzt5h_preset() -> PiezoelectricCeramic {
533    PiezoelectricCeramic {
534        d33: 593.0e-12,
535        d31: -274.0e-12,
536        g33: 19.7e-3,
537        epsilon_r: 3400.0,
538        youngs_modulus: 50.0e9,
539        k33: 0.75,
540        curie_temperature: 466.0,
541        quality_factor: 65.0,
542    }
543}
544
545/// Compute piezoelectric strain from applied electric field.
546///
547/// `S_33 = d33 * E_3`
548///
549/// # Arguments
550/// * `pzt` -- piezoelectric ceramic properties.
551/// * `e_field` -- applied electric field E_3 (V/m).
552///
553/// Returns strain (dimensionless).
554pub fn piezo_strain_33(pzt: &PiezoelectricCeramic, e_field: f64) -> f64 {
555    pzt.d33 * e_field
556}
557
558/// Compute piezoelectric transverse strain from applied electric field.
559///
560/// `S_11 = d31 * E_3`
561///
562/// # Arguments
563/// * `pzt` -- piezoelectric ceramic properties.
564/// * `e_field` -- applied electric field E_3 (V/m).
565///
566/// Returns transverse strain (dimensionless, typically negative for d31 < 0).
567pub fn piezo_strain_31(pzt: &PiezoelectricCeramic, e_field: f64) -> f64 {
568    pzt.d31 * e_field
569}
570
571/// Compute open-circuit voltage from applied stress on a piezoelectric element.
572///
573/// `V = g33 * sigma * t`
574///
575/// # Arguments
576/// * `pzt` -- piezoelectric ceramic properties.
577/// * `stress` -- applied mechanical stress (Pa).
578/// * `thickness` -- element thickness (m).
579///
580/// Returns voltage (V).
581pub fn piezo_open_circuit_voltage(pzt: &PiezoelectricCeramic, stress: f64, thickness: f64) -> f64 {
582    pzt.g33 * stress * thickness
583}
584
585/// Compute charge generated by a piezoelectric element under stress.
586///
587/// `Q = d33 * F = d33 * sigma * A`
588///
589/// # Arguments
590/// * `pzt` -- piezoelectric ceramic properties.
591/// * `stress` -- applied stress (Pa).
592/// * `area` -- electrode area (m^2).
593///
594/// Returns charge (C).
595pub fn piezo_charge_generated(pzt: &PiezoelectricCeramic, stress: f64, area: f64) -> f64 {
596    pzt.d33 * stress * area
597}
598
599/// Compute stored electrical energy density in a piezoelectric element.
600///
601/// `u_e = 0.5 * epsilon_0 * epsilon_r * E^2`
602///
603/// # Arguments
604/// * `pzt` -- piezoelectric ceramic properties.
605/// * `e_field` -- electric field (V/m).
606///
607/// Returns energy density (J/m^3).
608pub fn piezo_energy_density(pzt: &PiezoelectricCeramic, e_field: f64) -> f64 {
609    0.5 * EPS_0 * pzt.epsilon_r * e_field * e_field
610}
611
612/// Compute resonant frequency of a piezoelectric disc in thickness mode.
613///
614/// `f_r = v_s / (2 * t)`
615///
616/// where `v_s = sqrt(E / rho)` and we approximate density from the quality factor
617/// relationship. For a simpler model we accept sound velocity directly.
618///
619/// # Arguments
620/// * `sound_velocity` -- longitudinal sound velocity in the ceramic (m/s).
621/// * `thickness` -- element thickness (m).
622///
623/// Returns resonant frequency (Hz).
624pub fn piezo_resonant_frequency(sound_velocity: f64, thickness: f64) -> f64 {
625    sound_velocity / (2.0 * thickness)
626}
627
628// ---------------------------------------------------------------------------
629// Ferroelectric hysteresis
630// ---------------------------------------------------------------------------
631
632/// Ferroelectric hysteresis state for P-E loop modelling.
633///
634/// Uses a simplified hyperbolic tangent saturation model:
635/// `P = P_s * tanh((E - E_c * sign) / E_a)`
636///
637/// where P_s is saturation polarisation, E_c is the coercive field,
638/// and E_a is a shape parameter controlling the loop width.
639#[derive(Debug, Clone)]
640pub struct FerroelectricHysteresis {
641    /// Saturation polarisation P_s (C/m^2).
642    pub saturation_polarisation: f64,
643    /// Coercive field E_c (V/m).
644    pub coercive_field: f64,
645    /// Shape parameter E_a (V/m), controls steepness of the transition.
646    pub shape_param: f64,
647    /// Remanent polarisation P_r (C/m^2).
648    pub remanent_polarisation: f64,
649}
650
651impl FerroelectricHysteresis {
652    /// Create a new ferroelectric hysteresis model.
653    pub fn new(
654        saturation_polarisation: f64,
655        coercive_field: f64,
656        shape_param: f64,
657        remanent_polarisation: f64,
658    ) -> Self {
659        Self {
660            saturation_polarisation,
661            coercive_field,
662            shape_param,
663            remanent_polarisation,
664        }
665    }
666
667    /// BaTiO3 typical values.
668    pub fn barium_titanate() -> Self {
669        Self::new(0.26, 1.0e5, 5.0e4, 0.22)
670    }
671
672    /// PZT (soft) typical values.
673    pub fn pzt_soft() -> Self {
674        Self::new(0.35, 1.4e5, 6.0e4, 0.30)
675    }
676
677    /// Compute polarisation on the upper branch (increasing E).
678    ///
679    /// `P = P_s * tanh((E + E_c) / E_a)`
680    ///
681    /// # Arguments
682    /// * `e_field` -- applied electric field (V/m).
683    ///
684    /// Returns polarisation (C/m^2).
685    pub fn polarisation_upper(&self, e_field: f64) -> f64 {
686        self.saturation_polarisation * ((e_field + self.coercive_field) / self.shape_param).tanh()
687    }
688
689    /// Compute polarisation on the lower branch (decreasing E).
690    ///
691    /// `P = P_s * tanh((E - E_c) / E_a)`
692    ///
693    /// # Arguments
694    /// * `e_field` -- applied electric field (V/m).
695    ///
696    /// Returns polarisation (C/m^2).
697    pub fn polarisation_lower(&self, e_field: f64) -> f64 {
698        self.saturation_polarisation * ((e_field - self.coercive_field) / self.shape_param).tanh()
699    }
700
701    /// Estimate hysteresis loss per cycle from the coercive field and remanent
702    /// polarisation (simplified rectangular approximation).
703    ///
704    /// `W = 4 * E_c * P_r`
705    ///
706    /// Returns energy density per cycle (J/m^3).
707    pub fn hysteresis_loss_per_cycle(&self) -> f64 {
708        4.0 * self.coercive_field * self.remanent_polarisation
709    }
710
711    /// Curie-Weiss permittivity above the Curie temperature.
712    ///
713    /// `epsilon_r = C / (T - T_c)`
714    ///
715    /// # Arguments
716    /// * `curie_constant` -- Curie constant C (K).
717    /// * `curie_temp` -- Curie temperature T_c (K).
718    /// * `temp` -- current temperature (K).
719    ///
720    /// Returns relative permittivity (dimensionless), or `f64::INFINITY` if T <= T_c.
721    pub fn curie_weiss_permittivity(curie_constant: f64, curie_temp: f64, temp: f64) -> f64 {
722        if temp <= curie_temp {
723            f64::INFINITY
724        } else {
725            curie_constant / (temp - curie_temp)
726        }
727    }
728}
729
730// ---------------------------------------------------------------------------
731// Zirconia phase transformation toughening
732// ---------------------------------------------------------------------------
733
734/// Zirconia tetragonal-to-monoclinic transformation toughening model.
735///
736/// Based on the Budiansky-Hutchinson-Lambropoulos framework.
737#[derive(Debug, Clone)]
738pub struct ZirconiaToughening {
739    /// Dilatational transformation strain epsilon_T (dimensionless, ~0.04).
740    pub transformation_strain: f64,
741    /// Volume fraction of transformable tetragonal phase f_v (0-1).
742    pub volume_fraction: f64,
743    /// Shear modulus G (Pa).
744    pub shear_modulus: f64,
745    /// Base intrinsic fracture toughness K_IC0 (Pa sqrt(m)).
746    pub base_toughness: f64,
747    /// Transformation zone half-height h (m).
748    pub zone_height: f64,
749}
750
751impl ZirconiaToughening {
752    /// Create a new zirconia toughening model.
753    pub fn new(
754        transformation_strain: f64,
755        volume_fraction: f64,
756        shear_modulus: f64,
757        base_toughness: f64,
758        zone_height: f64,
759    ) -> Self {
760        Self {
761            transformation_strain,
762            volume_fraction,
763            shear_modulus,
764            base_toughness,
765            zone_height,
766        }
767    }
768
769    /// 3Y-TZP (3 mol% yttria-stabilised tetragonal zirconia polycrystal) preset.
770    pub fn y_tzp_3mol() -> Self {
771        Self::new(0.04, 0.30, 80.0e9, 5.0e6, 50.0e-6)
772    }
773
774    /// Toughening increment delta_K_IC (Pa sqrt(m)).
775    ///
776    /// `delta_K = 0.22 * E_eff * epsilon_T * f_v * sqrt(h)`
777    ///
778    /// where `E_eff = 2 * G`.
779    pub fn toughening_increment(&self) -> f64 {
780        let e_eff = 2.0 * self.shear_modulus;
781        0.22 * e_eff * self.transformation_strain * self.volume_fraction * self.zone_height.sqrt()
782    }
783
784    /// Effective fracture toughness K_IC_eff (Pa sqrt(m)).
785    pub fn effective_toughness(&self) -> f64 {
786        self.base_toughness + self.toughening_increment()
787    }
788
789    /// Transformation stress sigma_t = G * epsilon_T / (1 - f_v) (Pa).
790    pub fn transformation_stress(&self) -> f64 {
791        self.shear_modulus * self.transformation_strain / (1.0 - self.volume_fraction)
792    }
793
794    /// Estimated transformation zone size from Irwin-type analysis (m).
795    ///
796    /// `h_est = (K_IC_eff / sigma_t)^2 / (3 pi)`
797    pub fn zone_size_estimate(&self) -> f64 {
798        let sigma_t = self.transformation_stress();
799        let kic = self.effective_toughness();
800        (kic / sigma_t).powi(2) / (3.0 * PI)
801    }
802
803    /// Critical grain size above which spontaneous t->m transformation occurs.
804    ///
805    /// Approximation: `d_c = 6 * gamma / (delta_G_chem + E * epsilon_T^2 / (1-nu))`
806    ///
807    /// # Arguments
808    /// * `surface_energy` -- interface energy gamma (J/m^2).
809    /// * `chem_driving_force` -- chemical free energy difference delta_G (J/m^3).
810    /// * `youngs_modulus` -- E (Pa).
811    /// * `poissons_ratio` -- nu.
812    ///
813    /// Returns critical grain size (m).
814    pub fn critical_grain_size(
815        &self,
816        surface_energy: f64,
817        chem_driving_force: f64,
818        youngs_modulus: f64,
819        poissons_ratio: f64,
820    ) -> f64 {
821        let strain_energy =
822            youngs_modulus * self.transformation_strain.powi(2) / (1.0 - poissons_ratio);
823        6.0 * surface_energy / (chem_driving_force + strain_energy)
824    }
825}
826
827// ---------------------------------------------------------------------------
828// Alumina property models
829// ---------------------------------------------------------------------------
830
831/// Temperature-dependent Young's modulus of alumina.
832///
833/// Empirical fit: `E(T) = E_0 * (1 - b * (T - T_ref))`
834///
835/// where b ~ 5e-5 1/K for alumina.
836///
837/// # Arguments
838/// * `e0` -- room temperature modulus (Pa).
839/// * `temp` -- current temperature (K).
840/// * `t_ref` -- reference temperature (K), typically 300.
841/// * `b_coeff` -- temperature coefficient (1/K), ~5e-5 for alumina.
842///
843/// Returns Young's modulus at temperature T (Pa).
844pub fn alumina_modulus_vs_temp(e0: f64, temp: f64, t_ref: f64, b_coeff: f64) -> f64 {
845    (e0 * (1.0 - b_coeff * (temp - t_ref))).max(0.0)
846}
847
848/// Alumina flexural strength size effect (Weibull volume scaling).
849///
850/// `sigma(V) = sigma_ref * (V_ref / V)^(1/m)`
851///
852/// # Arguments
853/// * `sigma_ref` -- reference strength (Pa).
854/// * `v_ref` -- reference test volume (m^3).
855/// * `v_target` -- target component volume (m^3).
856/// * `weibull_m` -- Weibull modulus.
857///
858/// Returns scaled strength (Pa).
859pub fn alumina_size_effect(sigma_ref: f64, v_ref: f64, v_target: f64, weibull_m: f64) -> f64 {
860    weibull_volume_scaling(sigma_ref, v_ref, v_target, weibull_m)
861}
862
863/// Alumina thermal conductivity vs temperature (empirical inverse-T fit).
864///
865/// `k(T) = A / T + B`
866///
867/// # Arguments
868/// * `a_coeff` -- fitting constant A (W/m).
869/// * `b_coeff` -- fitting constant B (W/(m K)).
870/// * `temp` -- temperature (K).
871///
872/// Returns thermal conductivity (W/(m K)).
873pub fn alumina_thermal_conductivity_vs_temp(a_coeff: f64, b_coeff: f64, temp: f64) -> f64 {
874    a_coeff / temp + b_coeff
875}
876
877// ---------------------------------------------------------------------------
878// Indentation fracture toughness (Anstis method)
879// ---------------------------------------------------------------------------
880
881/// Indentation fracture toughness from Vickers indentation (Anstis method).
882///
883/// `K_IC = 0.016 * (E/H)^0.5 * P / c^1.5`
884///
885/// # Arguments
886/// * `youngs_modulus` -- E (Pa).
887/// * `hardness` -- Vickers hardness H (Pa).
888/// * `load` -- indentation load P (N).
889/// * `crack_length` -- radial crack half-length c (m).
890///
891/// Returns fracture toughness K_IC (Pa sqrt(m)).
892pub fn indentation_fracture_toughness(
893    youngs_modulus: f64,
894    hardness: f64,
895    load: f64,
896    crack_length: f64,
897) -> f64 {
898    0.016 * (youngs_modulus / hardness).sqrt() * load / crack_length.powf(1.5)
899}
900
901// ---------------------------------------------------------------------------
902// Ceramic fatigue (subcritical crack growth)
903// ---------------------------------------------------------------------------
904
905/// Subcritical crack growth rate (Paris-type law for ceramics).
906///
907/// `da/dN = A * (K_I / K_IC)^n`
908///
909/// where n is typically 10-50 for ceramics (much higher than metals).
910///
911/// # Arguments
912/// * `k_i` -- stress intensity factor (Pa sqrt(m)).
913/// * `k_ic` -- fracture toughness (Pa sqrt(m)).
914/// * `paris_a` -- Paris law coefficient A (m/cycle).
915/// * `paris_n` -- Paris law exponent n.
916///
917/// Returns crack growth rate (m/cycle).
918pub fn subcritical_crack_growth_rate(k_i: f64, k_ic: f64, paris_a: f64, paris_n: f64) -> f64 {
919    paris_a * (k_i / k_ic).powf(paris_n)
920}
921
922/// Estimate cycles to failure from initial crack size to critical crack size.
923///
924/// Uses numerical integration of the Paris law.
925///
926/// # Arguments
927/// * `a0` -- initial crack half-length (m).
928/// * `stress` -- applied stress (Pa).
929/// * `geometry_factor` -- Y factor.
930/// * `k_ic` -- fracture toughness (Pa sqrt(m)).
931/// * `paris_a` -- Paris coefficient (m/cycle).
932/// * `paris_n` -- Paris exponent.
933/// * `steps` -- number of integration steps.
934///
935/// Returns estimated number of cycles to failure.
936pub fn cycles_to_failure(
937    a0: f64,
938    stress: f64,
939    geometry_factor: f64,
940    k_ic: f64,
941    paris_a: f64,
942    paris_n: f64,
943    steps: usize,
944) -> f64 {
945    let a_c = critical_flaw_size(k_ic, stress, geometry_factor);
946    if a0 >= a_c {
947        return 0.0;
948    }
949    let da = (a_c - a0) / steps as f64;
950    let mut cycles = 0.0;
951    let mut a = a0;
952    for _ in 0..steps {
953        let k_i = geometry_factor * stress * (PI * a).sqrt();
954        let rate = subcritical_crack_growth_rate(k_i, k_ic, paris_a, paris_n);
955        if rate < 1e-30 {
956            cycles += da / 1e-30;
957        } else {
958            cycles += da / rate;
959        }
960        a += da;
961    }
962    cycles
963}
964
965// ---------------------------------------------------------------------------
966// Ceramic R-curve behaviour
967// ---------------------------------------------------------------------------
968
969/// Compute rising R-curve toughness as function of crack extension.
970///
971/// `K_R(da) = K_0 + (K_ss - K_0) * (1 - exp(-da / lambda))`
972///
973/// where K_0 is the initiation toughness, K_ss the steady-state toughness,
974/// and lambda is a characteristic bridging length.
975///
976/// # Arguments
977/// * `da` -- crack extension (m).
978/// * `k_init` -- initiation toughness K_0 (Pa sqrt(m)).
979/// * `k_steady` -- steady-state toughness K_ss (Pa sqrt(m)).
980/// * `lambda` -- characteristic bridging length (m).
981///
982/// Returns toughness at crack extension da (Pa sqrt(m)).
983pub fn r_curve_toughness(da: f64, k_init: f64, k_steady: f64, lambda: f64) -> f64 {
984    k_init + (k_steady - k_init) * (1.0 - (-da / lambda).exp())
985}
986
987// ---------------------------------------------------------------------------
988// Tests
989// ---------------------------------------------------------------------------
990
991#[cfg(test)]
992mod tests {
993    use super::*;
994
995    const EPS: f64 = 1e-10;
996
997    // --- ceramic_preset ---
998
999    #[test]
1000    fn test_preset_alumina_modulus() {
1001        let m = ceramic_preset(CeramicType::Alumina);
1002        assert!((m.youngs_modulus - 380.0e9).abs() < 1.0, "Alumina E");
1003    }
1004
1005    #[test]
1006    fn test_preset_zirconia_toughness() {
1007        let m = ceramic_preset(CeramicType::ZirconiaYSZ);
1008        assert!(m.fracture_toughness_kic > 5.0e6, "YSZ KIC > 5 MPa sqrt(m)");
1009    }
1010
1011    #[test]
1012    fn test_preset_sic_hardness() {
1013        let m = ceramic_preset(CeramicType::SiliconCarbide);
1014        assert!(m.hardness_vickers > 2000.0, "SiC HV > 2000");
1015    }
1016
1017    #[test]
1018    fn test_preset_si3n4_conductivity() {
1019        let m = ceramic_preset(CeramicType::SiliconNitride);
1020        assert!(m.thermal_conductivity > 20.0, "Si3N4 k > 20 W/mK");
1021    }
1022
1023    #[test]
1024    fn test_preset_b4c_melting_point() {
1025        let m = ceramic_preset(CeramicType::BoronCarbide);
1026        assert!(m.melting_point > 2500.0, "B4C melting > 2500 K");
1027    }
1028
1029    #[test]
1030    fn test_preset_ha_modulus_low() {
1031        let m = ceramic_preset(CeramicType::HydroxyApatite);
1032        assert!(m.youngs_modulus < 120.0e9, "HA E < 120 GPa");
1033    }
1034
1035    #[test]
1036    fn test_preset_all_positive_fields() {
1037        for ct in [
1038            CeramicType::Alumina,
1039            CeramicType::ZirconiaYSZ,
1040            CeramicType::SiliconCarbide,
1041            CeramicType::SiliconNitride,
1042            CeramicType::BoronCarbide,
1043            CeramicType::HydroxyApatite,
1044        ] {
1045            let m = ceramic_preset(ct);
1046            assert!(m.youngs_modulus > 0.0);
1047            assert!(m.fracture_toughness_kic > 0.0);
1048            assert!(m.hardness_vickers > 0.0);
1049            assert!(m.thermal_conductivity > 0.0);
1050            assert!(m.grain_size_um > 0.0);
1051        }
1052    }
1053
1054    // --- hall_petch_strength ---
1055
1056    #[test]
1057    fn test_hall_petch_basic() {
1058        let s = hall_petch_strength(0.5e6, 100.0e6, 1e-6);
1059        assert!((s - (100.0e6 + 0.5e6 / 1e-3)).abs() < 1.0, "HP value");
1060    }
1061
1062    #[test]
1063    fn test_hall_petch_larger_grain_lower_strength() {
1064        let s_fine = hall_petch_strength(0.5e6, 100.0e6, 1e-6);
1065        let s_coarse = hall_petch_strength(0.5e6, 100.0e6, 100e-6);
1066        assert!(s_fine > s_coarse, "finer grain -> higher strength");
1067    }
1068
1069    #[test]
1070    fn test_hall_petch_zero_k() {
1071        let s = hall_petch_strength(0.0, 200.0e6, 5e-6);
1072        assert!((s - 200.0e6).abs() < EPS, "k=0 -> sigma_0");
1073    }
1074
1075    // --- critical_flaw_size ---
1076
1077    #[test]
1078    fn test_critical_flaw_size_positive() {
1079        let a = critical_flaw_size(3.5e6, 200.0e6, 1.0);
1080        assert!(a > 0.0, "flaw size must be positive");
1081    }
1082
1083    #[test]
1084    fn test_critical_flaw_size_decreases_with_stress() {
1085        let a1 = critical_flaw_size(3.5e6, 100.0e6, 1.0);
1086        let a2 = critical_flaw_size(3.5e6, 200.0e6, 1.0);
1087        assert!(a1 > a2, "higher stress -> smaller critical flaw");
1088    }
1089
1090    #[test]
1091    fn test_critical_flaw_size_formula() {
1092        let kic = 3.5e6_f64;
1093        let sig = 200.0e6_f64;
1094        let y = 1.12_f64;
1095        let expected = (kic / (y * sig)).powi(2) / PI;
1096        let got = critical_flaw_size(kic, sig, y);
1097        assert!((got - expected).abs() < 1e-20, "formula");
1098    }
1099
1100    // --- thermal_shock_resistance ---
1101
1102    #[test]
1103    fn test_tsr_positive() {
1104        let m = ceramic_preset(CeramicType::Alumina);
1105        let r = thermal_shock_resistance(&m, 200.0);
1106        assert!(r > 0.0, "TSR must be positive");
1107    }
1108
1109    #[test]
1110    fn test_tsr_scales_with_delta_t() {
1111        let m = ceramic_preset(CeramicType::SiliconCarbide);
1112        let r1 = thermal_shock_resistance(&m, 100.0);
1113        let r2 = thermal_shock_resistance(&m, 200.0);
1114        assert!((r2 - 2.0 * r1).abs() < EPS * 1e6, "TSR linear in delta_T");
1115    }
1116
1117    // --- hasselman_parameters ---
1118
1119    #[test]
1120    fn test_hasselman_r_first_positive() {
1121        let h = hasselman_parameters(400.0e6, 380.0e9, 0.22, 8.1e-6, 25.0, 3.5e6, 1.0);
1122        assert!(h.r_first > 0.0, "R must be positive");
1123    }
1124
1125    #[test]
1126    fn test_hasselman_r_second_equals_k_times_r() {
1127        let k = 25.0;
1128        let h = hasselman_parameters(400.0e6, 380.0e9, 0.22, 8.1e-6, k, 3.5e6, 1.0);
1129        assert!((h.r_second - k * h.r_first).abs() < 1e-6, "R' = k * R");
1130    }
1131
1132    #[test]
1133    fn test_hasselman_r_fourth_positive() {
1134        let h = hasselman_parameters(400.0e6, 380.0e9, 0.22, 8.1e-6, 25.0, 3.5e6, 1.0);
1135        assert!(h.r_fourth > 0.0, "R'''' must be positive");
1136    }
1137
1138    // --- creep_rate_ceramic ---
1139
1140    #[test]
1141    fn test_creep_rate_positive() {
1142        let rate = creep_rate_ceramic(1500.0, 100.0e6, 5e-6, 400_000.0);
1143        assert!(rate > 0.0, "creep rate must be positive");
1144    }
1145
1146    #[test]
1147    fn test_creep_rate_higher_temp_faster() {
1148        let r1 = creep_rate_ceramic(1200.0, 100.0e6, 5e-6, 300_000.0);
1149        let r2 = creep_rate_ceramic(1600.0, 100.0e6, 5e-6, 300_000.0);
1150        assert!(r2 > r1, "higher T -> faster creep");
1151    }
1152
1153    // --- sintering_densification ---
1154
1155    #[test]
1156    fn test_sintering_density_increases_with_time() {
1157        let rho0 = 0.60;
1158        let d1 = sintering_densification(1600.0, 100.0, rho0);
1159        let d2 = sintering_densification(1600.0, 3600.0, rho0);
1160        assert!(d2 >= d1, "density should increase with time");
1161    }
1162
1163    #[test]
1164    fn test_sintering_density_bounded() {
1165        let d = sintering_densification(1800.0, 1e9, 0.50);
1166        assert!(d <= 1.0, "density cannot exceed 1.0");
1167        assert!(d >= 0.50, "density cannot decrease from initial");
1168    }
1169
1170    #[test]
1171    fn test_sintering_zero_time() {
1172        let rho0 = 0.65;
1173        let d = sintering_densification(1600.0, 0.0, rho0);
1174        assert!((d - rho0).abs() < 1e-12, "no sintering at t=0");
1175    }
1176
1177    // --- grain_growth ---
1178
1179    #[test]
1180    fn test_grain_growth_increases_with_time() {
1181        let d1 = grain_growth_size(1e-6, 2.0, 1e-14, 300e3, 1500.0, 100.0);
1182        let d2 = grain_growth_size(1e-6, 2.0, 1e-14, 300e3, 1500.0, 10000.0);
1183        assert!(d2 > d1, "grain size should increase with time");
1184    }
1185
1186    #[test]
1187    fn test_grain_growth_zero_time_equals_initial() {
1188        let d0 = 1.5e-6;
1189        let d = grain_growth_size(d0, 2.0, 1e-14, 300e3, 1500.0, 0.0);
1190        assert!((d - d0).abs() < 1e-15, "d(t=0) = d0");
1191    }
1192
1193    #[test]
1194    fn test_grain_growth_rate_positive() {
1195        let rate = grain_growth_rate(2e-6, 2.0, 1e-14, 300e3, 1500.0);
1196        assert!(rate > 0.0, "growth rate must be positive");
1197    }
1198
1199    // --- vickers_hardness_to_mpa ---
1200
1201    #[test]
1202    fn test_hv_conversion_known_value() {
1203        let mpa = vickers_hardness_to_mpa(1000.0);
1204        assert!((mpa - 9807.0).abs() < 0.01, "1000 HV = 9807 MPa");
1205    }
1206
1207    // --- ceramic_fracture_probability ---
1208
1209    #[test]
1210    fn test_weibull_at_scale_param() {
1211        let p = ceramic_fracture_probability(100.0e6, 100.0e6, 10.0);
1212        let expected = 1.0 - (-1.0_f64).exp();
1213        assert!((p - expected).abs() < 1e-12, "P_f at scale param");
1214    }
1215
1216    #[test]
1217    fn test_weibull_zero_stress() {
1218        let p = ceramic_fracture_probability(0.0, 100.0e6, 10.0);
1219        assert!(p.abs() < EPS, "zero stress -> zero probability");
1220    }
1221
1222    #[test]
1223    fn test_weibull_monotone_increasing() {
1224        let p1 = ceramic_fracture_probability(80.0e6, 100.0e6, 10.0);
1225        let p2 = ceramic_fracture_probability(120.0e6, 100.0e6, 10.0);
1226        assert!(p2 > p1, "P_f increases with stress");
1227    }
1228
1229    // --- weibull_strength_at_probability ---
1230
1231    #[test]
1232    fn test_weibull_strength_at_50_percent() {
1233        let sigma_0 = 400.0e6;
1234        let m = 10.0;
1235        let sigma = weibull_strength_at_probability(0.5, sigma_0, m);
1236        // Check round-trip
1237        let pf = ceramic_fracture_probability(sigma, sigma_0, m);
1238        assert!((pf - 0.5).abs() < 1e-10, "round-trip P_f = 0.5");
1239    }
1240
1241    // --- weibull_volume_scaling ---
1242
1243    #[test]
1244    fn test_volume_scaling_larger_weaker() {
1245        let sigma = weibull_volume_scaling(400.0e6, 1e-6, 1e-3, 10.0);
1246        assert!(sigma < 400.0e6, "larger volume -> lower strength");
1247    }
1248
1249    #[test]
1250    fn test_volume_scaling_same_volume() {
1251        let sigma = weibull_volume_scaling(400.0e6, 1e-6, 1e-6, 10.0);
1252        assert!(
1253            (sigma - 400.0e6).abs() < EPS,
1254            "same volume -> same strength"
1255        );
1256    }
1257
1258    // --- thermal_conductivity models ---
1259
1260    #[test]
1261    fn test_debye_conductivity_positive() {
1262        let k = debye_thermal_conductivity(2.5e6, 5000.0, 10e-9);
1263        assert!(k > 0.0, "conductivity must be positive");
1264    }
1265
1266    #[test]
1267    fn test_debye_conductivity_proportional_to_mfp() {
1268        let k1 = debye_thermal_conductivity(2.5e6, 5000.0, 10e-9);
1269        let k2 = debye_thermal_conductivity(2.5e6, 5000.0, 20e-9);
1270        assert!((k2 / k1 - 2.0).abs() < 1e-10, "linear in mfp");
1271    }
1272
1273    #[test]
1274    fn test_porous_conductivity_decreases_with_porosity() {
1275        let k0 = porous_thermal_conductivity(25.0, 0.0);
1276        let k1 = porous_thermal_conductivity(25.0, 0.3);
1277        assert!(k1 < k0, "porosity reduces conductivity");
1278    }
1279
1280    #[test]
1281    fn test_porous_conductivity_zero_porosity_equals_solid() {
1282        let k = porous_thermal_conductivity(25.0, 0.0);
1283        assert!((k - 25.0).abs() < 1e-10, "zero porosity -> solid value");
1284    }
1285
1286    #[test]
1287    fn test_umklapp_conductivity_inverse_t() {
1288        let k1 = umklapp_conductivity(25.0, 300.0, 300.0);
1289        let k2 = umklapp_conductivity(25.0, 300.0, 600.0);
1290        assert!((k1 - 25.0).abs() < 1e-10, "at T_ref -> k_ref");
1291        assert!((k2 - 12.5).abs() < 1e-10, "at 2*T_ref -> k_ref/2");
1292    }
1293
1294    // --- piezoelectric ceramics ---
1295
1296    #[test]
1297    fn test_pzt4_d33_positive() {
1298        let pzt = pzt4_preset();
1299        assert!(pzt.d33 > 0.0, "d33 must be positive for PZT");
1300    }
1301
1302    #[test]
1303    fn test_pzt5h_higher_d33_than_pzt4() {
1304        let p4 = pzt4_preset();
1305        let p5h = pzt5h_preset();
1306        assert!(p5h.d33 > p4.d33, "PZT-5H is softer -> higher d33");
1307    }
1308
1309    #[test]
1310    fn test_piezo_strain_sign() {
1311        let pzt = pzt5a_preset();
1312        let s33 = piezo_strain_33(&pzt, 1e6);
1313        let s31 = piezo_strain_31(&pzt, 1e6);
1314        assert!(s33 > 0.0, "d33 > 0 -> positive axial strain");
1315        assert!(s31 < 0.0, "d31 < 0 -> negative transverse strain");
1316    }
1317
1318    #[test]
1319    fn test_piezo_voltage_proportional_to_stress() {
1320        let pzt = pzt4_preset();
1321        let v1 = piezo_open_circuit_voltage(&pzt, 1.0e6, 1e-3);
1322        let v2 = piezo_open_circuit_voltage(&pzt, 2.0e6, 1e-3);
1323        assert!((v2 / v1 - 2.0).abs() < 1e-10, "V linear in stress");
1324    }
1325
1326    #[test]
1327    fn test_piezo_charge_proportional_to_area() {
1328        let pzt = pzt4_preset();
1329        let q1 = piezo_charge_generated(&pzt, 1e6, 1e-4);
1330        let q2 = piezo_charge_generated(&pzt, 1e6, 2e-4);
1331        assert!((q2 / q1 - 2.0).abs() < 1e-10, "charge linear in area");
1332    }
1333
1334    #[test]
1335    fn test_piezo_energy_density_positive() {
1336        let pzt = pzt5a_preset();
1337        let u = piezo_energy_density(&pzt, 1e6);
1338        assert!(u > 0.0, "energy density must be positive");
1339    }
1340
1341    #[test]
1342    fn test_piezo_resonant_frequency() {
1343        let f = piezo_resonant_frequency(4000.0, 1e-3);
1344        assert!((f - 2.0e6).abs() < 1.0, "f_r = v/(2t) = 4000/0.002 = 2 MHz");
1345    }
1346
1347    // --- ferroelectric hysteresis ---
1348
1349    #[test]
1350    fn test_hysteresis_upper_at_zero_field() {
1351        let h = FerroelectricHysteresis::barium_titanate();
1352        let p = h.polarisation_upper(0.0);
1353        // At E=0, upper branch: tanh(E_c / E_a) > 0
1354        assert!(p > 0.0, "upper branch at E=0 should be positive (remanent)");
1355    }
1356
1357    #[test]
1358    fn test_hysteresis_lower_at_zero_field() {
1359        let h = FerroelectricHysteresis::barium_titanate();
1360        let p = h.polarisation_lower(0.0);
1361        // At E=0, lower branch: tanh(-E_c / E_a) < 0
1362        assert!(p < 0.0, "lower branch at E=0 should be negative");
1363    }
1364
1365    #[test]
1366    fn test_hysteresis_saturation() {
1367        let h = FerroelectricHysteresis::pzt_soft();
1368        let p_up = h.polarisation_upper(1e7);
1369        // At very high field, should approach P_s
1370        assert!(
1371            (p_up - h.saturation_polarisation).abs() < 0.01,
1372            "should saturate at high field"
1373        );
1374    }
1375
1376    #[test]
1377    fn test_hysteresis_loss_positive() {
1378        let h = FerroelectricHysteresis::barium_titanate();
1379        assert!(h.hysteresis_loss_per_cycle() > 0.0, "loss must be positive");
1380    }
1381
1382    #[test]
1383    fn test_curie_weiss_diverges_at_tc() {
1384        let eps = FerroelectricHysteresis::curie_weiss_permittivity(1.7e5, 393.0, 393.0);
1385        assert!(eps.is_infinite(), "should diverge at T_c");
1386    }
1387
1388    #[test]
1389    fn test_curie_weiss_decreases_above_tc() {
1390        let eps1 = FerroelectricHysteresis::curie_weiss_permittivity(1.7e5, 393.0, 400.0);
1391        let eps2 = FerroelectricHysteresis::curie_weiss_permittivity(1.7e5, 393.0, 500.0);
1392        assert!(
1393            eps1 > eps2,
1394            "permittivity decreases as T increases above T_c"
1395        );
1396    }
1397
1398    // --- zirconia toughening ---
1399
1400    #[test]
1401    fn test_zirconia_toughening_increment_positive() {
1402        let z = ZirconiaToughening::y_tzp_3mol();
1403        assert!(z.toughening_increment() > 0.0, "delta_K must be positive");
1404    }
1405
1406    #[test]
1407    fn test_zirconia_effective_gt_base() {
1408        let z = ZirconiaToughening::y_tzp_3mol();
1409        assert!(z.effective_toughness() > z.base_toughness, "K_eff > K_0");
1410    }
1411
1412    #[test]
1413    fn test_zirconia_toughening_increases_with_fv() {
1414        let z1 = ZirconiaToughening::new(0.04, 0.1, 80e9, 5e6, 50e-6);
1415        let z2 = ZirconiaToughening::new(0.04, 0.4, 80e9, 5e6, 50e-6);
1416        assert!(
1417            z2.toughening_increment() > z1.toughening_increment(),
1418            "more transformable phase -> more toughening"
1419        );
1420    }
1421
1422    #[test]
1423    fn test_zirconia_zone_size_positive() {
1424        let z = ZirconiaToughening::y_tzp_3mol();
1425        assert!(z.zone_size_estimate() > 0.0, "zone size must be positive");
1426    }
1427
1428    #[test]
1429    fn test_zirconia_transformation_stress_positive() {
1430        let z = ZirconiaToughening::y_tzp_3mol();
1431        assert!(z.transformation_stress() > 0.0, "sigma_t must be positive");
1432    }
1433
1434    #[test]
1435    fn test_zirconia_critical_grain_size_positive() {
1436        let z = ZirconiaToughening::y_tzp_3mol();
1437        let dc = z.critical_grain_size(1.0, 100e6, 210e9, 0.31);
1438        assert!(dc > 0.0, "critical grain size must be positive");
1439    }
1440
1441    // --- alumina models ---
1442
1443    #[test]
1444    fn test_alumina_modulus_decreases_with_temp() {
1445        let e_rt = alumina_modulus_vs_temp(380e9, 300.0, 300.0, 5e-5);
1446        let e_ht = alumina_modulus_vs_temp(380e9, 1000.0, 300.0, 5e-5);
1447        assert!(e_ht < e_rt, "modulus should decrease at high temperature");
1448    }
1449
1450    #[test]
1451    fn test_alumina_modulus_at_ref_temp() {
1452        let e = alumina_modulus_vs_temp(380e9, 300.0, 300.0, 5e-5);
1453        assert!((e - 380e9).abs() < 1.0, "E(T_ref) = E_0");
1454    }
1455
1456    // --- indentation fracture toughness ---
1457
1458    #[test]
1459    fn test_indentation_toughness_positive() {
1460        let k = indentation_fracture_toughness(380e9, 18e9, 10.0, 50e-6);
1461        assert!(k > 0.0, "K_IC must be positive");
1462    }
1463
1464    // --- subcritical crack growth ---
1465
1466    #[test]
1467    fn test_scg_rate_increases_with_ki() {
1468        let r1 = subcritical_crack_growth_rate(2e6, 4e6, 1e-15, 20.0);
1469        let r2 = subcritical_crack_growth_rate(3e6, 4e6, 1e-15, 20.0);
1470        assert!(r2 > r1, "higher K_I -> faster growth");
1471    }
1472
1473    #[test]
1474    fn test_cycles_to_failure_positive() {
1475        let n = cycles_to_failure(10e-6, 200e6, 1.12, 3.5e6, 1e-15, 20.0, 1000);
1476        assert!(n > 0.0, "cycles must be positive for subcritical crack");
1477    }
1478
1479    // --- R-curve ---
1480
1481    #[test]
1482    fn test_r_curve_at_zero_extension_equals_k0() {
1483        let k = r_curve_toughness(0.0, 3.0e6, 8.0e6, 100e-6);
1484        assert!((k - 3.0e6).abs() < 1e-6, "K_R(0) = K_0");
1485    }
1486
1487    #[test]
1488    fn test_r_curve_approaches_steady_state() {
1489        let k = r_curve_toughness(1.0, 3.0e6, 8.0e6, 100e-6);
1490        assert!((k - 8.0e6).abs() < 1.0, "K_R at large extension -> K_ss");
1491    }
1492
1493    #[test]
1494    fn test_r_curve_monotonically_increasing() {
1495        let k1 = r_curve_toughness(10e-6, 3.0e6, 8.0e6, 100e-6);
1496        let k2 = r_curve_toughness(50e-6, 3.0e6, 8.0e6, 100e-6);
1497        assert!(k2 > k1, "R-curve should be monotonically increasing");
1498    }
1499}