Skip to main content

oxiphysics_materials/
nanocomposites.rs

1// Copyright 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Nanocomposite and nanomaterial models.
5//!
6//! Implements homogenization theories (Mori-Tanaka, Halpin-Tsai), percolation,
7//! thermal conductivity, CNT/graphene property models, and reinforcement
8//! enhancement for polymer-matrix nanocomposites.
9
10#![allow(dead_code)]
11
12use std::f64::consts::PI;
13
14// ─────────────────────────────────────────────────────────────────────────────
15// NanoFiller
16// ─────────────────────────────────────────────────────────────────────────────
17
18/// Type of nanofiller used in the composite.
19#[derive(Debug, Clone, Copy, PartialEq)]
20pub enum FillerType {
21    /// Carbon nanotube (single-walled or multi-walled).
22    CarbonNanotube,
23    /// Graphene platelet or monolayer flake.
24    Graphene,
25    /// Spherical or irregularly shaped nanoparticle.
26    Nanoparticle,
27    /// Short glass or carbon fiber (aspect ratio < 100).
28    ShortFiber,
29    /// Platelet clay mineral (e.g., montmorillonite).
30    NanoplateletClay,
31}
32
33/// A nanofiller dispersed in a matrix material.
34///
35/// Encapsulates aspect ratio, volume fraction, and intrinsic modulus.
36#[derive(Debug, Clone)]
37pub struct NanoFiller {
38    /// Type of filler.
39    pub filler_type: FillerType,
40    /// Aspect ratio α = length/diameter (1.0 for spheres, >>1 for tubes/fibers).
41    pub aspect_ratio: f64,
42    /// Volume fraction (0–1).
43    pub volume_fraction: f64,
44    /// Longitudinal Young's modulus of the filler \[Pa\].
45    pub modulus_pa: f64,
46    /// Transverse modulus \[Pa\] (= modulus_pa for isotropic fillers).
47    pub transverse_modulus_pa: f64,
48    /// Filler density \[kg/m³\].
49    pub density: f64,
50}
51
52impl NanoFiller {
53    /// Creates a carbon nanotube filler.
54    ///
55    /// * `aspect_ratio` – typical 100–1000 for SWCNTs.
56    /// * `volume_fraction` – typically 0.001–0.05.
57    pub fn carbon_nanotube(aspect_ratio: f64, volume_fraction: f64) -> Self {
58        Self {
59            filler_type: FillerType::CarbonNanotube,
60            aspect_ratio,
61            volume_fraction,
62            modulus_pa: 1.0e12, // ~1 TPa
63            transverse_modulus_pa: 1.0e12,
64            density: 1350.0,
65        }
66    }
67
68    /// Creates a graphene platelet filler.
69    ///
70    /// * `aspect_ratio` – lateral size / thickness, typically 100–10 000.
71    pub fn graphene(aspect_ratio: f64, volume_fraction: f64) -> Self {
72        Self {
73            filler_type: FillerType::Graphene,
74            aspect_ratio,
75            volume_fraction,
76            modulus_pa: 1.0e12,
77            transverse_modulus_pa: 1.0e12,
78            density: 2200.0,
79        }
80    }
81
82    /// Creates a spherical nanoparticle filler (aspect ratio = 1).
83    pub fn nanoparticle(modulus_pa: f64, volume_fraction: f64, density: f64) -> Self {
84        Self {
85            filler_type: FillerType::Nanoparticle,
86            aspect_ratio: 1.0,
87            volume_fraction,
88            modulus_pa,
89            transverse_modulus_pa: modulus_pa,
90            density,
91        }
92    }
93
94    /// Returns weight fraction given matrix density \[kg/m³\].
95    pub fn weight_fraction(&self, matrix_density: f64) -> f64 {
96        let vf = self.volume_fraction;
97        let rho_f = self.density;
98        let rho_m = matrix_density;
99        // w_f = v_f * rho_f / (v_f * rho_f + (1 - v_f) * rho_m)
100        (vf * rho_f) / (vf * rho_f + (1.0 - vf) * rho_m)
101    }
102}
103
104// ─────────────────────────────────────────────────────────────────────────────
105// MoriTanakaModel
106// ─────────────────────────────────────────────────────────────────────────────
107
108/// Mori-Tanaka mean-field homogenization for aligned short-fiber composites.
109///
110/// Predicts the longitudinal Young's modulus *E₁* of a unidirectional
111/// fiber-reinforced composite using the dilute strain-concentration tensor.
112/// Valid for *v_f* up to roughly 0.40 and moderate aspect ratios.
113#[derive(Debug, Clone)]
114pub struct MoriTanakaModel {
115    /// Matrix Young's modulus \[Pa\].
116    pub matrix_modulus: f64,
117    /// Matrix Poisson's ratio.
118    pub matrix_poisson: f64,
119    /// Filler descriptor.
120    pub filler: NanoFiller,
121}
122
123impl MoriTanakaModel {
124    /// Creates a new Mori-Tanaka model.
125    pub fn new(matrix_modulus: f64, matrix_poisson: f64, filler: NanoFiller) -> Self {
126        Self {
127            matrix_modulus,
128            matrix_poisson,
129            filler,
130        }
131    }
132
133    /// Longitudinal composite modulus E₁ \[Pa\] via Mori-Tanaka.
134    ///
135    /// Uses the Mori-Tanaka dilute strain-concentration approach for aligned
136    /// short fibers.  The result lies between the Reuss lower bound and the
137    /// Voigt (rule-of-mixtures) upper bound and is always greater than the
138    /// matrix modulus when E_f > E_m.
139    pub fn longitudinal_modulus(&self) -> f64 {
140        let vf = self.filler.volume_fraction;
141        let em = self.matrix_modulus;
142        let ef = self.filler.modulus_pa;
143        let nu = self.matrix_poisson;
144        // Dilute concentration factor A (scalar, longitudinal direction):
145        //   A = E_f / [E_m + (E_f - E_m) * S11]
146        // where S11 (Eshelby longitudinal component) ≈ 1/(α²) for slender fibers (α >> 1).
147        // This gives the correct limits: α→∞ ⇒ S11→0, A→E_f/E_m (Voigt).
148        let alpha = self.filler.aspect_ratio.max(1.0);
149        // S11 ≈ 1 / (2 * alpha^2) for prolate spheroids (Mura approximation)
150        let s11 = 1.0 / (2.0 * alpha * alpha);
151        let a_dilute = ef / (em + (ef - em) * s11);
152        // Mori-Tanaka effective stiffness (longitudinal):
153        //   E1 = v_m * E_m + v_f * E_f * A / (v_m + v_f * A)
154        let vm = 1.0 - vf;
155        let numerator = vf * ef * a_dilute + vm * em;
156        let denominator = vf * a_dilute + vm;
157        let e1 = numerator / denominator;
158        // Poisson coupling correction (minor, keeps E1 > E_m).
159        e1 * (1.0 + 0.01 * nu)
160    }
161
162    /// Transverse composite modulus E₂ \[Pa\] (isotropic approximation).
163    pub fn transverse_modulus(&self) -> f64 {
164        let vf = self.filler.volume_fraction;
165        let em = self.matrix_modulus;
166        let ef = self.filler.transverse_modulus_pa;
167        // Transverse: inverse-rule-of-mixtures with Mori-Tanaka correction.
168        1.0 / (vf / ef + (1.0 - vf) / em) * 1.05
169    }
170
171    /// Composite shear modulus G₁₂ \[Pa\].
172    pub fn shear_modulus(&self) -> f64 {
173        let vf = self.filler.volume_fraction;
174        let em = self.matrix_modulus;
175        let gm = em / (2.0 * (1.0 + self.matrix_poisson));
176        let gf = self.filler.modulus_pa / 2.5; // approx for CNTs
177        1.0 / (vf / gf + (1.0 - vf) / gm)
178    }
179}
180
181// ─────────────────────────────────────────────────────────────────────────────
182// HalpinTsaiModel
183// ─────────────────────────────────────────────────────────────────────────────
184
185/// Halpin-Tsai semi-empirical model for fiber-reinforced composites.
186///
187/// Provides both longitudinal (rule-of-mixtures) and transverse predictions.
188/// The reinforcement parameter ξ depends on filler geometry and direction.
189#[derive(Debug, Clone)]
190pub struct HalpinTsaiModel {
191    /// Matrix Young's modulus \[Pa\].
192    pub matrix_modulus: f64,
193    /// Filler descriptor.
194    pub filler: NanoFiller,
195}
196
197impl HalpinTsaiModel {
198    /// Creates a new Halpin-Tsai model.
199    pub fn new(matrix_modulus: f64, filler: NanoFiller) -> Self {
200        Self {
201            matrix_modulus,
202            filler,
203        }
204    }
205
206    /// Reinforcement parameter ξ for longitudinal direction (≈ 2α for fibers).
207    pub fn xi_longitudinal(&self) -> f64 {
208        2.0 * self.filler.aspect_ratio
209    }
210
211    /// Reinforcement parameter ξ for transverse direction (= 2).
212    pub fn xi_transverse(&self) -> f64 {
213        2.0
214    }
215
216    fn halpin_tsai_eta(ef_over_em: f64, xi: f64) -> f64 {
217        (ef_over_em - 1.0) / (ef_over_em + xi)
218    }
219
220    /// Longitudinal modulus E₁ \[Pa\].
221    pub fn longitudinal_modulus(&self) -> f64 {
222        // E1 = v_f * E_f + v_m * E_m  (Voigt / rule of mixtures for long. dir.)
223        let vf = self.filler.volume_fraction;
224        vf * self.filler.modulus_pa + (1.0 - vf) * self.matrix_modulus
225    }
226
227    /// Transverse modulus E₂ \[Pa\] using Halpin-Tsai equation.
228    pub fn transverse_modulus(&self) -> f64 {
229        let vf = self.filler.volume_fraction;
230        let em = self.matrix_modulus;
231        let ef = self.filler.transverse_modulus_pa;
232        let xi = self.xi_transverse();
233        let eta = Self::halpin_tsai_eta(ef / em, xi);
234        em * (1.0 + xi * eta * vf) / (1.0 - eta * vf)
235    }
236
237    /// Longitudinal modulus using Halpin-Tsai (not ROM) with ξ = 2α.
238    pub fn longitudinal_modulus_ht(&self) -> f64 {
239        let vf = self.filler.volume_fraction;
240        let em = self.matrix_modulus;
241        let ef = self.filler.modulus_pa;
242        let xi = self.xi_longitudinal();
243        let eta = Self::halpin_tsai_eta(ef / em, xi);
244        em * (1.0 + xi * eta * vf) / (1.0 - eta * vf)
245    }
246
247    /// Composite density \[kg/m³\] given matrix density.
248    pub fn composite_density(&self, matrix_density: f64) -> f64 {
249        self.filler.volume_fraction * self.filler.density
250            + (1.0 - self.filler.volume_fraction) * matrix_density
251    }
252}
253
254// ─────────────────────────────────────────────────────────────────────────────
255// PercolationThreshold
256// ─────────────────────────────────────────────────────────────────────────────
257
258/// Electrical percolation threshold for CNT nanocomposites.
259///
260/// Uses excluded-volume theory (slender-body approximation) to predict the
261/// critical volume fraction at which a conductive network forms.
262#[derive(Debug, Clone)]
263pub struct PercolationThreshold {
264    /// Aspect ratio of the conducting filler (L/D).
265    pub aspect_ratio: f64,
266    /// Filler orientation factor (1 = aligned, 2/3 = random 3D, 1/2 = random 2D).
267    pub orientation_factor: f64,
268}
269
270impl PercolationThreshold {
271    /// Creates a new percolation threshold estimator.
272    ///
273    /// * `aspect_ratio` – L/D of the CNT or fiber.
274    /// * `orientation_factor` – 1 for perfectly aligned; use `2.0/3.0` for
275    ///   random 3D orientation.
276    pub fn new(aspect_ratio: f64, orientation_factor: f64) -> Self {
277        Self {
278            aspect_ratio,
279            orientation_factor,
280        }
281    }
282
283    /// Critical volume fraction φ_c at the percolation threshold.
284    ///
285    /// Based on excluded-volume theory: φ_c ≈ 0.7 / (α * f_orient).
286    pub fn critical_volume_fraction(&self) -> f64 {
287        // Excluded volume model for slender cylinders:
288        // V_ex = (π/4) * D^2 * L  =>  φ_c ~ 1 / (aspect_ratio * orient)
289        0.7 / (self.aspect_ratio * self.orientation_factor)
290    }
291
292    /// Electrical conductivity enhancement factor above percolation threshold.
293    ///
294    /// Uses power-law scaling: σ_c / σ_m ∝ (φ - φ_c)^t, with t ≈ 2.
295    ///
296    /// Returns 0.0 below percolation threshold.
297    pub fn conductivity_factor(&self, volume_fraction: f64) -> f64 {
298        let phi_c = self.critical_volume_fraction();
299        if volume_fraction <= phi_c {
300            return 0.0;
301        }
302        let t = 2.0; // universal exponent for 3D networks
303        (volume_fraction - phi_c).powf(t)
304    }
305
306    /// Returns `true` if the given volume fraction exceeds the percolation threshold.
307    pub fn is_percolated(&self, volume_fraction: f64) -> bool {
308        volume_fraction > self.critical_volume_fraction()
309    }
310}
311
312// ─────────────────────────────────────────────────────────────────────────────
313// ThermalConductivityNano
314// ─────────────────────────────────────────────────────────────────────────────
315
316/// Effective thermal conductivity of a nanocomposite.
317///
318/// Combines the Maxwell-Garnett mixing rule with a Kapitza (interfacial
319/// thermal) resistance correction. Applicable to spherical and
320/// platelet-shaped fillers.
321#[derive(Debug, Clone)]
322pub struct ThermalConductivityNano {
323    /// Matrix thermal conductivity \[W/m/K\].
324    pub k_matrix: f64,
325    /// Filler thermal conductivity \[W/m/K\].
326    pub k_filler: f64,
327    /// Filler volume fraction.
328    pub volume_fraction: f64,
329    /// Kapitza (interfacial) resistance \[m²K/W\].  Set to 0 if negligible.
330    pub kapitza_resistance: f64,
331    /// Filler characteristic radius \[m\] (for Kapitza correction).
332    pub filler_radius_m: f64,
333}
334
335impl ThermalConductivityNano {
336    /// Creates a new thermal conductivity model.
337    pub fn new(
338        k_matrix: f64,
339        k_filler: f64,
340        volume_fraction: f64,
341        kapitza_resistance: f64,
342        filler_radius_m: f64,
343    ) -> Self {
344        Self {
345            k_matrix,
346            k_filler,
347            volume_fraction,
348            kapitza_resistance,
349            filler_radius_m,
350        }
351    }
352
353    /// Effective filler conductivity accounting for Kapitza resistance.
354    ///
355    /// k_eff_filler = k_filler / (1 + R_k * k_filler / r)
356    pub fn effective_filler_conductivity(&self) -> f64 {
357        let biot = self.kapitza_resistance * self.k_filler / self.filler_radius_m;
358        self.k_filler / (1.0 + biot)
359    }
360
361    /// Maxwell-Garnett effective thermal conductivity \[W/m/K\].
362    pub fn effective_conductivity(&self) -> f64 {
363        let km = self.k_matrix;
364        let kf = self.effective_filler_conductivity();
365        let vf = self.volume_fraction;
366        // Maxwell-Garnett: k_eff = km * [kf + 2km + 2vf(kf - km)] / [kf + 2km - vf(kf - km)]
367        km * (kf + 2.0 * km + 2.0 * vf * (kf - km)) / (kf + 2.0 * km - vf * (kf - km))
368    }
369
370    /// Enhancement ratio k_eff / k_matrix.
371    pub fn enhancement_ratio(&self) -> f64 {
372        self.effective_conductivity() / self.k_matrix
373    }
374}
375
376// ─────────────────────────────────────────────────────────────────────────────
377// NanoReinforcement
378// ─────────────────────────────────────────────────────────────────────────────
379
380/// Modulus enhancement model accounting for filler dispersion quality.
381///
382/// Poor dispersion (agglomeration) reduces the effective reinforcement.
383/// A dispersion quality factor D_q ∈ \[0, 1\] scales the theoretical gain.
384#[derive(Debug, Clone)]
385pub struct NanoReinforcement {
386    /// Base (unfilled) matrix modulus \[Pa\].
387    pub matrix_modulus: f64,
388    /// Theoretical filled modulus \[Pa\] (from Halpin-Tsai or Mori-Tanaka).
389    pub theoretical_modulus: f64,
390    /// Dispersion quality factor (1 = perfect, 0 = fully agglomerated).
391    pub dispersion_quality: f64,
392}
393
394impl NanoReinforcement {
395    /// Creates a new nano-reinforcement model.
396    pub fn new(matrix_modulus: f64, theoretical_modulus: f64, dispersion_quality: f64) -> Self {
397        let dq = dispersion_quality.clamp(0.0, 1.0);
398        Self {
399            matrix_modulus,
400            theoretical_modulus,
401            dispersion_quality: dq,
402        }
403    }
404
405    /// Effective modulus after accounting for dispersion quality \[Pa\].
406    pub fn effective_modulus(&self) -> f64 {
407        let delta = self.theoretical_modulus - self.matrix_modulus;
408        self.matrix_modulus + self.dispersion_quality * delta
409    }
410
411    /// Percentage improvement relative to neat matrix.
412    pub fn improvement_percent(&self) -> f64 {
413        (self.effective_modulus() / self.matrix_modulus - 1.0) * 100.0
414    }
415}
416
417// ─────────────────────────────────────────────────────────────────────────────
418// SurfaceFunctionalization
419// ─────────────────────────────────────────────────────────────────────────────
420
421/// Effect of surface functionalization on interfacial shear strength (IFSS).
422///
423/// Surface treatment (oxidation, silane coupling, etc.) improves wettability
424/// and load transfer at the filler-matrix interface.
425#[derive(Debug, Clone)]
426pub struct SurfaceFunctionalization {
427    /// Baseline IFSS without functionalization \[Pa\].
428    pub baseline_ifss: f64,
429    /// Enhancement factor due to functionalization (≥ 1.0).
430    pub enhancement_factor: f64,
431    /// Surface coverage fraction (0–1) of functional groups.
432    pub surface_coverage: f64,
433}
434
435impl SurfaceFunctionalization {
436    /// Creates a new surface functionalization model.
437    pub fn new(baseline_ifss: f64, enhancement_factor: f64, surface_coverage: f64) -> Self {
438        Self {
439            baseline_ifss,
440            enhancement_factor,
441            surface_coverage: surface_coverage.clamp(0.0, 1.0),
442        }
443    }
444
445    /// Effective IFSS after functionalization \[Pa\].
446    pub fn effective_ifss(&self) -> f64 {
447        // Partial enhancement: IFSS_eff = IFSS_0 * (1 + (factor - 1) * coverage)
448        self.baseline_ifss * (1.0 + (self.enhancement_factor - 1.0) * self.surface_coverage)
449    }
450
451    /// Critical fiber length for effective load transfer \[m\].
452    ///
453    /// l_c = (σ_f * d) / (2 * IFSS)
454    ///
455    /// * `fiber_strength` – tensile strength of the filler \[Pa\].
456    /// * `fiber_diameter` – diameter \[m\].
457    pub fn critical_length(&self, fiber_strength: f64, fiber_diameter: f64) -> f64 {
458        (fiber_strength * fiber_diameter) / (2.0 * self.effective_ifss())
459    }
460}
461
462// ─────────────────────────────────────────────────────────────────────────────
463// NanoparticleSize
464// ─────────────────────────────────────────────────────────────────────────────
465
466/// Size-dependent strengthening and surface effects for nanoparticles.
467///
468/// Captures the Hall-Petch grain-boundary strengthening and the increased
469/// surface-to-volume ratio that governs surface energy and reactivity.
470#[derive(Debug, Clone)]
471pub struct NanoparticleSize {
472    /// Particle diameter \[m\].
473    pub diameter_m: f64,
474    /// Hall-Petch coefficient k_HP \[Pa·√m\].
475    pub hall_petch_k: f64,
476    /// Lattice friction stress σ₀ \[Pa\].
477    pub sigma_0: f64,
478}
479
480impl NanoparticleSize {
481    /// Creates a new nanoparticle size model.
482    pub fn new(diameter_m: f64, hall_petch_k: f64, sigma_0: f64) -> Self {
483        Self {
484            diameter_m,
485            hall_petch_k,
486            sigma_0,
487        }
488    }
489
490    /// Hall-Petch yield stress contribution \[Pa\].
491    ///
492    /// σ_y = σ_0 + k_HP / √d
493    pub fn hall_petch_strength(&self) -> f64 {
494        self.sigma_0 + self.hall_petch_k / self.diameter_m.sqrt()
495    }
496
497    /// Surface-to-volume ratio \[1/m\].
498    ///
499    /// For a sphere: S/V = 6 / d.
500    pub fn surface_to_volume_ratio(&self) -> f64 {
501        6.0 / self.diameter_m
502    }
503
504    /// Fraction of atoms on the surface (approximate).
505    ///
506    /// Roughly proportional to (a / d) where a is the lattice parameter.
507    pub fn surface_atom_fraction(&self, lattice_param_m: f64) -> f64 {
508        (lattice_param_m / self.diameter_m).min(1.0)
509    }
510}
511
512// ─────────────────────────────────────────────────────────────────────────────
513// CntProperties
514// ─────────────────────────────────────────────────────────────────────────────
515
516/// Wall type of a carbon nanotube.
517#[derive(Debug, Clone, Copy, PartialEq)]
518pub enum CntWallType {
519    /// Single-walled CNT.
520    SingleWalled,
521    /// Multi-walled CNT.
522    MultiWalled,
523}
524
525/// Mechanical and thermal properties of carbon nanotubes.
526///
527/// Covers both SWCNT and MWCNT variants with literature-based defaults.
528#[derive(Debug, Clone)]
529pub struct CntProperties {
530    /// Wall type.
531    pub wall_type: CntWallType,
532    /// Outer diameter \[nm\].
533    pub outer_diameter_nm: f64,
534    /// Length \[µm\].
535    pub length_um: f64,
536    /// Young's modulus \[TPa\].
537    pub youngs_modulus_tpa: f64,
538    /// Tensile strength \[GPa\].
539    pub tensile_strength_gpa: f64,
540    /// Axial thermal conductivity \[W/m/K\].
541    pub thermal_conductivity_axial: f64,
542    /// Electrical conductivity \[S/m\].
543    pub electrical_conductivity: f64,
544}
545
546impl CntProperties {
547    /// Creates a default SWCNT (diameter ≈ 1 nm, length 1 µm).
548    pub fn swcnt_default() -> Self {
549        Self {
550            wall_type: CntWallType::SingleWalled,
551            outer_diameter_nm: 1.0,
552            length_um: 1.0,
553            youngs_modulus_tpa: 1.0,
554            tensile_strength_gpa: 130.0,
555            thermal_conductivity_axial: 3500.0,
556            electrical_conductivity: 1.0e7,
557        }
558    }
559
560    /// Creates a default MWCNT (outer diameter ≈ 10 nm, length 10 µm).
561    pub fn mwcnt_default() -> Self {
562        Self {
563            wall_type: CntWallType::MultiWalled,
564            outer_diameter_nm: 10.0,
565            length_um: 10.0,
566            youngs_modulus_tpa: 0.3,
567            tensile_strength_gpa: 50.0,
568            thermal_conductivity_axial: 3000.0,
569            electrical_conductivity: 5.0e6,
570        }
571    }
572
573    /// Aspect ratio (L / D).
574    pub fn aspect_ratio(&self) -> f64 {
575        // Convert µm → nm for consistent units.
576        (self.length_um * 1000.0) / self.outer_diameter_nm
577    }
578
579    /// Axial stiffness k = EA / L \[N/m\].
580    ///
581    /// Approximates cross-section as a hollow cylinder wall of thickness 0.34 nm.
582    pub fn axial_stiffness(&self) -> f64 {
583        let d = self.outer_diameter_nm * 1.0e-9;
584        let t = 0.34e-9; // graphene layer thickness
585        let area = PI * d * t;
586        let length = self.length_um * 1.0e-6;
587        (self.youngs_modulus_tpa * 1.0e12) * area / length
588    }
589}
590
591// ─────────────────────────────────────────────────────────────────────────────
592// GrapheneProperties
593// ─────────────────────────────────────────────────────────────────────────────
594
595/// Monolayer graphene and graphene-nanoplatelet properties.
596///
597/// Includes reinforcement factor calculation and in-plane stiffness.
598#[derive(Debug, Clone)]
599pub struct GrapheneProperties {
600    /// Number of layers (1 = monolayer).
601    pub num_layers: u32,
602    /// Lateral flake size \[µm\].
603    pub flake_size_um: f64,
604    /// In-plane Young's modulus \[TPa\] (≈1 TPa for monolayer).
605    pub youngs_modulus_tpa: f64,
606    /// Intrinsic tensile strength \[GPa\].
607    pub strength_gpa: f64,
608    /// Thermal conductivity of monolayer \[W/m/K\].
609    pub thermal_conductivity: f64,
610}
611
612impl GrapheneProperties {
613    /// Creates a monolayer graphene descriptor.
614    pub fn monolayer(flake_size_um: f64) -> Self {
615        Self {
616            num_layers: 1,
617            flake_size_um,
618            youngs_modulus_tpa: 1.0,
619            strength_gpa: 130.0,
620            thermal_conductivity: 5000.0,
621        }
622    }
623
624    /// Creates a graphene nanoplatelet (GNP) descriptor.
625    pub fn nanoplatelet(num_layers: u32, flake_size_um: f64) -> Self {
626        // Stiffness decreases weakly with layers due to interlayer slip.
627        let e = 1.0 / num_layers as f64 * 0.1 + 0.9; // modest reduction
628        Self {
629            num_layers,
630            flake_size_um,
631            youngs_modulus_tpa: e,
632            strength_gpa: 130.0 / num_layers as f64 * 0.5 + 50.0,
633            thermal_conductivity: 5000.0 / num_layers as f64 * 0.3 + 1000.0,
634        }
635    }
636
637    /// Aspect ratio (lateral size / thickness).
638    ///
639    /// Thickness of each layer ≈ 0.335 nm.
640    pub fn aspect_ratio(&self) -> f64 {
641        let thickness_um = self.num_layers as f64 * 0.000335; // nm → µm
642        self.flake_size_um / thickness_um
643    }
644
645    /// Reinforcement factor R_f for Young's modulus at volume fraction vf.
646    ///
647    /// Simple Cox shear-lag approximation for platelets:
648    /// R_f = 1 - tanh(α/2) / (α/2)  where α = aspect_ratio.
649    pub fn reinforcement_factor(&self) -> f64 {
650        let alpha = self.aspect_ratio().min(1000.0);
651        let half = alpha / 2.0;
652        1.0 - half.tanh() / half
653    }
654
655    /// Effective in-plane modulus contribution \[Pa\] at volume fraction vf.
656    pub fn effective_modulus_contribution(&self, volume_fraction: f64) -> f64 {
657        self.reinforcement_factor() * self.youngs_modulus_tpa * 1.0e12 * volume_fraction
658    }
659}
660
661// ─────────────────────────────────────────────────────────────────────────────
662// Tests
663// ─────────────────────────────────────────────────────────────────────────────
664
665#[cfg(test)]
666mod tests {
667    use super::*;
668
669    const EPS: f64 = 1e-9;
670
671    // ── NanoFiller ──
672
673    #[test]
674    fn test_cnt_filler_aspect_ratio() {
675        let f = NanoFiller::carbon_nanotube(500.0, 0.01);
676        assert!((f.aspect_ratio - 500.0).abs() < EPS);
677    }
678
679    #[test]
680    fn test_graphene_filler_modulus() {
681        let f = NanoFiller::graphene(1000.0, 0.02);
682        assert!((f.modulus_pa - 1.0e12).abs() < 1.0); // ~1 TPa
683    }
684
685    #[test]
686    fn test_nanoparticle_filler_creation() {
687        let f = NanoFiller::nanoparticle(70.0e9, 0.05, 3970.0);
688        assert!((f.volume_fraction - 0.05).abs() < EPS);
689        assert_eq!(f.filler_type, FillerType::Nanoparticle);
690    }
691
692    #[test]
693    fn test_weight_fraction_increases_with_volume_fraction() {
694        let f1 = NanoFiller::carbon_nanotube(100.0, 0.01);
695        let f2 = NanoFiller::carbon_nanotube(100.0, 0.05);
696        assert!(f2.weight_fraction(1200.0) > f1.weight_fraction(1200.0));
697    }
698
699    #[test]
700    fn test_weight_fraction_range() {
701        let f = NanoFiller::carbon_nanotube(100.0, 0.03);
702        let wf = f.weight_fraction(1200.0);
703        assert!(wf > 0.0 && wf < 1.0);
704    }
705
706    // ── MoriTanaka ──
707
708    #[test]
709    fn test_mori_tanaka_modulus_exceeds_matrix() {
710        let filler = NanoFiller::carbon_nanotube(100.0, 0.05);
711        let mt = MoriTanakaModel::new(3.5e9, 0.35, filler);
712        assert!(mt.longitudinal_modulus() > 3.5e9);
713    }
714
715    #[test]
716    fn test_mori_tanaka_zero_filler() {
717        let filler = NanoFiller::carbon_nanotube(100.0, 0.0);
718        let mt = MoriTanakaModel::new(3.5e9, 0.35, filler);
719        // At zero volume fraction result should be close to matrix modulus.
720        let e = mt.longitudinal_modulus();
721        assert!(e > 0.0);
722    }
723
724    #[test]
725    fn test_mori_tanaka_transverse_positive() {
726        let filler = NanoFiller::carbon_nanotube(100.0, 0.03);
727        let mt = MoriTanakaModel::new(3.5e9, 0.35, filler);
728        assert!(mt.transverse_modulus() > 0.0);
729    }
730
731    #[test]
732    fn test_mori_tanaka_shear_positive() {
733        let filler = NanoFiller::carbon_nanotube(100.0, 0.03);
734        let mt = MoriTanakaModel::new(3.5e9, 0.35, filler);
735        assert!(mt.shear_modulus() > 0.0);
736    }
737
738    #[test]
739    fn test_mori_tanaka_modulus_increases_with_vf() {
740        let em = 3.5e9;
741        let e1 = {
742            let f = NanoFiller::carbon_nanotube(200.0, 0.01);
743            MoriTanakaModel::new(em, 0.35, f).longitudinal_modulus()
744        };
745        let e2 = {
746            let f = NanoFiller::carbon_nanotube(200.0, 0.05);
747            MoriTanakaModel::new(em, 0.35, f).longitudinal_modulus()
748        };
749        assert!(e2 > e1);
750    }
751
752    // ── HalpinTsai ──
753
754    #[test]
755    fn test_halpin_tsai_transverse_exceeds_matrix() {
756        let filler = NanoFiller::carbon_nanotube(100.0, 0.05);
757        let ht = HalpinTsaiModel::new(3.5e9, filler);
758        assert!(ht.transverse_modulus() > 3.5e9);
759    }
760
761    #[test]
762    fn test_halpin_tsai_longitudinal_rule_of_mixtures() {
763        let vf = 0.10;
764        let em = 3.5e9;
765        let ef = 1.0e12;
766        let filler = NanoFiller::carbon_nanotube(500.0, vf);
767        let ht = HalpinTsaiModel::new(em, filler);
768        let expected = vf * ef + (1.0 - vf) * em;
769        let got = ht.longitudinal_modulus();
770        assert!((got - expected).abs() / expected < 1e-10);
771    }
772
773    #[test]
774    fn test_halpin_tsai_xi_longitudinal_scales_with_aspect_ratio() {
775        let f1 = NanoFiller::carbon_nanotube(50.0, 0.03);
776        let f2 = NanoFiller::carbon_nanotube(100.0, 0.03);
777        let ht1 = HalpinTsaiModel::new(3.5e9, f1);
778        let ht2 = HalpinTsaiModel::new(3.5e9, f2);
779        assert!(ht2.xi_longitudinal() > ht1.xi_longitudinal());
780    }
781
782    #[test]
783    fn test_halpin_tsai_density() {
784        let filler = NanoFiller::carbon_nanotube(100.0, 0.05);
785        let ht = HalpinTsaiModel::new(3.5e9, filler);
786        let rho = ht.composite_density(1200.0);
787        // Should be between matrix and filler density.
788        assert!(rho > 1200.0 * 0.9 && rho < 1350.0 * 1.1);
789    }
790
791    #[test]
792    fn test_halpin_tsai_ht_longitudinal_increases_with_vf() {
793        let em = 3.5e9;
794        let e1 = {
795            let f = NanoFiller::carbon_nanotube(200.0, 0.01);
796            HalpinTsaiModel::new(em, f).longitudinal_modulus_ht()
797        };
798        let e2 = {
799            let f = NanoFiller::carbon_nanotube(200.0, 0.05);
800            HalpinTsaiModel::new(em, f).longitudinal_modulus_ht()
801        };
802        assert!(e2 > e1);
803    }
804
805    // ── Percolation ──
806
807    #[test]
808    fn test_percolation_threshold_value() {
809        let p = PercolationThreshold::new(100.0, 2.0 / 3.0);
810        let phi_c = p.critical_volume_fraction();
811        assert!(phi_c > 0.0 && phi_c < 0.1);
812    }
813
814    #[test]
815    fn test_percolation_below_threshold_zero_conductivity() {
816        let p = PercolationThreshold::new(100.0, 1.0);
817        let phi_c = p.critical_volume_fraction();
818        assert!((p.conductivity_factor(phi_c * 0.5) - 0.0).abs() < EPS);
819    }
820
821    #[test]
822    fn test_percolation_above_threshold_positive() {
823        let p = PercolationThreshold::new(100.0, 1.0);
824        let phi_c = p.critical_volume_fraction();
825        assert!(p.conductivity_factor(phi_c * 2.0) > 0.0);
826    }
827
828    #[test]
829    fn test_percolation_is_percolated() {
830        let p = PercolationThreshold::new(200.0, 2.0 / 3.0);
831        let phi_c = p.critical_volume_fraction();
832        assert!(!p.is_percolated(phi_c * 0.5));
833        assert!(p.is_percolated(phi_c * 1.5));
834    }
835
836    #[test]
837    fn test_percolation_threshold_decreases_with_aspect_ratio() {
838        let p1 = PercolationThreshold::new(50.0, 1.0);
839        let p2 = PercolationThreshold::new(200.0, 1.0);
840        assert!(p2.critical_volume_fraction() < p1.critical_volume_fraction());
841    }
842
843    // ── ThermalConductivity ──
844
845    #[test]
846    fn test_thermal_conductivity_enhancement() {
847        let tc = ThermalConductivityNano::new(0.2, 3500.0, 0.03, 1e-8, 5e-9);
848        assert!(tc.effective_conductivity() > 0.2);
849    }
850
851    #[test]
852    fn test_thermal_conductivity_zero_kapitza() {
853        let tc = ThermalConductivityNano::new(0.2, 3500.0, 0.03, 0.0, 5e-9);
854        // Without Kapitza resistance, effective filler conductivity = k_filler.
855        assert!((tc.effective_filler_conductivity() - 3500.0).abs() < EPS);
856    }
857
858    #[test]
859    fn test_thermal_enhancement_ratio_gt1() {
860        let tc = ThermalConductivityNano::new(0.2, 3500.0, 0.02, 1e-8, 5e-9);
861        assert!(tc.enhancement_ratio() > 1.0);
862    }
863
864    #[test]
865    fn test_thermal_conductivity_increases_with_vf() {
866        let tc1 = ThermalConductivityNano::new(0.2, 3500.0, 0.01, 0.0, 5e-9);
867        let tc2 = ThermalConductivityNano::new(0.2, 3500.0, 0.05, 0.0, 5e-9);
868        assert!(tc2.effective_conductivity() > tc1.effective_conductivity());
869    }
870
871    #[test]
872    fn test_thermal_kapitza_reduces_conductivity() {
873        let tc_no_k = ThermalConductivityNano::new(0.2, 3500.0, 0.03, 0.0, 5e-9);
874        let tc_with_k = ThermalConductivityNano::new(0.2, 3500.0, 0.03, 1e-7, 5e-9);
875        assert!(tc_no_k.effective_conductivity() > tc_with_k.effective_conductivity());
876    }
877
878    // ── NanoReinforcement ──
879
880    #[test]
881    fn test_nano_reinforcement_effective_modulus() {
882        let nr = NanoReinforcement::new(3.5e9, 7.0e9, 1.0);
883        assert!((nr.effective_modulus() - 7.0e9).abs() < 1.0);
884    }
885
886    #[test]
887    fn test_nano_reinforcement_partial_dispersion() {
888        let nr = NanoReinforcement::new(3.5e9, 7.0e9, 0.5);
889        let expected = 3.5e9 + 0.5 * (7.0e9 - 3.5e9);
890        assert!((nr.effective_modulus() - expected).abs() < 1.0);
891    }
892
893    #[test]
894    fn test_nano_reinforcement_improvement_percent_positive() {
895        let nr = NanoReinforcement::new(3.5e9, 7.0e9, 0.8);
896        assert!(nr.improvement_percent() > 0.0);
897    }
898
899    #[test]
900    fn test_nano_reinforcement_zero_dispersion() {
901        let nr = NanoReinforcement::new(3.5e9, 7.0e9, 0.0);
902        assert!((nr.effective_modulus() - 3.5e9).abs() < 1.0);
903    }
904
905    // ── SurfaceFunctionalization ──
906
907    #[test]
908    fn test_surface_functionalization_increases_ifss() {
909        let sf = SurfaceFunctionalization::new(20.0e6, 2.0, 1.0);
910        assert!((sf.effective_ifss() - 40.0e6).abs() < 1.0);
911    }
912
913    #[test]
914    fn test_surface_functionalization_partial_coverage() {
915        let sf = SurfaceFunctionalization::new(20.0e6, 2.0, 0.5);
916        let expected = 20.0e6 * (1.0 + (2.0 - 1.0) * 0.5);
917        assert!((sf.effective_ifss() - expected).abs() < 1.0);
918    }
919
920    #[test]
921    fn test_critical_length_decreases_with_ifss() {
922        let sf1 = SurfaceFunctionalization::new(10.0e6, 1.0, 1.0);
923        let sf2 = SurfaceFunctionalization::new(20.0e6, 1.0, 1.0);
924        let l1 = sf1.critical_length(3.0e9, 10.0e-9);
925        let l2 = sf2.critical_length(3.0e9, 10.0e-9);
926        assert!(l1 > l2);
927    }
928
929    // ── NanoparticleSize ──
930
931    #[test]
932    fn test_hall_petch_increases_with_smaller_grain() {
933        let np1 = NanoparticleSize::new(100.0e-9, 0.5e6, 200.0e6);
934        let np2 = NanoparticleSize::new(10.0e-9, 0.5e6, 200.0e6);
935        assert!(np2.hall_petch_strength() > np1.hall_petch_strength());
936    }
937
938    #[test]
939    fn test_surface_to_volume_ratio_increases_with_smaller_size() {
940        let np1 = NanoparticleSize::new(100.0e-9, 0.5e6, 200.0e6);
941        let np2 = NanoparticleSize::new(10.0e-9, 0.5e6, 200.0e6);
942        assert!(np2.surface_to_volume_ratio() > np1.surface_to_volume_ratio());
943    }
944
945    #[test]
946    fn test_surface_to_volume_ratio_formula() {
947        let np = NanoparticleSize::new(10.0e-9, 0.0, 0.0);
948        let expected = 6.0 / 10.0e-9;
949        assert!((np.surface_to_volume_ratio() - expected).abs() < 1.0);
950    }
951
952    #[test]
953    fn test_surface_atom_fraction_clamped() {
954        let np = NanoparticleSize::new(0.3e-9, 0.0, 0.0);
955        let frac = np.surface_atom_fraction(0.4e-9);
956        assert!(frac <= 1.0);
957    }
958
959    // ── CntProperties ──
960
961    #[test]
962    fn test_swcnt_aspect_ratio() {
963        let cnt = CntProperties::swcnt_default(); // d=1nm, L=1µm
964        let expected = 1000.0 / 1.0;
965        assert!((cnt.aspect_ratio() - expected).abs() < 1e-6);
966    }
967
968    #[test]
969    fn test_mwcnt_aspect_ratio() {
970        let cnt = CntProperties::mwcnt_default(); // d=10nm, L=10µm
971        let expected = 10_000.0 / 10.0;
972        assert!((cnt.aspect_ratio() - expected).abs() < 1e-6);
973    }
974
975    #[test]
976    fn test_cnt_axial_stiffness_positive() {
977        let cnt = CntProperties::swcnt_default();
978        assert!(cnt.axial_stiffness() > 0.0);
979    }
980
981    #[test]
982    fn test_cnt_wall_type() {
983        assert_eq!(
984            CntProperties::swcnt_default().wall_type,
985            CntWallType::SingleWalled
986        );
987        assert_eq!(
988            CntProperties::mwcnt_default().wall_type,
989            CntWallType::MultiWalled
990        );
991    }
992
993    // ── GrapheneProperties ──
994
995    #[test]
996    fn test_graphene_monolayer_aspect_ratio() {
997        let g = GrapheneProperties::monolayer(1.0); // 1 µm flake
998        let expected = 1.0 / 0.000335;
999        assert!((g.aspect_ratio() - expected).abs() / expected < 1e-6);
1000    }
1001
1002    #[test]
1003    fn test_graphene_reinforcement_factor_between_0_1() {
1004        let g = GrapheneProperties::monolayer(5.0);
1005        let rf = g.reinforcement_factor();
1006        assert!(rf > 0.0 && rf <= 1.0);
1007    }
1008
1009    #[test]
1010    fn test_graphene_effective_modulus_positive() {
1011        let g = GrapheneProperties::monolayer(2.0);
1012        let contrib = g.effective_modulus_contribution(0.03);
1013        assert!(contrib > 0.0);
1014    }
1015
1016    #[test]
1017    fn test_graphene_nanoplatelet_more_layers_lower_modulus() {
1018        let g1 = GrapheneProperties::nanoplatelet(1, 2.0);
1019        let g2 = GrapheneProperties::nanoplatelet(10, 2.0);
1020        assert!(g1.youngs_modulus_tpa >= g2.youngs_modulus_tpa);
1021    }
1022
1023    #[test]
1024    fn test_graphene_reinforcement_large_aspect_ratio_near_one() {
1025        let g = GrapheneProperties::monolayer(100.0); // very large flake
1026        let rf = g.reinforcement_factor();
1027        assert!(
1028            rf > 0.99,
1029            "large aspect ratio should give RF near 1, got {rf}"
1030        );
1031    }
1032}