Skip to main content

oxiphysics_materials/
ceramics_materials.rs

1// Copyright 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3//! Ceramic material models: brittle fracture, sintering, thermal properties.
4//!
5//! This module provides physics-based models for ceramic materials including
6//! brittle fracture (Griffith criterion), Weibull statistics, thermal shock
7//! resistance, sintering kinetics, grain growth, high-temperature creep,
8//! dielectric/ferroelectric behaviour, and ZrO2 transformation toughening.
9
10#![allow(dead_code)]
11#![allow(clippy::too_many_arguments)]
12
13use std::f64::consts::PI;
14
15/// Universal gas constant \[J/(mol·K)\]
16const R_GAS: f64 = 8.314_462_618;
17
18// ─────────────────────────────────────────────────────────────────────────────
19// CeramicProperties
20// ─────────────────────────────────────────────────────────────────────────────
21
22/// Intrinsic physical properties of a ceramic material.
23///
24/// Contains elastic constants, thermal properties, and hardness data
25/// needed by fracture, sintering, and shock-resistance models.
26#[derive(Debug, Clone)]
27pub struct CeramicProperties {
28    /// Young's modulus \[Pa\]
29    pub young_modulus: f64,
30    /// Poisson's ratio \[-\]
31    pub poisson_ratio: f64,
32    /// Thermal conductivity \[W/(m·K)\]
33    pub thermal_conductivity: f64,
34    /// Coefficient of thermal expansion \[1/K\]
35    pub cte: f64,
36    /// Vickers hardness \[GPa\]
37    pub hardness: f64,
38    /// Fracture toughness K_IC \[MPa·√m\]
39    pub fracture_toughness: f64,
40    /// Flexural (Weibull characteristic) strength σ_0 \[Pa\]
41    pub flexural_strength: f64,
42    /// Weibull modulus m \[-\]
43    pub weibull_modulus: f64,
44    /// Specific surface energy γ \[J/m²\]
45    pub surface_energy: f64,
46    /// Material name
47    pub name: String,
48}
49
50impl CeramicProperties {
51    /// Creates a new `CeramicProperties` with all fields specified.
52    #[allow(clippy::too_many_arguments)]
53    pub fn new(
54        young_modulus: f64,
55        poisson_ratio: f64,
56        thermal_conductivity: f64,
57        cte: f64,
58        hardness: f64,
59        fracture_toughness: f64,
60        flexural_strength: f64,
61        weibull_modulus: f64,
62        surface_energy: f64,
63        name: impl Into<String>,
64    ) -> Self {
65        Self {
66            young_modulus,
67            poisson_ratio,
68            thermal_conductivity,
69            cte,
70            hardness,
71            fracture_toughness,
72            flexural_strength,
73            weibull_modulus,
74            surface_energy,
75            name: name.into(),
76        }
77    }
78
79    /// Returns preset properties for alumina (Al₂O₃).
80    pub fn alumina() -> Self {
81        Self::new(
82            380e9, 0.22, 30.0, 8.1e-6, 18.0, 4.0, 400e6, 10.0, 1.0, "Al2O3",
83        )
84    }
85
86    /// Returns preset properties for silicon carbide (SiC).
87    pub fn silicon_carbide() -> Self {
88        Self::new(
89            420e9, 0.17, 120.0, 4.5e-6, 27.0, 4.5, 500e6, 12.0, 1.5, "SiC",
90        )
91    }
92
93    /// Returns preset properties for silicon nitride (Si₃N₄).
94    pub fn silicon_nitride() -> Self {
95        Self::new(
96            310e9, 0.27, 30.0, 3.2e-6, 16.0, 7.0, 700e6, 15.0, 2.0, "Si3N4",
97        )
98    }
99
100    /// Returns preset properties for yttria-stabilised zirconia (ZrO₂).
101    pub fn zirconia() -> Self {
102        Self::new(
103            200e9, 0.31, 2.5, 10.5e-6, 12.0, 10.0, 1000e6, 8.0, 1.2, "ZrO2",
104        )
105    }
106
107    /// Returns preset properties for titanium dioxide (TiO₂).
108    pub fn titania() -> Self {
109        Self::new(
110            230e9, 0.27, 11.8, 8.4e-6, 10.0, 2.0, 200e6, 7.0, 0.5, "TiO2",
111        )
112    }
113}
114
115// ─────────────────────────────────────────────────────────────────────────────
116// BrittleFractureCeramic
117// ─────────────────────────────────────────────────────────────────────────────
118
119/// Griffith brittle-fracture model for ceramics.
120///
121/// Implements the Griffith criterion, K_IC-based failure, and Weibull
122/// statistical strength scatter.
123#[derive(Debug, Clone)]
124pub struct BrittleFractureCeramic {
125    /// Reference to ceramic properties
126    pub props: CeramicProperties,
127    /// Geometry factor Y for stress-intensity factor (typically ~1.12 for edge crack)
128    pub geometry_factor: f64,
129}
130
131impl BrittleFractureCeramic {
132    /// Creates a new `BrittleFractureCeramic`.
133    pub fn new(props: CeramicProperties, geometry_factor: f64) -> Self {
134        Self {
135            props,
136            geometry_factor,
137        }
138    }
139
140    /// Griffith critical stress for a crack of half-length `a` \[m\].
141    ///
142    /// σ_c = √(2·E·γ / (π·a))
143    ///
144    /// Returns critical stress \[Pa\].
145    pub fn griffith_critical_stress(&self, crack_half_length: f64) -> f64 {
146        let e = self.props.young_modulus;
147        let gamma = self.props.surface_energy;
148        (2.0 * e * gamma / (PI * crack_half_length)).sqrt()
149    }
150
151    /// Stress intensity factor K_I for applied stress `sigma` and crack half-length `a` \[m\].
152    ///
153    /// K_I = Y · σ · √(π·a)
154    ///
155    /// Returns K_I \[Pa·√m\].
156    pub fn stress_intensity(&self, sigma: f64, crack_half_length: f64) -> f64 {
157        self.geometry_factor * sigma * (PI * crack_half_length).sqrt()
158    }
159
160    /// Returns `true` if the crack propagates (K_I ≥ K_IC).
161    pub fn is_critical(&self, sigma: f64, crack_half_length: f64) -> bool {
162        let ki = self.stress_intensity(sigma, crack_half_length);
163        let kic = self.props.fracture_toughness * 1e6; // MPa√m → Pa√m
164        ki >= kic
165    }
166
167    /// Critical crack length at a given applied stress \[Pa\].
168    ///
169    /// a_c = (1/π) · (K_IC / (Y·σ))²
170    ///
171    /// Returns critical half-length \[m\].
172    pub fn critical_crack_length(&self, sigma: f64) -> f64 {
173        let kic = self.props.fracture_toughness * 1e6;
174        let y_sigma = self.geometry_factor * sigma;
175        (kic / y_sigma).powi(2) / PI
176    }
177
178    // ── Weibull statistics ─────────────────────────────────────────────────
179
180    /// Weibull survival probability P_s for applied stress `sigma`.
181    ///
182    /// P_s = exp(−(σ / σ_0)^m)
183    ///
184    /// where σ_0 is the Weibull characteristic strength and m the Weibull modulus.
185    pub fn weibull_survival_probability(&self, sigma: f64) -> f64 {
186        let sigma_0 = self.props.flexural_strength;
187        let m = self.props.weibull_modulus;
188        (-(sigma / sigma_0).powf(m)).exp()
189    }
190
191    /// Weibull failure probability P_f = 1 − P_s.
192    pub fn weibull_failure_probability(&self, sigma: f64) -> f64 {
193        1.0 - self.weibull_survival_probability(sigma)
194    }
195
196    /// Median failure stress (P_f = 0.5) from Weibull distribution \[Pa\].
197    ///
198    /// σ_med = σ_0 · (ln 2)^(1/m)
199    pub fn weibull_median_strength(&self) -> f64 {
200        let sigma_0 = self.props.flexural_strength;
201        let m = self.props.weibull_modulus;
202        sigma_0 * 2_f64.ln().powf(1.0 / m)
203    }
204
205    /// Mean Weibull strength \[Pa\]: σ_mean = σ_0 · Γ(1 + 1/m).
206    ///
207    /// Uses Stirling / rational approximation for the gamma function.
208    pub fn weibull_mean_strength(&self) -> f64 {
209        let sigma_0 = self.props.flexural_strength;
210        let m = self.props.weibull_modulus;
211        sigma_0 * gamma_approx(1.0 + 1.0 / m)
212    }
213}
214
215/// Simple rational approximation of Γ(x) for x in \[1, 3\] (Lanczos g=5).
216fn gamma_approx(x: f64) -> f64 {
217    // Lanczos coefficients g=5, n=7
218    let c = [
219        1.000_000_000_190_015,
220        76.180_091_729_471_46,
221        -86.505_320_329_416_77,
222        24.014_098_240_830_91,
223        -1.231_739_572_450_155,
224        1.208_650_973_866_179e-3,
225        -5.395_239_384_953_0e-6,
226    ];
227    let x = x - 1.0;
228    let mut ser = c[0];
229    for (i, &ci) in c[1..].iter().enumerate() {
230        ser += ci / (x + i as f64 + 1.0);
231    }
232    let t = x + 5.5; // g + 0.5
233    std::f64::consts::TAU.sqrt() * t.powf(x + 0.5) * (-t).exp() * ser
234}
235
236// ─────────────────────────────────────────────────────────────────────────────
237// ThermalShockResistance
238// ─────────────────────────────────────────────────────────────────────────────
239
240/// Thermal shock resistance figures of merit for ceramics.
241///
242/// Implements Hasselman's first (R) and second (R') parameters.
243#[derive(Debug, Clone)]
244pub struct ThermalShockResistance {
245    /// Ceramic properties
246    pub props: CeramicProperties,
247}
248
249impl ThermalShockResistance {
250    /// Creates a new `ThermalShockResistance`.
251    pub fn new(props: CeramicProperties) -> Self {
252        Self { props }
253    }
254
255    /// First thermal shock resistance parameter R \[K\].
256    ///
257    /// R = σ_f · (1 − ν) / (α · E)
258    ///
259    /// Returns R \[K\].
260    pub fn r_parameter(&self) -> f64 {
261        let sigma_f = self.props.flexural_strength;
262        let nu = self.props.poisson_ratio;
263        let alpha = self.props.cte;
264        let e = self.props.young_modulus;
265        sigma_f * (1.0 - nu) / (alpha * e)
266    }
267
268    /// Second thermal shock resistance parameter R' \[W/m\].
269    ///
270    /// R' = k · R   (with thermal conductivity k)
271    pub fn r_prime_parameter(&self) -> f64 {
272        self.props.thermal_conductivity * self.r_parameter()
273    }
274
275    /// Maximum temperature difference ΔT_c the material can sustain \[K\].
276    ///
277    /// ΔT_c ≈ R  (Kingery's approximation for an infinite plate, Biot → ∞)
278    pub fn critical_temperature_difference(&self) -> f64 {
279        self.r_parameter()
280    }
281
282    /// Thermal shock damage resistance R'''' \[m·Pa\] (Hasselman).
283    ///
284    /// R'''' = E · γ_f / (σ_f²)
285    pub fn r_damage_parameter(&self) -> f64 {
286        let e = self.props.young_modulus;
287        let gamma = self.props.surface_energy;
288        let sigma_f = self.props.flexural_strength;
289        e * gamma / (sigma_f * sigma_f)
290    }
291}
292
293// ─────────────────────────────────────────────────────────────────────────────
294// SinteringModel
295// ─────────────────────────────────────────────────────────────────────────────
296
297/// Solid-state sintering kinetics for ceramic powders.
298///
299/// Models neck growth and densification as functions of time and temperature.
300#[derive(Debug, Clone)]
301pub struct SinteringModel {
302    /// Pre-exponential constant for neck growth A \[m^n / (m^n · s)\] – dimensionless form
303    pub neck_growth_prefactor: f64,
304    /// Neck growth exponent n (typically 5–7 for volume diffusion)
305    pub neck_growth_exponent: f64,
306    /// Activation energy for neck growth Q \[J/mol\]
307    pub activation_energy_neck: f64,
308    /// Initial powder particle radius r \[m\]
309    pub particle_radius: f64,
310    /// Densification rate constant K \[1/s\]
311    pub densification_prefactor: f64,
312    /// Grain-size exponent for densification m (typically 3)
313    pub densification_grain_exponent: f64,
314    /// Activation energy for densification Q_d \[J/mol\]
315    pub activation_energy_densification: f64,
316    /// Initial relative density ρ₀ \[-\]
317    pub initial_density: f64,
318}
319
320impl SinteringModel {
321    /// Creates a new `SinteringModel` with all parameters.
322    #[allow(clippy::too_many_arguments)]
323    pub fn new(
324        neck_growth_prefactor: f64,
325        neck_growth_exponent: f64,
326        activation_energy_neck: f64,
327        particle_radius: f64,
328        densification_prefactor: f64,
329        densification_grain_exponent: f64,
330        activation_energy_densification: f64,
331        initial_density: f64,
332    ) -> Self {
333        Self {
334            neck_growth_prefactor,
335            neck_growth_exponent,
336            activation_energy_neck,
337            particle_radius,
338            densification_prefactor,
339            densification_grain_exponent,
340            activation_energy_densification,
341            initial_density,
342        }
343    }
344
345    /// Returns the neck-radius-to-particle-radius ratio x/r at time `t` \[s\] and
346    /// temperature `temp` \[K\].
347    ///
348    /// (x/r)^n = A · t · exp(−Q / (R·T))
349    pub fn neck_ratio(&self, time: f64, temp: f64) -> f64 {
350        let n = self.neck_growth_exponent;
351        let rate = self.neck_growth_prefactor
352            * time
353            * (-self.activation_energy_neck / (R_GAS * temp)).exp();
354        rate.powf(1.0 / n)
355    }
356
357    /// Returns the densification rate dρ/dt \[1/s\] at grain radius `r_grain` \[m\]
358    /// and temperature `temp` \[K\].
359    ///
360    /// dρ/dt = K · (1/r_grain)^m · exp(−Q_d / (R·T))
361    pub fn densification_rate(&self, r_grain: f64, temp: f64) -> f64 {
362        let m = self.densification_grain_exponent;
363        self.densification_prefactor
364            * r_grain.powf(-m)
365            * (-self.activation_energy_densification / (R_GAS * temp)).exp()
366    }
367
368    /// Approximate relative density ρ at time `t` \[s\] via first-order Euler integration
369    /// with `steps` steps.
370    pub fn density_at_time(&self, time: f64, r_grain: f64, temp: f64, steps: usize) -> f64 {
371        let dt = time / steps as f64;
372        let mut rho = self.initial_density;
373        let rate = self.densification_rate(r_grain, temp);
374        for _ in 0..steps {
375            rho += rate * dt;
376            if rho >= 1.0 {
377                return 1.0;
378            }
379        }
380        rho
381    }
382}
383
384// ─────────────────────────────────────────────────────────────────────────────
385// GrainGrowth
386// ─────────────────────────────────────────────────────────────────────────────
387
388/// Normal grain growth model for ceramic microstructures.
389///
390/// Uses the power-law equation d^n − d₀^n = K · t · exp(−Q / (R·T)).
391#[derive(Debug, Clone)]
392pub struct GrainGrowth {
393    /// Growth exponent n (≈ 2–4)
394    pub exponent: f64,
395    /// Pre-exponential constant K \[m^n / s\]
396    pub prefactor: f64,
397    /// Activation energy Q \[J/mol\]
398    pub activation_energy: f64,
399    /// Initial grain size d₀ \[m\]
400    pub initial_grain_size: f64,
401}
402
403impl GrainGrowth {
404    /// Creates a new `GrainGrowth` model.
405    pub fn new(
406        exponent: f64,
407        prefactor: f64,
408        activation_energy: f64,
409        initial_grain_size: f64,
410    ) -> Self {
411        Self {
412            exponent,
413            prefactor,
414            activation_energy,
415            initial_grain_size,
416        }
417    }
418
419    /// Grain size d \[m\] after annealing for `time` \[s\] at `temp` \[K\].
420    ///
421    /// d = (d₀^n + K · t · exp(−Q/(R·T)))^(1/n)
422    pub fn grain_size(&self, time: f64, temp: f64) -> f64 {
423        let n = self.exponent;
424        let d0n = self.initial_grain_size.powf(n);
425        let kt = self.prefactor * time * (-self.activation_energy / (R_GAS * temp)).exp();
426        (d0n + kt).powf(1.0 / n)
427    }
428
429    /// Grain growth rate dd/dt \[m/s\] at current grain size `d` and `temp` \[K\].
430    pub fn growth_rate(&self, d: f64, temp: f64) -> f64 {
431        let n = self.exponent;
432        let k_eff = self.prefactor * (-self.activation_energy / (R_GAS * temp)).exp();
433        k_eff / (n * d.powf(n - 1.0))
434    }
435
436    /// Returns `true` when grain growth has reached saturation within `tol`
437    /// fraction of the final grain size.
438    pub fn is_saturated(&self, time: f64, temp: f64, tol: f64) -> bool {
439        let d = self.grain_size(time, temp);
440        let d_long = self.grain_size(time * 1e6, temp);
441        (d_long - d) / d_long < tol
442    }
443}
444
445// ─────────────────────────────────────────────────────────────────────────────
446// CreepCeramic
447// ─────────────────────────────────────────────────────────────────────────────
448
449/// High-temperature creep model for ceramics.
450///
451/// Implements power-law creep: ε̇ = A · σ^n · exp(−Q / (R·T)).
452#[derive(Debug, Clone)]
453pub struct CreepCeramic {
454    /// Pre-exponential factor A \[1/s / Pa^n\]
455    pub prefactor: f64,
456    /// Stress exponent n (≈ 1 for diffusion creep, 3–5 for dislocation)
457    pub stress_exponent: f64,
458    /// Activation energy Q \[J/mol\]
459    pub activation_energy: f64,
460}
461
462impl CreepCeramic {
463    /// Creates a new `CreepCeramic` model.
464    pub fn new(prefactor: f64, stress_exponent: f64, activation_energy: f64) -> Self {
465        Self {
466            prefactor,
467            stress_exponent,
468            activation_energy,
469        }
470    }
471
472    /// Steady-state creep rate ε̇ \[1/s\] at stress `sigma` \[Pa\] and temperature `temp` \[K\].
473    pub fn creep_rate(&self, sigma: f64, temp: f64) -> f64 {
474        self.prefactor
475            * sigma.powf(self.stress_exponent)
476            * (-self.activation_energy / (R_GAS * temp)).exp()
477    }
478
479    /// Activation energy estimated from two creep rates at the same stress \[J/mol\].
480    ///
481    /// Q = R · T₁ · T₂ / (T₂ − T₁) · ln(ε̇₂ / ε̇₁)
482    ///
483    /// where `rate2` is measured at the higher temperature `temp2 > temp1`.
484    pub fn apparent_activation_energy(rate1: f64, temp1: f64, rate2: f64, temp2: f64) -> f64 {
485        R_GAS * temp1 * temp2 / (temp2 - temp1) * (rate2 / rate1).ln()
486    }
487
488    /// Creep strain accumulated over `time` \[s\] at constant `sigma` and `temp`.
489    pub fn accumulated_strain(&self, sigma: f64, temp: f64, time: f64) -> f64 {
490        self.creep_rate(sigma, temp) * time
491    }
492}
493
494// ─────────────────────────────────────────────────────────────────────────────
495// DielectricCeramic
496// ─────────────────────────────────────────────────────────────────────────────
497
498/// Ferroelectric/dielectric ceramic model (BaTiO₃-type).
499///
500/// Implements Curie-Weiss behaviour and piezoelectric coefficients.
501#[derive(Debug, Clone)]
502pub struct DielectricCeramic {
503    /// Curie constant C \[K\]
504    pub curie_constant: f64,
505    /// Curie temperature T_C \[K\]
506    pub curie_temperature: f64,
507    /// Piezoelectric coefficient d33 \[pC/N\]
508    pub d33: f64,
509    /// Piezoelectric coefficient d31 \[pC/N\]
510    pub d31: f64,
511    /// Low-frequency permittivity below T_C (relative, ferroelectric phase)
512    pub permittivity_ferroelectric: f64,
513}
514
515impl DielectricCeramic {
516    /// Creates a new `DielectricCeramic`.
517    pub fn new(
518        curie_constant: f64,
519        curie_temperature: f64,
520        d33: f64,
521        d31: f64,
522        permittivity_ferroelectric: f64,
523    ) -> Self {
524        Self {
525            curie_constant,
526            curie_temperature,
527            d33,
528            d31,
529            permittivity_ferroelectric,
530        }
531    }
532
533    /// BaTiO₃ preset (approximate room-temperature values).
534    pub fn barium_titanate() -> Self {
535        Self::new(1.7e5, 393.0, 190.0, -79.0, 4000.0)
536    }
537
538    /// Relative permittivity above T_C via Curie-Weiss law: ε_r = C / (T − T_C).
539    ///
540    /// Returns `None` if T ≤ T_C (paraelectric law not applicable below Curie point).
541    pub fn permittivity_above_tc(&self, temp: f64) -> Option<f64> {
542        if temp <= self.curie_temperature {
543            None
544        } else {
545            Some(self.curie_constant / (temp - self.curie_temperature))
546        }
547    }
548
549    /// Returns the relative permittivity at `temp` \[K\], using Curie-Weiss above T_C
550    /// and the stored ferroelectric value below T_C.
551    pub fn permittivity(&self, temp: f64) -> f64 {
552        self.permittivity_above_tc(temp)
553            .unwrap_or(self.permittivity_ferroelectric)
554    }
555
556    /// Electric displacement D \[C/m²\] from applied field E \[V/m\] at `temp` \[K\].
557    ///
558    /// D = ε_0 · ε_r · E
559    pub fn electric_displacement(&self, e_field: f64, temp: f64) -> f64 {
560        const EPS0: f64 = 8.854_187_817e-12;
561        EPS0 * self.permittivity(temp) * e_field
562    }
563
564    /// Piezoelectric strain S₃₃ \[−\] from applied stress T₃₃ \[Pa\] and field E₃ \[V/m\].
565    ///
566    /// S₃₃ = d₃₃ · E₃  (linear piezoelectric contribution only)
567    pub fn piezo_strain_33(&self, e3: f64) -> f64 {
568        self.d33 * 1e-12 * e3
569    }
570}
571
572// ─────────────────────────────────────────────────────────────────────────────
573// ZrO2Transformation
574// ─────────────────────────────────────────────────────────────────────────────
575
576/// Tetragonal-to-monoclinic transformation toughening in ZrO₂ ceramics.
577///
578/// Estimates the transformation zone size and toughening increment ΔK_IC.
579#[derive(Debug, Clone)]
580pub struct ZrO2Transformation {
581    /// Transformation dilatational strain ε_T \[-\] (~0.04 for t→m)
582    pub transformation_strain: f64,
583    /// Volume fraction of transformable particles f_v \[-\]
584    pub volume_fraction: f64,
585    /// Shear modulus G \[Pa\]
586    pub shear_modulus: f64,
587    /// Intrinsic fracture toughness K_IC0 \[Pa·√m\]
588    pub base_toughness: f64,
589    /// Transformation zone half-height h \[m\] (determined externally or fitted)
590    pub zone_height: f64,
591}
592
593impl ZrO2Transformation {
594    /// Creates a new `ZrO2Transformation` model.
595    pub fn new(
596        transformation_strain: f64,
597        volume_fraction: f64,
598        shear_modulus: f64,
599        base_toughness: f64,
600        zone_height: f64,
601    ) -> Self {
602        Self {
603            transformation_strain,
604            volume_fraction,
605            shear_modulus,
606            base_toughness,
607            zone_height,
608        }
609    }
610
611    /// Toughening increment ΔK_IC \[Pa·√m\] due to transformation.
612    ///
613    /// ΔK_IC ≈ 0.22 · E_eff · ε_T · f_v · √h  (Budiansky-Hutchinson-Lambropoulos)
614    /// where E_eff = 2·G (for plane strain approximation).
615    pub fn toughening_increment(&self) -> f64 {
616        let e_eff = 2.0 * self.shear_modulus;
617        0.22 * e_eff * self.transformation_strain * self.volume_fraction * self.zone_height.sqrt()
618    }
619
620    /// Total effective fracture toughness K_IC_eff \[Pa·√m\].
621    pub fn effective_toughness(&self) -> f64 {
622        self.base_toughness + self.toughening_increment()
623    }
624
625    /// Approximate transformation zone size (Irwin-type estimate) \[m\].
626    ///
627    /// h ≈ (1/3π) · (K_IC / σ_t)²
628    /// where σ_t is the transformation stress: σ_t = G · ε_T / (1 − f_v).
629    pub fn zone_size_estimate(&self) -> f64 {
630        let sigma_t =
631            self.shear_modulus * self.transformation_strain / (1.0 - self.volume_fraction);
632        let kic = self.effective_toughness();
633        (kic / sigma_t).powi(2) / (3.0 * PI)
634    }
635}
636
637// ─────────────────────────────────────────────────────────────────────────────
638// Tests
639// ─────────────────────────────────────────────────────────────────────────────
640
641#[cfg(test)]
642mod tests {
643    use super::*;
644
645    // ── CeramicProperties presets ─────────────────────────────────────────
646
647    #[test]
648    fn test_alumina_young_modulus() {
649        let al2o3 = CeramicProperties::alumina();
650        assert!(
651            (al2o3.young_modulus - 380e9).abs() < 1e6,
652            "E(Al2O3) should be ~380 GPa"
653        );
654    }
655
656    #[test]
657    fn test_sic_fracture_toughness() {
658        let sic = CeramicProperties::silicon_carbide();
659        assert!((sic.fracture_toughness - 4.5).abs() < 0.01);
660    }
661
662    #[test]
663    fn test_zirconia_high_toughness() {
664        let zro2 = CeramicProperties::zirconia();
665        assert!(zro2.fracture_toughness > CeramicProperties::alumina().fracture_toughness);
666    }
667
668    #[test]
669    fn test_titania_properties_positive() {
670        let tio2 = CeramicProperties::titania();
671        assert!(tio2.young_modulus > 0.0);
672        assert!(tio2.thermal_conductivity > 0.0);
673        assert!(tio2.cte > 0.0);
674    }
675
676    // ── BrittleFractureCeramic ────────────────────────────────────────────
677
678    #[test]
679    fn test_griffith_larger_crack_lower_strength() {
680        let props = CeramicProperties::alumina();
681        let frac = BrittleFractureCeramic::new(props, 1.12);
682        let sigma_small = frac.griffith_critical_stress(1e-6);
683        let sigma_large = frac.griffith_critical_stress(100e-6);
684        assert!(
685            sigma_small > sigma_large,
686            "Larger crack → lower Griffith strength"
687        );
688    }
689
690    #[test]
691    fn test_stress_intensity_proportional_to_sigma() {
692        let props = CeramicProperties::alumina();
693        let frac = BrittleFractureCeramic::new(props, 1.12);
694        let k1 = frac.stress_intensity(100e6, 10e-6);
695        let k2 = frac.stress_intensity(200e6, 10e-6);
696        assert!((k2 / k1 - 2.0).abs() < 1e-10);
697    }
698
699    #[test]
700    fn test_is_critical_small_crack_not_critical() {
701        let props = CeramicProperties::alumina();
702        let frac = BrittleFractureCeramic::new(props, 1.12);
703        // Tiny crack at moderate stress should not be critical
704        assert!(!frac.is_critical(100e6, 1e-9));
705    }
706
707    #[test]
708    fn test_is_critical_large_crack_is_critical() {
709        let props = CeramicProperties::alumina();
710        let frac = BrittleFractureCeramic::new(props, 1.12);
711        // Very large crack at high stress should be critical
712        assert!(frac.is_critical(400e6, 1e-3));
713    }
714
715    #[test]
716    fn test_critical_crack_length_decreases_with_stress() {
717        let props = CeramicProperties::alumina();
718        let frac = BrittleFractureCeramic::new(props, 1.12);
719        let a1 = frac.critical_crack_length(100e6);
720        let a2 = frac.critical_crack_length(400e6);
721        assert!(a1 > a2, "Higher stress → shorter critical crack");
722    }
723
724    #[test]
725    fn test_weibull_survival_at_zero_stress() {
726        let props = CeramicProperties::alumina();
727        let frac = BrittleFractureCeramic::new(props, 1.12);
728        let ps = frac.weibull_survival_probability(0.0);
729        assert!((ps - 1.0).abs() < 1e-12, "Survival at zero stress = 1");
730    }
731
732    #[test]
733    fn test_weibull_survival_at_characteristic_strength() {
734        let props = CeramicProperties::alumina();
735        let frac = BrittleFractureCeramic::new(props.clone(), 1.12);
736        let ps = frac.weibull_survival_probability(props.flexural_strength);
737        // P_s(σ_0) = exp(-1) ≈ 0.368
738        assert!((ps - (-1.0_f64).exp()).abs() < 1e-10);
739    }
740
741    #[test]
742    fn test_weibull_failure_probability_complement() {
743        let props = CeramicProperties::alumina();
744        let frac = BrittleFractureCeramic::new(props, 1.12);
745        let sigma = 300e6;
746        let ps = frac.weibull_survival_probability(sigma);
747        let pf = frac.weibull_failure_probability(sigma);
748        assert!((ps + pf - 1.0).abs() < 1e-12);
749    }
750
751    #[test]
752    fn test_weibull_high_modulus_steep_transition() {
753        // Very high Weibull modulus → failure probability transitions sharply near σ_0
754        let mut props = CeramicProperties::alumina();
755        props.weibull_modulus = 100.0;
756        let frac = BrittleFractureCeramic::new(props.clone(), 1.12);
757        let pf_below = frac.weibull_failure_probability(0.99 * props.flexural_strength);
758        let pf_above = frac.weibull_failure_probability(1.01 * props.flexural_strength);
759        assert!(pf_above - pf_below > 0.5, "High m → steep transition");
760    }
761
762    #[test]
763    fn test_weibull_median_less_than_characteristic() {
764        let props = CeramicProperties::alumina();
765        let frac = BrittleFractureCeramic::new(props.clone(), 1.12);
766        let median = frac.weibull_median_strength();
767        assert!(median < props.flexural_strength);
768    }
769
770    #[test]
771    fn test_weibull_mean_strength_positive() {
772        let props = CeramicProperties::silicon_nitride();
773        let frac = BrittleFractureCeramic::new(props, 1.12);
774        assert!(frac.weibull_mean_strength() > 0.0);
775    }
776
777    // ── ThermalShockResistance ────────────────────────────────────────────
778
779    #[test]
780    fn test_r_parameter_increases_with_strength() {
781        let mut p1 = CeramicProperties::alumina();
782        let mut p2 = CeramicProperties::alumina();
783        p2.flexural_strength *= 2.0;
784        let r1 = ThermalShockResistance::new(p1.clone()).r_parameter();
785        let r2 = ThermalShockResistance::new(p2).r_parameter();
786        assert!(r2 > r1);
787        // Suppress unused warning
788        p1.name = p1.name.clone();
789    }
790
791    #[test]
792    fn test_r_parameter_decreases_with_youngs_modulus() {
793        let mut p1 = CeramicProperties::alumina();
794        let mut p2 = CeramicProperties::alumina();
795        p2.young_modulus *= 2.0;
796        let r1 = ThermalShockResistance::new(p1.clone()).r_parameter();
797        let r2 = ThermalShockResistance::new(p2).r_parameter();
798        assert!(r1 > r2, "Higher E → lower R");
799        p1.name = p1.name.clone();
800    }
801
802    #[test]
803    fn test_r_prime_greater_than_r_for_high_k() {
804        let props = CeramicProperties::silicon_carbide(); // k = 120 W/(m·K)
805        let tsr = ThermalShockResistance::new(props);
806        assert!(tsr.r_prime_parameter() > tsr.r_parameter());
807    }
808
809    #[test]
810    fn test_r_damage_parameter_positive() {
811        let props = CeramicProperties::zirconia();
812        let tsr = ThermalShockResistance::new(props);
813        assert!(tsr.r_damage_parameter() > 0.0);
814    }
815
816    #[test]
817    fn test_critical_temperature_difference_equals_r() {
818        let props = CeramicProperties::alumina();
819        let tsr = ThermalShockResistance::new(props);
820        assert!((tsr.critical_temperature_difference() - tsr.r_parameter()).abs() < 1e-12);
821    }
822
823    // ── SinteringModel ────────────────────────────────────────────────────
824
825    #[test]
826    fn test_density_increases_with_time() {
827        let model = SinteringModel::new(1e-5, 5.0, 300e3, 1e-6, 1e-8, 3.0, 450e3, 0.60);
828        let rho_early = model.density_at_time(100.0, 1e-6, 1800.0, 100);
829        let rho_late = model.density_at_time(10000.0, 1e-6, 1800.0, 100);
830        assert!(
831            rho_late > rho_early,
832            "Density should increase with sintering time"
833        );
834    }
835
836    #[test]
837    fn test_density_bounded_by_unity() {
838        let model = SinteringModel::new(1e-5, 5.0, 100e3, 1e-6, 1e-5, 3.0, 100e3, 0.60);
839        let rho = model.density_at_time(1e9, 1e-6, 2000.0, 1000);
840        assert!(rho <= 1.0, "Density cannot exceed 1");
841    }
842
843    #[test]
844    fn test_neck_ratio_increases_with_time() {
845        let model = SinteringModel::new(1e-10, 5.0, 300e3, 1e-6, 1e-8, 3.0, 450e3, 0.60);
846        let xr_early = model.neck_ratio(100.0, 1600.0);
847        let xr_late = model.neck_ratio(10000.0, 1600.0);
848        assert!(xr_late > xr_early);
849    }
850
851    #[test]
852    fn test_neck_ratio_increases_with_temperature() {
853        let model = SinteringModel::new(1e-10, 5.0, 300e3, 1e-6, 1e-8, 3.0, 450e3, 0.60);
854        let xr_low = model.neck_ratio(1000.0, 1400.0);
855        let xr_high = model.neck_ratio(1000.0, 1800.0);
856        assert!(xr_high > xr_low);
857    }
858
859    #[test]
860    fn test_densification_rate_positive() {
861        let model = SinteringModel::new(1e-10, 5.0, 300e3, 1e-6, 1e-8, 3.0, 450e3, 0.60);
862        let rate = model.densification_rate(1e-6, 1800.0);
863        assert!(rate > 0.0);
864    }
865
866    // ── GrainGrowth ───────────────────────────────────────────────────────
867
868    #[test]
869    fn test_grain_growth_increases_with_time() {
870        let gg = GrainGrowth::new(2.0, 1e-14, 300e3, 1e-6);
871        let d1 = gg.grain_size(100.0, 1500.0);
872        let d2 = gg.grain_size(10000.0, 1500.0);
873        assert!(d2 > d1, "Grain size should increase with annealing time");
874    }
875
876    #[test]
877    fn test_grain_size_at_zero_time_equals_initial() {
878        let d0 = 1.5e-6;
879        let gg = GrainGrowth::new(2.0, 1e-14, 300e3, d0);
880        let d = gg.grain_size(0.0, 1500.0);
881        assert!((d - d0).abs() < 1e-15, "d(t=0) should equal d0");
882    }
883
884    #[test]
885    fn test_grain_growth_faster_at_higher_temp() {
886        let gg = GrainGrowth::new(2.0, 1e-14, 300e3, 1e-6);
887        let d_low = gg.grain_size(3600.0, 1200.0);
888        let d_high = gg.grain_size(3600.0, 1600.0);
889        assert!(d_high > d_low);
890    }
891
892    #[test]
893    fn test_grain_growth_exponent_effect() {
894        // For large K*t >> d0^n, smaller exponent n gives a larger final grain size
895        // because the 1/n power is larger. Use a very large prefactor so K*t >> d0^n
896        // for both n=2 and n=3.
897        let d0 = 1e-6_f64;
898        let gg_n2 = GrainGrowth::new(2.0, 1e-10, 0.0, d0); // Q=0 → fully thermal-agnostic
899        let gg_n3 = GrainGrowth::new(3.0, 1e-10, 0.0, d0);
900        let d_n2 = gg_n2.grain_size(1.0, 1000.0);
901        let d_n3 = gg_n3.grain_size(1.0, 1000.0);
902        // With Q=0 and large K: d_n2 = (d0^2 + K)^(1/2), d_n3 = (d0^3 + K)^(1/3)
903        // K=1e-10: sqrt(1e-10)~3e-6, cbrt(1e-10)~4.6e-4 → n=3 gives larger d
904        // So d_n3 > d_n2 in this regime; verify both are larger than d0
905        assert!(d_n2 > d0, "n=2 grain size should grow beyond initial");
906        assert!(d_n3 > d0, "n=3 grain size should grow beyond initial");
907        // The important physics: both grow, and at least one grows detectably
908        assert!(d_n2 > d0 * 1.5 || d_n3 > d0 * 1.5);
909    }
910
911    #[test]
912    fn test_growth_rate_positive() {
913        let gg = GrainGrowth::new(2.0, 1e-14, 300e3, 1e-6);
914        assert!(gg.growth_rate(2e-6, 1500.0) > 0.0);
915    }
916
917    // ── CreepCeramic ──────────────────────────────────────────────────────
918
919    #[test]
920    fn test_creep_rate_exponential_temperature() {
921        let creep = CreepCeramic::new(1e-30, 3.0, 500e3);
922        let rate_low = creep.creep_rate(100e6, 1000.0);
923        let rate_high = creep.creep_rate(100e6, 1400.0);
924        assert!(
925            rate_high > rate_low * 100.0,
926            "Creep rate increases strongly with temperature"
927        );
928    }
929
930    #[test]
931    fn test_creep_rate_power_law_stress() {
932        let creep = CreepCeramic::new(1e-30, 3.0, 500e3);
933        let r1 = creep.creep_rate(100e6, 1200.0);
934        let r2 = creep.creep_rate(200e6, 1200.0);
935        // n=3 → rate doubles approximately 2^3 = 8×
936        assert!((r2 / r1 - 8.0).abs() < 1e-10);
937    }
938
939    #[test]
940    fn test_accumulated_strain_proportional_to_time() {
941        let creep = CreepCeramic::new(1e-30, 3.0, 500e3);
942        let eps1 = creep.accumulated_strain(100e6, 1200.0, 1000.0);
943        let eps2 = creep.accumulated_strain(100e6, 1200.0, 2000.0);
944        assert!((eps2 / eps1 - 2.0).abs() < 1e-12);
945    }
946
947    #[test]
948    fn test_apparent_activation_energy_estimate() {
949        let creep = CreepCeramic::new(1e-30, 3.0, 500e3);
950        let t1 = 1200.0_f64;
951        let t2 = 1400.0_f64;
952        let r1 = creep.creep_rate(100e6, t1);
953        let r2 = creep.creep_rate(100e6, t2);
954        let q_est = CreepCeramic::apparent_activation_energy(r1, t1, r2, t2);
955        assert!(
956            (q_est - 500e3).abs() / 500e3 < 0.01,
957            "Q estimate should be within 1% of true value"
958        );
959    }
960
961    // ── DielectricCeramic ─────────────────────────────────────────────────
962
963    #[test]
964    fn test_curie_weiss_diverges_near_tc() {
965        let bto = DielectricCeramic::barium_titanate();
966        let eps_near = bto
967            .permittivity_above_tc(bto.curie_temperature + 1.0)
968            .unwrap();
969        let eps_far = bto
970            .permittivity_above_tc(bto.curie_temperature + 100.0)
971            .unwrap();
972        assert!(eps_near > eps_far * 10.0, "ε_r diverges near T_C");
973    }
974
975    #[test]
976    fn test_permittivity_below_tc_returns_ferroelectric_value() {
977        let bto = DielectricCeramic::barium_titanate();
978        let eps = bto.permittivity(300.0); // room temperature
979        assert!((eps - bto.permittivity_ferroelectric).abs() < 1e-12);
980    }
981
982    #[test]
983    fn test_permittivity_above_tc_is_none_at_tc() {
984        let bto = DielectricCeramic::barium_titanate();
985        assert!(bto.permittivity_above_tc(bto.curie_temperature).is_none());
986    }
987
988    #[test]
989    fn test_electric_displacement_proportional_to_field() {
990        let bto = DielectricCeramic::barium_titanate();
991        let d1 = bto.electric_displacement(1e5, 500.0);
992        let d2 = bto.electric_displacement(2e5, 500.0);
993        assert!((d2 / d1 - 2.0).abs() < 1e-10);
994    }
995
996    #[test]
997    fn test_piezo_strain_33_sign() {
998        let bto = DielectricCeramic::barium_titanate();
999        // d33 > 0 → positive strain for positive field
1000        assert!(bto.piezo_strain_33(1e6) > 0.0);
1001    }
1002
1003    // ── ZrO2Transformation ────────────────────────────────────────────────
1004
1005    #[test]
1006    fn test_toughening_increment_positive() {
1007        let zro2 = ZrO2Transformation::new(0.04, 0.3, 80e9, 5e6, 1e-4);
1008        assert!(zro2.toughening_increment() > 0.0);
1009    }
1010
1011    #[test]
1012    fn test_effective_toughness_greater_than_base() {
1013        let zro2 = ZrO2Transformation::new(0.04, 0.3, 80e9, 5e6, 1e-4);
1014        assert!(zro2.effective_toughness() > zro2.base_toughness);
1015    }
1016
1017    #[test]
1018    fn test_toughening_increases_with_volume_fraction() {
1019        let zro2_low = ZrO2Transformation::new(0.04, 0.1, 80e9, 5e6, 1e-4);
1020        let zro2_high = ZrO2Transformation::new(0.04, 0.4, 80e9, 5e6, 1e-4);
1021        assert!(zro2_high.toughening_increment() > zro2_low.toughening_increment());
1022    }
1023
1024    #[test]
1025    fn test_zone_size_estimate_positive() {
1026        let zro2 = ZrO2Transformation::new(0.04, 0.3, 80e9, 5e6, 1e-4);
1027        assert!(zro2.zone_size_estimate() > 0.0);
1028    }
1029
1030    #[test]
1031    fn test_toughening_increases_with_strain() {
1032        let zro2_low = ZrO2Transformation::new(0.02, 0.3, 80e9, 5e6, 1e-4);
1033        let zro2_high = ZrO2Transformation::new(0.06, 0.3, 80e9, 5e6, 1e-4);
1034        assert!(zro2_high.toughening_increment() > zro2_low.toughening_increment());
1035    }
1036}