Skip to main content

oxiphysics_materials/
nanomaterials.rs

1// Copyright 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Nanomaterial mechanical, thermal, and quantum properties.
5//!
6//! Covers:
7//! - Carbon nanotube (CNT) mechanics for armchair, zigzag, and chiral types
8//! - Graphene 2D elastic stiffness tensor
9//! - Quantum confinement effects on band gaps and Young's modulus
10//! - Nanocomposite rule-of-mixtures (Voigt, Reuss, Halpin-Tsai)
11//! - Surface/interface energy effects at the nanoscale
12//! - Size-dependent plasticity and Hall-Petch breakdown
13//! - Nano-indentation Oliver-Pharr analysis
14//! - Molecular mechanics of polymer chains (freely-jointed and worm-like chain)
15//! - Nanoparticle surface-area-to-volume ratio
16//! - Thermal conductivity phonon mean-free-path model
17
18#![allow(dead_code)]
19#![allow(clippy::too_many_arguments)]
20
21use rand::RngExt;
22
23// ---------------------------------------------------------------------------
24// Constants
25// ---------------------------------------------------------------------------
26
27/// Boltzmann constant (J/K).
28pub const K_B: f64 = 1.380_649e-23;
29
30/// Planck constant (J·s).
31pub const H_PLANCK: f64 = 6.626_070_15e-34;
32
33/// Reduced Planck constant (J·s).
34pub const H_BAR: f64 = H_PLANCK / (2.0 * std::f64::consts::PI);
35
36/// Elementary charge (C).
37pub const E_CHARGE: f64 = 1.602_176_634e-19;
38
39/// Carbon–carbon bond length in graphene/CNT (nm).
40pub const CC_BOND_NM: f64 = 0.142;
41
42/// Graphene lattice parameter (nm).
43pub const A_GRAPHENE: f64 = 0.246;
44
45// ---------------------------------------------------------------------------
46// CNT chirality
47// ---------------------------------------------------------------------------
48
49/// Chirality type of a single-walled carbon nanotube (SWCNT).
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum CntChirality {
52    /// Armchair nanotube: indices (n, n). Always metallic.
53    Armchair,
54    /// Zigzag nanotube: indices (n, 0). Metallic when n mod 3 == 0.
55    Zigzag,
56    /// Chiral nanotube: general (n, m) with n ≠ m and m ≠ 0.
57    Chiral,
58}
59
60impl CntChirality {
61    /// Returns whether this chirality type is always metallic.
62    pub fn is_metallic(&self) -> bool {
63        matches!(self, CntChirality::Armchair)
64    }
65}
66
67// ---------------------------------------------------------------------------
68// CNT geometry
69// ---------------------------------------------------------------------------
70
71/// Diameter of an (n, m) SWCNT in nanometres.
72pub fn cnt_diameter_nm(n: u32, m: u32) -> f64 {
73    let n = n as f64;
74    let m = m as f64;
75    let a = A_GRAPHENE;
76    a * (n * n + n * m + m * m).sqrt() / std::f64::consts::PI
77}
78
79/// Chiral angle of an (n, m) SWCNT in radians.
80pub fn cnt_chiral_angle(n: u32, m: u32) -> f64 {
81    let n = n as f64;
82    let m = m as f64;
83    (3.0_f64.sqrt() * m / (2.0 * n + m)).atan()
84}
85
86/// Returns the chirality classification for (n, m) indices.
87pub fn classify_chirality(n: u32, m: u32) -> CntChirality {
88    if n == m {
89        CntChirality::Armchair
90    } else if m == 0 {
91        CntChirality::Zigzag
92    } else {
93        CntChirality::Chiral
94    }
95}
96
97/// Determines whether an (n, m) CNT is metallic.
98///
99/// Rule: metallic if `(n - m) mod 3 == 0`.
100pub fn cnt_is_metallic(n: u32, m: u32) -> bool {
101    let diff = (n as i32 - m as i32).unsigned_abs();
102    diff.is_multiple_of(3)
103}
104
105// ---------------------------------------------------------------------------
106// CNT mechanical properties
107// ---------------------------------------------------------------------------
108
109/// Mechanical and thermal properties of a single-walled CNT.
110#[derive(Debug, Clone)]
111pub struct CntProperties {
112    /// Chiral index n.
113    pub n: u32,
114    /// Chiral index m.
115    pub m: u32,
116    /// Chirality type.
117    pub chirality: CntChirality,
118    /// Diameter (nm).
119    pub diameter_nm: f64,
120    /// Chiral angle (radians).
121    pub chiral_angle: f64,
122    /// Young's modulus (TPa). Typical value ~1 TPa.
123    pub youngs_modulus_tpa: f64,
124    /// Poisson's ratio (dimensionless).
125    pub poisson_ratio: f64,
126    /// Tensile strength (GPa).
127    pub tensile_strength_gpa: f64,
128    /// Thermal conductivity along tube axis (W/m/K).
129    pub thermal_conductivity_axial: f64,
130    /// Electronic band gap (eV); 0 for metallic.
131    pub band_gap_ev: f64,
132}
133
134impl CntProperties {
135    /// Constructs CNT properties for a given (n, m) pair using empirical models.
136    ///
137    /// Young's modulus is modelled as slightly diameter-dependent based on
138    /// atomistic simulation fits: `E ≈ 1.06 - 0.02/d²` TPa.
139    pub fn new(n: u32, m: u32) -> Self {
140        let chirality = classify_chirality(n, m);
141        let diameter_nm = cnt_diameter_nm(n, m);
142        let chiral_angle = cnt_chiral_angle(n, m);
143
144        // Empirical Young's modulus (TPa) — slight curvature correction
145        let d_sq = diameter_nm * diameter_nm;
146        let youngs_modulus_tpa = (1.06 - 0.02 / (d_sq + 1e-6)).clamp(0.9, 1.10);
147
148        // Poisson ratio: armchair ~0.16, zigzag ~0.19, chiral interpolated
149        let poisson_ratio = match chirality {
150            CntChirality::Armchair => 0.16,
151            CntChirality::Zigzag => 0.19,
152            CntChirality::Chiral => {
153                let theta = chiral_angle;
154                let frac = theta / (std::f64::consts::PI / 6.0);
155                0.16 + 0.03 * frac
156            }
157        };
158
159        // Tensile strength: ~100 GPa for ideal SWCNTs
160        let tensile_strength_gpa = 100.0 - 5.0 / (diameter_nm + 0.5);
161
162        // Thermal conductivity: ballistic at small diameter, diffusive at large
163        let thermal_conductivity_axial = 3500.0 * (1.0 - (-diameter_nm).exp());
164
165        // Band gap: metallic => 0, semiconducting => ~0.9 eV/d(nm)
166        let band_gap_ev = if cnt_is_metallic(n, m) {
167            0.0
168        } else {
169            0.9 / diameter_nm
170        };
171
172        Self {
173            n,
174            m,
175            chirality,
176            diameter_nm,
177            chiral_angle,
178            youngs_modulus_tpa,
179            poisson_ratio,
180            tensile_strength_gpa,
181            thermal_conductivity_axial,
182            band_gap_ev,
183        }
184    }
185
186    /// Returns the wall thickness used in equivalent continuum models (nm).
187    ///
188    /// Standard value: 0.34 nm (graphene interlayer spacing).
189    pub fn wall_thickness_nm(&self) -> f64 {
190        0.34
191    }
192
193    /// Axial stiffness per unit length `EA` (N) of the equivalent shell.
194    pub fn axial_stiffness_n(&self) -> f64 {
195        let e_pa = self.youngs_modulus_tpa * 1e12; // Pa
196        let h = self.wall_thickness_nm() * 1e-9; // m
197        let d = self.diameter_nm * 1e-9; // m
198        e_pa * std::f64::consts::PI * d * h
199    }
200
201    /// Bending stiffness `EI` (N·m²) of the equivalent hollow cylinder.
202    pub fn bending_stiffness_nm2(&self) -> f64 {
203        let e_pa = self.youngs_modulus_tpa * 1e12;
204        let r = self.diameter_nm * 0.5e-9;
205        let h = self.wall_thickness_nm() * 1e-9;
206        // I ≈ π r³ h for thin-walled tube
207        let i = std::f64::consts::PI * r * r * r * h;
208        e_pa * i
209    }
210
211    /// Critical Euler buckling load (N) for a tube of length `length_nm`.
212    pub fn euler_buckling_load_n(&self, length_nm: f64) -> f64 {
213        let ei = self.bending_stiffness_nm2();
214        let l = length_nm * 1e-9;
215        std::f64::consts::PI * std::f64::consts::PI * ei / (l * l)
216    }
217}
218
219// ---------------------------------------------------------------------------
220// Graphene 2D elastic tensor
221// ---------------------------------------------------------------------------
222
223/// In-plane elastic stiffness tensor for monolayer graphene.
224///
225/// Graphene is a 2D hexagonal crystal with two independent elastic constants:
226/// `C₁₁` and `C₁₂`. The tensor is in Voigt notation `[C₁₁, C₁₂, C₆₆]`.
227#[derive(Debug, Clone, Copy)]
228pub struct GrapheneElasticTensor {
229    /// C₁₁ = C₂₂ component (N/m, 2D stiffness).
230    pub c11: f64,
231    /// C₁₂ component (N/m).
232    pub c12: f64,
233    /// C₆₆ = (C₁₁ − C₁₂)/2 (N/m), shear component.
234    pub c66: f64,
235}
236
237impl GrapheneElasticTensor {
238    /// Constructs the graphene elastic tensor from ab initio values.
239    ///
240    /// Reference values: C₁₁ ≈ 357 N/m, C₁₂ ≈ 60 N/m (DFT).
241    pub fn default_graphene() -> Self {
242        let c11 = 357.0;
243        let c12 = 60.0;
244        let c66 = (c11 - c12) / 2.0;
245        Self { c11, c12, c66 }
246    }
247
248    /// Returns the 2D Young's modulus (N/m) in the zigzag direction.
249    pub fn youngs_modulus_2d(&self) -> f64 {
250        (self.c11 * self.c11 - self.c12 * self.c12) / self.c11
251    }
252
253    /// Returns the 2D Young's modulus as bulk value (Pa) given thickness `h` (m).
254    pub fn youngs_modulus_3d(&self, thickness_m: f64) -> f64 {
255        self.youngs_modulus_2d() / thickness_m
256    }
257
258    /// Returns the Poisson ratio `ν = C₁₂/C₁₁`.
259    pub fn poisson_ratio(&self) -> f64 {
260        self.c12 / self.c11
261    }
262
263    /// Returns the biaxial modulus `B = C₁₁ + C₁₂` (N/m).
264    pub fn biaxial_modulus(&self) -> f64 {
265        self.c11 + self.c12
266    }
267
268    /// Computes in-plane stress vector `[σ₁, σ₂, σ₆]` (N/m) from strain `[ε₁, ε₂, γ₆]`.
269    pub fn stress_from_strain(&self, strain: [f64; 3]) -> [f64; 3] {
270        let s1 = self.c11 * strain[0] + self.c12 * strain[1];
271        let s2 = self.c12 * strain[0] + self.c11 * strain[1];
272        let s6 = self.c66 * strain[2];
273        [s1, s2, s6]
274    }
275
276    /// Computes strain from stress using the compliance tensor.
277    pub fn strain_from_stress(&self, stress: [f64; 3]) -> [f64; 3] {
278        let det = self.c11 * self.c11 - self.c12 * self.c12;
279        let e1 = (self.c11 * stress[0] - self.c12 * stress[1]) / det;
280        let e2 = (self.c11 * stress[1] - self.c12 * stress[0]) / det;
281        let g6 = stress[2] / self.c66;
282        [e1, e2, g6]
283    }
284}
285
286// ---------------------------------------------------------------------------
287// Quantum confinement
288// ---------------------------------------------------------------------------
289
290/// Quantum confinement model for a spherical quantum dot.
291///
292/// Uses the particle-in-a-sphere model for the ground-state energy shift.
293#[derive(Debug, Clone, Copy)]
294pub struct QuantumDot {
295    /// Dot radius (nm).
296    pub radius_nm: f64,
297    /// Effective electron mass (relative to free electron mass).
298    pub electron_mass_ratio: f64,
299    /// Bulk band gap of the semiconductor (eV).
300    pub bulk_bandgap_ev: f64,
301}
302
303impl QuantumDot {
304    /// Creates a new quantum dot.
305    pub fn new(radius_nm: f64, electron_mass_ratio: f64, bulk_bandgap_ev: f64) -> Self {
306        Self {
307            radius_nm,
308            electron_mass_ratio,
309            bulk_bandgap_ev,
310        }
311    }
312
313    /// Returns the quantum confinement energy shift ΔE (eV).
314    ///
315    /// Uses the Brus equation: `ΔE = ħ²π²/(2m*r²)`.
316    pub fn confinement_energy_ev(&self) -> f64 {
317        let m_star = self.electron_mass_ratio * 9.109_383_7e-31; // kg
318        let r = self.radius_nm * 1e-9; // m
319        let pi = std::f64::consts::PI;
320        H_BAR * H_BAR * pi * pi / (2.0 * m_star * r * r * E_CHARGE) // eV
321    }
322
323    /// Returns the effective band gap of the quantum dot (eV).
324    pub fn effective_bandgap_ev(&self) -> f64 {
325        self.bulk_bandgap_ev + self.confinement_energy_ev()
326    }
327
328    /// Returns the emission wavelength (nm) corresponding to the quantum dot band gap.
329    pub fn emission_wavelength_nm(&self) -> f64 {
330        let e_j = self.effective_bandgap_ev() * E_CHARGE; // J
331        H_PLANCK * 3e8 / e_j * 1e9 // nm
332    }
333
334    /// Estimates the size-dependent Young's modulus relative to bulk.
335    ///
336    /// Uses a surface-stress model: `E(r) = E_bulk * (1 + α/r)` where `α` ~ 0.1 nm.
337    pub fn size_dependent_youngs_ratio(&self, alpha_nm: f64) -> f64 {
338        1.0 + alpha_nm / (self.radius_nm + 1e-15)
339    }
340}
341
342// ---------------------------------------------------------------------------
343// Nanocomposite rule-of-mixtures
344// ---------------------------------------------------------------------------
345
346/// Nanocomposite mechanical model using rule of mixtures and Halpin-Tsai.
347#[derive(Debug, Clone)]
348pub struct Nanocomposite {
349    /// Matrix Young's modulus (GPa).
350    pub e_matrix_gpa: f64,
351    /// Filler Young's modulus (GPa).
352    pub e_filler_gpa: f64,
353    /// Matrix Poisson ratio.
354    pub nu_matrix: f64,
355    /// Filler Poisson ratio.
356    pub nu_filler: f64,
357    /// Volume fraction of filler (0–1).
358    pub volume_fraction: f64,
359    /// Filler aspect ratio (length/diameter).
360    pub aspect_ratio: f64,
361}
362
363impl Nanocomposite {
364    /// Creates a new nanocomposite.
365    pub fn new(
366        e_matrix_gpa: f64,
367        e_filler_gpa: f64,
368        nu_matrix: f64,
369        nu_filler: f64,
370        volume_fraction: f64,
371        aspect_ratio: f64,
372    ) -> Self {
373        Self {
374            e_matrix_gpa,
375            e_filler_gpa,
376            nu_matrix,
377            nu_filler,
378            volume_fraction,
379            aspect_ratio,
380        }
381    }
382
383    /// Voigt upper bound Young's modulus (GPa).
384    ///
385    /// `E_Voigt = Vf * Ef + Vm * Em`
386    pub fn voigt_modulus_gpa(&self) -> f64 {
387        let vf = self.volume_fraction;
388        let vm = 1.0 - vf;
389        vf * self.e_filler_gpa + vm * self.e_matrix_gpa
390    }
391
392    /// Reuss lower bound Young's modulus (GPa).
393    ///
394    /// `1/E_Reuss = Vf/Ef + Vm/Em`
395    pub fn reuss_modulus_gpa(&self) -> f64 {
396        let vf = self.volume_fraction;
397        let vm = 1.0 - vf;
398        1.0 / (vf / (self.e_filler_gpa + 1e-15) + vm / (self.e_matrix_gpa + 1e-15))
399    }
400
401    /// Halpin-Tsai longitudinal modulus (GPa).
402    ///
403    /// Uses parameter ξ = 2 × aspect_ratio for aligned fibers.
404    pub fn halpin_tsai_longitudinal_gpa(&self) -> f64 {
405        let xi = 2.0 * self.aspect_ratio;
406        let ef = self.e_filler_gpa;
407        let em = self.e_matrix_gpa;
408        let eta = (ef / em - 1.0) / (ef / em + xi);
409        let vf = self.volume_fraction;
410        em * (1.0 + xi * eta * vf) / (1.0 - eta * vf)
411    }
412
413    /// Halpin-Tsai transverse modulus (GPa).
414    ///
415    /// Uses ξ = 2 for transverse stiffness of discontinuous fibers.
416    pub fn halpin_tsai_transverse_gpa(&self) -> f64 {
417        let xi = 2.0;
418        let ef = self.e_filler_gpa;
419        let em = self.e_matrix_gpa;
420        let eta = (ef / em - 1.0) / (ef / em + xi);
421        let vf = self.volume_fraction;
422        em * (1.0 + xi * eta * vf) / (1.0 - eta * vf)
423    }
424
425    /// Effective Poisson ratio via rule of mixtures.
426    pub fn effective_poisson_ratio(&self) -> f64 {
427        let vf = self.volume_fraction;
428        vf * self.nu_filler + (1.0 - vf) * self.nu_matrix
429    }
430
431    /// Bounds check: returns true if Voigt ≥ Reuss (as required by theory).
432    pub fn bounds_valid(&self) -> bool {
433        self.voigt_modulus_gpa() >= self.reuss_modulus_gpa() - 1e-9
434    }
435}
436
437// ---------------------------------------------------------------------------
438// Surface/interface energy at nanoscale
439// ---------------------------------------------------------------------------
440
441/// Surface energy model for nanoparticles.
442///
443/// Accounts for the increasing importance of surface energy
444/// relative to bulk energy as particle radius decreases.
445#[derive(Debug, Clone, Copy)]
446pub struct NanoparticleSurface {
447    /// Particle radius (nm).
448    pub radius_nm: f64,
449    /// Bulk surface energy density (J/m²).
450    pub surface_energy_j_m2: f64,
451    /// Bulk cohesive energy per unit volume (J/m³).
452    pub cohesive_energy_j_m3: f64,
453}
454
455impl NanoparticleSurface {
456    /// Creates a new nanoparticle surface model.
457    pub fn new(radius_nm: f64, surface_energy_j_m2: f64, cohesive_energy_j_m3: f64) -> Self {
458        Self {
459            radius_nm,
460            surface_energy_j_m2,
461            cohesive_energy_j_m3,
462        }
463    }
464
465    /// Surface area (m²) of a spherical nanoparticle.
466    pub fn surface_area_m2(&self) -> f64 {
467        let r = self.radius_nm * 1e-9;
468        4.0 * std::f64::consts::PI * r * r
469    }
470
471    /// Volume (m³) of a spherical nanoparticle.
472    pub fn volume_m3(&self) -> f64 {
473        let r = self.radius_nm * 1e-9;
474        4.0 / 3.0 * std::f64::consts::PI * r * r * r
475    }
476
477    /// Surface-area-to-volume ratio (1/m).
478    pub fn sa_to_volume_ratio(&self) -> f64 {
479        self.surface_area_m2() / self.volume_m3()
480    }
481
482    /// Total surface energy (J).
483    pub fn total_surface_energy_j(&self) -> f64 {
484        self.surface_energy_j_m2 * self.surface_area_m2()
485    }
486
487    /// Ratio of surface energy to bulk cohesive energy (dimensionless).
488    ///
489    /// This ratio → 1 for very small particles, indicating surface dominance.
490    pub fn surface_energy_ratio(&self) -> f64 {
491        let e_surf = self.total_surface_energy_j();
492        let e_bulk = self.cohesive_energy_j_m3 * self.volume_m3();
493        e_surf / (e_bulk + 1e-30)
494    }
495
496    /// Laplace pressure inside the nanoparticle (Pa).
497    ///
498    /// `ΔP = 2γ/r` (Young-Laplace).
499    pub fn laplace_pressure_pa(&self) -> f64 {
500        let r = self.radius_nm * 1e-9;
501        2.0 * self.surface_energy_j_m2 / (r + 1e-30)
502    }
503
504    /// Size-dependent melting point suppression factor (Gibbs-Thomson effect).
505    ///
506    /// `ΔTm/Tm_bulk ≈ −4γ_sl/(ρ L d)` — returns the ratio `ΔTm/Tm_bulk`.
507    pub fn melting_point_suppression_ratio(
508        &self,
509        density_kg_m3: f64,
510        latent_heat_j_kg: f64,
511    ) -> f64 {
512        let d = 2.0 * self.radius_nm * 1e-9;
513        -4.0 * self.surface_energy_j_m2 / (density_kg_m3 * latent_heat_j_kg * d + 1e-30)
514    }
515}
516
517// ---------------------------------------------------------------------------
518// Hall-Petch and its breakdown
519// ---------------------------------------------------------------------------
520
521/// Hall-Petch and inverse Hall-Petch (breakdown at nanoscale).
522///
523/// Describes grain-size-dependent yield strength.
524#[derive(Debug, Clone, Copy)]
525pub struct HallPetchModel {
526    /// Friction stress (yield strength of a single crystal) σ₀ (MPa).
527    pub sigma_0_mpa: f64,
528    /// Hall-Petch slope k (MPa·√nm).
529    pub k_hp_mpa_sqrt_nm: f64,
530    /// Critical grain size below which inverse HP (softening) occurs (nm).
531    pub d_critical_nm: f64,
532}
533
534impl HallPetchModel {
535    /// Creates a Hall-Petch model.
536    pub fn new(sigma_0_mpa: f64, k_hp_mpa_sqrt_nm: f64, d_critical_nm: f64) -> Self {
537        Self {
538            sigma_0_mpa,
539            k_hp_mpa_sqrt_nm,
540            d_critical_nm,
541        }
542    }
543
544    /// Yield strength (MPa) using the classical Hall-Petch equation.
545    ///
546    /// `σy = σ₀ + k / √d`
547    pub fn yield_strength_classical_mpa(&self, grain_size_nm: f64) -> f64 {
548        self.sigma_0_mpa + self.k_hp_mpa_sqrt_nm / grain_size_nm.sqrt()
549    }
550
551    /// Yield strength (MPa) with inverse Hall-Petch softening for d < d_critical.
552    ///
553    /// Uses a bilinear model: classical HP for d ≥ d_c, then linear softening.
554    pub fn yield_strength_mpa(&self, grain_size_nm: f64) -> f64 {
555        if grain_size_nm >= self.d_critical_nm {
556            self.yield_strength_classical_mpa(grain_size_nm)
557        } else {
558            // Inverse HP: softening proportional to (d_c - d)
559            let sigma_at_dc = self.yield_strength_classical_mpa(self.d_critical_nm);
560            let slope = sigma_at_dc / (self.d_critical_nm * 2.0);
561            (sigma_at_dc - slope * (self.d_critical_nm - grain_size_nm)).max(0.0)
562        }
563    }
564
565    /// Grain size (nm) at which yield strength is maximised.
566    pub fn optimal_grain_size_nm(&self) -> f64 {
567        self.d_critical_nm
568    }
569}
570
571// ---------------------------------------------------------------------------
572// Oliver-Pharr nano-indentation
573// ---------------------------------------------------------------------------
574
575/// Oliver-Pharr method for nano-indentation analysis.
576///
577/// Extracts hardness and reduced modulus from load-displacement data.
578#[derive(Debug, Clone)]
579pub struct OliverPharr {
580    /// Peak load (mN).
581    pub p_max_mn: f64,
582    /// Contact depth at peak load (nm).
583    pub h_max_nm: f64,
584    /// Contact stiffness S = dP/dh at unloading (mN/nm).
585    pub stiffness_mn_per_nm: f64,
586    /// Indenter geometry constant β (1.034 for Berkovich).
587    pub beta: f64,
588    /// Indenter area function coefficient C₀ (nm²/nm²).
589    ///
590    /// The projected contact area is `A = C₀ * h_c²`.
591    pub c0: f64,
592}
593
594impl OliverPharr {
595    /// Creates an Oliver-Pharr analysis instance.
596    pub fn new(p_max_mn: f64, h_max_nm: f64, stiffness_mn_per_nm: f64, beta: f64, c0: f64) -> Self {
597        Self {
598            p_max_mn,
599            h_max_nm,
600            stiffness_mn_per_nm,
601            beta,
602            c0,
603        }
604    }
605
606    /// Returns the default Berkovich indenter instance placeholder.
607    ///
608    /// The C₀ for a perfect Berkovich tip is 24.5 (θ = 65.27°).
609    pub fn berkovich() -> Self {
610        Self {
611            p_max_mn: 1.0,
612            h_max_nm: 100.0,
613            stiffness_mn_per_nm: 0.05,
614            beta: 1.034,
615            c0: 24.5,
616        }
617    }
618
619    /// Contact depth `h_c` (nm).
620    ///
621    /// `h_c = h_max - ε * P_max / S` where `ε = 0.75` for paraboloid.
622    pub fn contact_depth_nm(&self) -> f64 {
623        let eps = 0.75;
624        self.h_max_nm - eps * self.p_max_mn / (self.stiffness_mn_per_nm + 1e-30)
625    }
626
627    /// Projected contact area `A` (nm²).
628    pub fn contact_area_nm2(&self) -> f64 {
629        let hc = self.contact_depth_nm();
630        self.c0 * hc * hc
631    }
632
633    /// Hardness H (GPa).
634    ///
635    /// `H = P_max / A`
636    pub fn hardness_gpa(&self) -> f64 {
637        let a_m2 = self.contact_area_nm2() * 1e-18; // nm² → m²
638        let p_n = self.p_max_mn * 1e-3; // mN → N
639        p_n / (a_m2 + 1e-30) / 1e9 // Pa → GPa
640    }
641
642    /// Reduced modulus E_r (GPa).
643    ///
644    /// `E_r = (√π / (2β)) * S / √A`
645    pub fn reduced_modulus_gpa(&self) -> f64 {
646        let a_nm2 = self.contact_area_nm2();
647        let sqrt_a_nm = a_nm2.sqrt() * 1e-9; // m
648        let s_n_per_m = self.stiffness_mn_per_nm * 1e-3 / 1e-9; // N/m
649        let pi = std::f64::consts::PI;
650        let er_pa = (pi.sqrt() / (2.0 * self.beta)) * s_n_per_m / (sqrt_a_nm + 1e-30);
651        er_pa / 1e9
652    }
653
654    /// Extracts the sample Young's modulus (GPa) given indenter properties.
655    ///
656    /// `1/E_r = (1-ν²)/E + (1-νi²)/Ei`
657    pub fn sample_youngs_modulus_gpa(
658        &self,
659        poisson_sample: f64,
660        indenter_modulus_gpa: f64,
661        indenter_poisson: f64,
662    ) -> f64 {
663        let er = self.reduced_modulus_gpa();
664        let inv_er = 1.0 / (er + 1e-15);
665        let inv_ei = (1.0 - indenter_poisson * indenter_poisson) / (indenter_modulus_gpa + 1e-15);
666        let inv_e = inv_er - inv_ei;
667        if inv_e < 1e-15 {
668            return 0.0;
669        }
670        (1.0 - poisson_sample * poisson_sample) / inv_e
671    }
672}
673
674// ---------------------------------------------------------------------------
675// Polymer chain molecular mechanics
676// ---------------------------------------------------------------------------
677
678/// Freely-jointed chain (FJC) model for a linear polymer.
679#[derive(Debug, Clone, Copy)]
680pub struct FreelyJointedChain {
681    /// Number of statistical segments.
682    pub n_segments: u64,
683    /// Kuhn segment length (nm).
684    pub b_nm: f64,
685}
686
687impl FreelyJointedChain {
688    /// Creates a new FJC.
689    pub fn new(n_segments: u64, b_nm: f64) -> Self {
690        Self { n_segments, b_nm }
691    }
692
693    /// End-to-end distance RMS `<R²>^(1/2)` (nm).
694    pub fn rms_end_to_end_nm(&self) -> f64 {
695        (self.n_segments as f64).sqrt() * self.b_nm
696    }
697
698    /// Contour length `L` (nm).
699    pub fn contour_length_nm(&self) -> f64 {
700        self.n_segments as f64 * self.b_nm
701    }
702
703    /// Radius of gyration `Rg` (nm).
704    pub fn radius_of_gyration_nm(&self) -> f64 {
705        self.rms_end_to_end_nm() / 6.0_f64.sqrt()
706    }
707
708    /// Entropic restoring force (pN) at extension `r_nm`.
709    ///
710    /// Uses the Langevin approximation: `f = (k_B T / b) L^{-1}(r/L)`
711    /// where `L^{-1}` is the inverse Langevin, approximated by `3x/(1-x²)`.
712    pub fn restoring_force_pn(&self, r_nm: f64, temp_k: f64) -> f64 {
713        let l = self.contour_length_nm() * 1e-9;
714        let r = (r_nm * 1e-9).min(l * 0.9999);
715        let x = r / l;
716        // Cohen's Padé approximation to inverse Langevin
717        let inv_l = x * (3.0 - x * x) / (1.0 - x * x);
718        let b = self.b_nm * 1e-9;
719        let f_n = K_B * temp_k / b * inv_l;
720        f_n * 1e12 // pN
721    }
722}
723
724/// Worm-like chain (WLC) model for semi-flexible polymers (e.g. DNA).
725#[derive(Debug, Clone, Copy)]
726pub struct WormLikeChain {
727    /// Persistence length `Lp` (nm).
728    pub persistence_length_nm: f64,
729    /// Contour length `L` (nm).
730    pub contour_length_nm: f64,
731}
732
733impl WormLikeChain {
734    /// Creates a new WLC.
735    pub fn new(persistence_length_nm: f64, contour_length_nm: f64) -> Self {
736        Self {
737            persistence_length_nm,
738            contour_length_nm,
739        }
740    }
741
742    /// End-to-end distance RMS (nm) using the WLC result.
743    pub fn rms_end_to_end_nm(&self) -> f64 {
744        let lp = self.persistence_length_nm;
745        let l = self.contour_length_nm;
746        (2.0 * lp * l * (1.0 - lp / l * (1.0 - (-l / lp).exp()))).sqrt()
747    }
748
749    /// Restoring force (pN) at extension `r_nm` (Marko-Siggia interpolation).
750    ///
751    /// `f ≈ (k_B T / Lp) [1/(4(1-x)²) − 1/4 + x]` where `x = r/L`.
752    pub fn restoring_force_pn(&self, r_nm: f64, temp_k: f64) -> f64 {
753        let l = self.contour_length_nm * 1e-9;
754        let lp = self.persistence_length_nm * 1e-9;
755        let r = (r_nm * 1e-9).min(l * 0.9999);
756        let x = r / l;
757        let factor = K_B * temp_k / lp;
758        let f_n = factor * (0.25 / ((1.0 - x) * (1.0 - x)) - 0.25 + x);
759        f_n * 1e12 // pN
760    }
761
762    /// Bending stiffness `κ = k_B T * Lp` (J·m).
763    pub fn bending_stiffness_jm(&self, temp_k: f64) -> f64 {
764        K_B * temp_k * self.persistence_length_nm * 1e-9
765    }
766}
767
768// ---------------------------------------------------------------------------
769// Nanoparticle surface-area-to-volume
770// ---------------------------------------------------------------------------
771
772/// Shape of a nanoparticle for SA/V computation.
773#[derive(Debug, Clone, Copy, PartialEq)]
774pub enum NanoparticleShape {
775    /// Spherical nanoparticle.
776    Sphere,
777    /// Cubic nanoparticle.
778    Cube,
779    /// Cylindrical nanoparticle (rod).
780    Cylinder,
781    /// Disk-shaped nanoparticle.
782    Disk,
783}
784
785/// Surface-area-to-volume ratio (nm⁻¹) for a nanoparticle.
786///
787/// - Sphere: `6/d` (d = diameter)
788/// - Cube: `6/a` (a = edge)
789/// - Cylinder: `2(r+l)/(rl)` with aspect ratio `l/d`
790/// - Disk: `2(r+h)/(rh)`
791pub fn sa_to_volume_ratio_nm(
792    shape: NanoparticleShape,
793    characteristic_dim_nm: f64,
794    aspect_ratio: f64,
795) -> f64 {
796    match shape {
797        NanoparticleShape::Sphere => {
798            let d = characteristic_dim_nm;
799            6.0 / d
800        }
801        NanoparticleShape::Cube => {
802            let a = characteristic_dim_nm;
803            6.0 / a
804        }
805        NanoparticleShape::Cylinder => {
806            let r = characteristic_dim_nm / 2.0;
807            let l = r * 2.0 * aspect_ratio;
808            (2.0 * std::f64::consts::PI * r * (r + l)) / (std::f64::consts::PI * r * r * l)
809        }
810        NanoparticleShape::Disk => {
811            let r = characteristic_dim_nm / 2.0;
812            let h = characteristic_dim_nm / aspect_ratio;
813            2.0 * (r + h) / (r * h)
814        }
815    }
816}
817
818/// Fraction of atoms on the surface of a spherical nanoparticle.
819///
820/// Approximation: `f_surf ≈ 6 * d_atom / d_particle`.
821pub fn surface_atom_fraction(particle_diameter_nm: f64, atom_diameter_nm: f64) -> f64 {
822    (6.0 * atom_diameter_nm / particle_diameter_nm).min(1.0)
823}
824
825// ---------------------------------------------------------------------------
826// Nanoscale thermal conductivity — phonon mean free path
827// ---------------------------------------------------------------------------
828
829/// Phonon-limited thermal conductivity model at the nanoscale.
830///
831/// Based on the kinetic theory expression `κ = (1/3) C_v v Λ`
832/// with a Matthiessen-rule combination of bulk and size-limited MFP.
833#[derive(Debug, Clone, Copy)]
834pub struct PhononThermalModel {
835    /// Bulk thermal conductivity (W/m/K).
836    pub kappa_bulk_w_mk: f64,
837    /// Phonon mean free path in bulk (nm).
838    pub mfp_bulk_nm: f64,
839    /// Speed of sound (group velocity) (m/s).
840    pub v_sound_m_s: f64,
841    /// Volumetric heat capacity (J/m³/K).
842    pub cv_j_m3_k: f64,
843}
844
845impl PhononThermalModel {
846    /// Creates a phonon thermal model.
847    pub fn new(kappa_bulk_w_mk: f64, mfp_bulk_nm: f64, v_sound_m_s: f64, cv_j_m3_k: f64) -> Self {
848        Self {
849            kappa_bulk_w_mk,
850            mfp_bulk_nm,
851            v_sound_m_s,
852            cv_j_m3_k,
853        }
854    }
855
856    /// Silicon-like defaults at 300 K.
857    pub fn silicon_300k() -> Self {
858        Self {
859            kappa_bulk_w_mk: 150.0,
860            mfp_bulk_nm: 300.0,
861            v_sound_m_s: 8_430.0,
862            cv_j_m3_k: 1.63e6,
863        }
864    }
865
866    /// Effective phonon MFP (nm) in a nanostructure of characteristic size `d_nm`.
867    ///
868    /// Matthiessen's rule: `1/Λ = 1/Λ_bulk + 1/d`.
869    pub fn effective_mfp_nm(&self, d_nm: f64) -> f64 {
870        1.0 / (1.0 / self.mfp_bulk_nm + 1.0 / d_nm)
871    }
872
873    /// Effective thermal conductivity (W/m/K) for a nanostructure of size `d_nm`.
874    ///
875    /// Scales as `κ_eff = κ_bulk * Λ_eff / Λ_bulk`.
876    pub fn effective_conductivity_w_mk(&self, d_nm: f64) -> f64 {
877        let lam_eff = self.effective_mfp_nm(d_nm);
878        self.kappa_bulk_w_mk * lam_eff / self.mfp_bulk_nm
879    }
880
881    /// Knudsen number `Kn = Λ/d` (dimensionless).
882    ///
883    /// `Kn >> 1`: ballistic regime; `Kn << 1`: diffusive.
884    pub fn knudsen_number(&self, d_nm: f64) -> f64 {
885        self.mfp_bulk_nm / d_nm
886    }
887
888    /// Thermal boundary (Kapitza) resistance (m²·K/W) at a grain boundary,
889    /// using the acoustic mismatch model approximation.
890    ///
891    /// `R_K ≈ 4 / (C_v v)`.
892    pub fn kapitza_resistance_m2kw(&self) -> f64 {
893        4.0 / (self.cv_j_m3_k * self.v_sound_m_s + 1e-30)
894    }
895}
896
897// ---------------------------------------------------------------------------
898// Size-dependent elastic modulus (combined model)
899// ---------------------------------------------------------------------------
900
901/// Computes a size-dependent Young's modulus (GPa) using the core-shell model.
902///
903/// `E_nano = E_bulk * (1 - f_surf) + E_surf * f_surf`
904///
905/// where `f_surf` is the surface atom fraction.
906pub fn size_dependent_modulus_gpa(
907    e_bulk_gpa: f64,
908    e_surface_gpa: f64,
909    particle_diameter_nm: f64,
910    atom_diameter_nm: f64,
911) -> f64 {
912    let f = surface_atom_fraction(particle_diameter_nm, atom_diameter_nm);
913    e_bulk_gpa * (1.0 - f) + e_surface_gpa * f
914}
915
916// ---------------------------------------------------------------------------
917// Multi-walled CNT (MWCNT) model
918// ---------------------------------------------------------------------------
919
920/// Properties of a multi-walled CNT approximated as concentric SWCNTs.
921#[derive(Debug, Clone)]
922pub struct MwcntProperties {
923    /// Number of walls.
924    pub n_walls: u32,
925    /// Outermost diameter (nm).
926    pub outer_diameter_nm: f64,
927    /// Interlayer spacing (nm), typically 0.34 nm.
928    pub interlayer_spacing_nm: f64,
929    /// Effective Young's modulus (TPa) (lower than SWCNT due to interlayer shear).
930    pub youngs_modulus_tpa: f64,
931}
932
933impl MwcntProperties {
934    /// Creates a MWCNT model from outer diameter and wall count.
935    pub fn new(outer_diameter_nm: f64, n_walls: u32) -> Self {
936        let interlayer_spacing_nm = 0.34;
937        // Effective E decreases due to interlayer coupling
938        let youngs_modulus_tpa = 0.9 - 0.05 * (n_walls as f64 - 1.0).max(0.0) / 10.0;
939        Self {
940            n_walls,
941            outer_diameter_nm,
942            interlayer_spacing_nm,
943            youngs_modulus_tpa: youngs_modulus_tpa.max(0.5),
944        }
945    }
946
947    /// Inner diameter (nm).
948    pub fn inner_diameter_nm(&self) -> f64 {
949        (self.outer_diameter_nm - 2.0 * self.n_walls as f64 * self.interlayer_spacing_nm).max(0.5)
950    }
951
952    /// Cross-sectional area of tube wall (nm²).
953    pub fn wall_area_nm2(&self) -> f64 {
954        let pi = std::f64::consts::PI;
955        let ro = self.outer_diameter_nm / 2.0;
956        let ri = self.inner_diameter_nm() / 2.0;
957        pi * (ro * ro - ri * ri)
958    }
959
960    /// Axial stiffness EA (nN) = E * A.
961    pub fn axial_stiffness_nn(&self) -> f64 {
962        let e_pa = self.youngs_modulus_tpa * 1e12;
963        let a_m2 = self.wall_area_nm2() * 1e-18;
964        e_pa * a_m2 * 1e9 // nN
965    }
966}
967
968// ---------------------------------------------------------------------------
969// Monte-Carlo sampling of CNT bundles
970// ---------------------------------------------------------------------------
971
972/// Result of Monte-Carlo sampling of nanotube orientation effects.
973#[derive(Debug, Clone)]
974pub struct CntBundleSample {
975    /// Number of CNTs sampled.
976    pub n_cnt: usize,
977    /// Mean axial Young's modulus (TPa).
978    pub mean_modulus_tpa: f64,
979    /// Standard deviation of modulus (TPa).
980    pub std_modulus_tpa: f64,
981    /// Mean diameter (nm).
982    pub mean_diameter_nm: f64,
983}
984
985/// Samples a bundle of CNTs with random (n, m) indices and returns aggregate properties.
986pub fn sample_cnt_bundle(n_samples: usize, n_max: u32) -> CntBundleSample {
987    let mut rng = rand::rng();
988    let mut moduli = Vec::with_capacity(n_samples);
989    let mut diams = Vec::with_capacity(n_samples);
990    for _ in 0..n_samples {
991        let n = rng.random_range(5..=n_max);
992        let m = rng.random_range(0..=n);
993        let cnt = CntProperties::new(n, m);
994        moduli.push(cnt.youngs_modulus_tpa);
995        diams.push(cnt.diameter_nm);
996    }
997    let n = moduli.len() as f64;
998    let mean_mod = moduli.iter().sum::<f64>() / n;
999    let std_mod = {
1000        let var = moduli
1001            .iter()
1002            .map(|&x| (x - mean_mod) * (x - mean_mod))
1003            .sum::<f64>()
1004            / n;
1005        var.sqrt()
1006    };
1007    let mean_diam = diams.iter().sum::<f64>() / n;
1008    CntBundleSample {
1009        n_cnt: n_samples,
1010        mean_modulus_tpa: mean_mod,
1011        std_modulus_tpa: std_mod,
1012        mean_diameter_nm: mean_diam,
1013    }
1014}
1015
1016// ---------------------------------------------------------------------------
1017// Graphene grain boundary model
1018// ---------------------------------------------------------------------------
1019
1020/// Poly-crystalline graphene model with grain boundary effects.
1021#[derive(Debug, Clone, Copy)]
1022pub struct PolycrystallineGraphene {
1023    /// Average grain size (nm).
1024    pub grain_size_nm: f64,
1025    /// Grain boundary energy density (J/m²).
1026    pub grain_boundary_energy_j_m2: f64,
1027    /// Reduction factor of Young's modulus due to grain boundaries.
1028    pub modulus_reduction_factor: f64,
1029}
1030
1031impl PolycrystallineGraphene {
1032    /// Creates a polycrystalline graphene model.
1033    pub fn new(grain_size_nm: f64, grain_boundary_energy_j_m2: f64) -> Self {
1034        // Larger grains → less reduction
1035        let modulus_reduction_factor = 1.0 - 0.05 * (10.0 / grain_size_nm).min(1.0);
1036        Self {
1037            grain_size_nm,
1038            grain_boundary_energy_j_m2,
1039            modulus_reduction_factor,
1040        }
1041    }
1042
1043    /// Effective Young's modulus (2D, N/m) including grain boundary reduction.
1044    pub fn effective_modulus_2d_nm(&self) -> f64 {
1045        let pristine = GrapheneElasticTensor::default_graphene();
1046        pristine.youngs_modulus_2d() * self.modulus_reduction_factor
1047    }
1048
1049    /// Grain boundary area fraction (per unit area of graphene).
1050    ///
1051    /// Approximated as `4 * δ / d_g` where δ = 0.5 nm boundary width.
1052    pub fn grain_boundary_area_fraction(&self) -> f64 {
1053        let delta_nm = 0.5;
1054        (4.0 * delta_nm / self.grain_size_nm).min(1.0)
1055    }
1056}
1057
1058// ---------------------------------------------------------------------------
1059// Summary / Report
1060// ---------------------------------------------------------------------------
1061
1062/// A compact summary of nanomaterial properties for reporting.
1063#[derive(Debug, Clone)]
1064pub struct NanomaterialSummary {
1065    /// Name of the material.
1066    pub name: String,
1067    /// Young's modulus (GPa).
1068    pub youngs_modulus_gpa: f64,
1069    /// Tensile strength (GPa).
1070    pub tensile_strength_gpa: f64,
1071    /// Thermal conductivity (W/m/K).
1072    pub thermal_conductivity_w_mk: f64,
1073    /// Characteristic dimension (nm).
1074    pub characteristic_dim_nm: f64,
1075}
1076
1077impl NanomaterialSummary {
1078    /// Creates a summary from a SWCNT.
1079    pub fn from_cnt(cnt: &CntProperties) -> Self {
1080        Self {
1081            name: format!("SWCNT({},{})", cnt.n, cnt.m),
1082            youngs_modulus_gpa: cnt.youngs_modulus_tpa * 1000.0,
1083            tensile_strength_gpa: cnt.tensile_strength_gpa,
1084            thermal_conductivity_w_mk: cnt.thermal_conductivity_axial,
1085            characteristic_dim_nm: cnt.diameter_nm,
1086        }
1087    }
1088}
1089
1090// ---------------------------------------------------------------------------
1091// Unit tests
1092// ---------------------------------------------------------------------------
1093
1094#[cfg(test)]
1095mod tests {
1096    use super::*;
1097
1098    const PI: f64 = std::f64::consts::PI;
1099
1100    // --- CNT geometry ---
1101
1102    #[test]
1103    fn test_cnt_diameter_armchair_10_10() {
1104        let d = cnt_diameter_nm(10, 10);
1105        // Expected: a * sqrt(300) / pi ≈ 0.246 * 17.32 / 3.14159 ≈ 1.356 nm
1106        assert!((d - 1.356).abs() < 0.01, "Diameter: {d}");
1107    }
1108
1109    #[test]
1110    fn test_cnt_chirality_armchair() {
1111        assert_eq!(classify_chirality(5, 5), CntChirality::Armchair);
1112        assert_eq!(classify_chirality(10, 10), CntChirality::Armchair);
1113    }
1114
1115    #[test]
1116    fn test_cnt_chirality_zigzag() {
1117        assert_eq!(classify_chirality(8, 0), CntChirality::Zigzag);
1118    }
1119
1120    #[test]
1121    fn test_cnt_chirality_chiral() {
1122        assert_eq!(classify_chirality(6, 4), CntChirality::Chiral);
1123    }
1124
1125    #[test]
1126    fn test_cnt_is_metallic_armchair() {
1127        // (n,n): n-n=0 → metallic
1128        assert!(cnt_is_metallic(5, 5));
1129        assert!(cnt_is_metallic(10, 10));
1130    }
1131
1132    #[test]
1133    fn test_cnt_is_metallic_rule() {
1134        // (6,3): 6-3=3 → metallic
1135        assert!(cnt_is_metallic(6, 3));
1136        // (7,3): 7-3=4 → not metallic
1137        assert!(!cnt_is_metallic(7, 3));
1138    }
1139
1140    #[test]
1141    fn test_cnt_chiral_angle_armchair() {
1142        let theta = cnt_chiral_angle(5, 5);
1143        // Armchair angle = 30° = pi/6
1144        assert!((theta - PI / 6.0).abs() < 1e-10);
1145    }
1146
1147    #[test]
1148    fn test_cnt_chiral_angle_zigzag() {
1149        let theta = cnt_chiral_angle(8, 0);
1150        assert!(theta.abs() < 1e-12);
1151    }
1152
1153    // --- CntProperties ---
1154
1155    #[test]
1156    fn test_cnt_properties_modulus_range() {
1157        let cnt = CntProperties::new(10, 10);
1158        assert!(cnt.youngs_modulus_tpa > 0.8 && cnt.youngs_modulus_tpa < 1.2);
1159    }
1160
1161    #[test]
1162    fn test_cnt_properties_band_gap_metallic() {
1163        let cnt = CntProperties::new(5, 5);
1164        assert!(cnt.band_gap_ev.abs() < 1e-12);
1165    }
1166
1167    #[test]
1168    fn test_cnt_properties_band_gap_semiconducting() {
1169        let cnt = CntProperties::new(7, 3);
1170        assert!(cnt.band_gap_ev > 0.0);
1171    }
1172
1173    #[test]
1174    fn test_cnt_axial_stiffness_positive() {
1175        let cnt = CntProperties::new(10, 10);
1176        assert!(cnt.axial_stiffness_n() > 0.0);
1177    }
1178
1179    #[test]
1180    fn test_cnt_bending_stiffness_positive() {
1181        let cnt = CntProperties::new(10, 10);
1182        assert!(cnt.bending_stiffness_nm2() > 0.0);
1183    }
1184
1185    #[test]
1186    fn test_cnt_euler_buckling_decreases_with_length() {
1187        let cnt = CntProperties::new(10, 10);
1188        let f1 = cnt.euler_buckling_load_n(10.0);
1189        let f2 = cnt.euler_buckling_load_n(20.0);
1190        assert!(f1 > f2);
1191    }
1192
1193    // --- Graphene ---
1194
1195    #[test]
1196    fn test_graphene_modulus_positive() {
1197        let g = GrapheneElasticTensor::default_graphene();
1198        assert!(g.youngs_modulus_2d() > 0.0);
1199    }
1200
1201    #[test]
1202    fn test_graphene_poisson_ratio() {
1203        let g = GrapheneElasticTensor::default_graphene();
1204        let nu = g.poisson_ratio();
1205        assert!(nu > 0.0 && nu < 0.5);
1206    }
1207
1208    #[test]
1209    fn test_graphene_stress_strain_roundtrip() {
1210        let g = GrapheneElasticTensor::default_graphene();
1211        let strain = [1e-3, 0.5e-3, 0.2e-3];
1212        let stress = g.stress_from_strain(strain);
1213        let recovered = g.strain_from_stress(stress);
1214        for i in 0..3 {
1215            assert!((recovered[i] - strain[i]).abs() < 1e-15, "Mismatch at {i}");
1216        }
1217    }
1218
1219    #[test]
1220    fn test_graphene_biaxial_modulus() {
1221        let g = GrapheneElasticTensor::default_graphene();
1222        assert!(g.biaxial_modulus() > g.c11);
1223    }
1224
1225    // --- Quantum dot ---
1226
1227    #[test]
1228    fn test_quantum_dot_confinement_positive() {
1229        let qd = QuantumDot::new(3.0, 0.067, 1.42);
1230        assert!(qd.confinement_energy_ev() > 0.0);
1231    }
1232
1233    #[test]
1234    fn test_quantum_dot_bandgap_exceeds_bulk() {
1235        let qd = QuantumDot::new(3.0, 0.067, 1.42);
1236        assert!(qd.effective_bandgap_ev() > qd.bulk_bandgap_ev);
1237    }
1238
1239    #[test]
1240    fn test_quantum_dot_wavelength_positive() {
1241        let qd = QuantumDot::new(3.0, 0.067, 1.42);
1242        assert!(qd.emission_wavelength_nm() > 0.0);
1243    }
1244
1245    #[test]
1246    fn test_quantum_dot_size_modulus_larger() {
1247        let qd = QuantumDot::new(2.0, 0.067, 1.42);
1248        let ratio = qd.size_dependent_youngs_ratio(0.1);
1249        assert!(ratio > 1.0);
1250    }
1251
1252    // --- Nanocomposite ---
1253
1254    #[test]
1255    fn test_nanocomposite_voigt_bounds() {
1256        let nc = Nanocomposite::new(3.0, 1000.0, 0.35, 0.2, 0.05, 100.0);
1257        assert!(nc.voigt_modulus_gpa() > nc.reuss_modulus_gpa());
1258    }
1259
1260    #[test]
1261    fn test_nanocomposite_bounds_valid() {
1262        let nc = Nanocomposite::new(3.0, 1000.0, 0.35, 0.2, 0.05, 100.0);
1263        assert!(nc.bounds_valid());
1264    }
1265
1266    #[test]
1267    fn test_nanocomposite_halpin_tsai_between_bounds() {
1268        let nc = Nanocomposite::new(3.0, 1000.0, 0.35, 0.2, 0.05, 100.0);
1269        let ht = nc.halpin_tsai_longitudinal_gpa();
1270        assert!(ht > nc.reuss_modulus_gpa());
1271        assert!(ht < nc.voigt_modulus_gpa() + 1.0);
1272    }
1273
1274    #[test]
1275    fn test_nanocomposite_zero_vf() {
1276        let nc = Nanocomposite::new(3.0, 1000.0, 0.35, 0.2, 0.0, 100.0);
1277        assert!((nc.voigt_modulus_gpa() - 3.0).abs() < 1e-10);
1278    }
1279
1280    // --- Surface energy ---
1281
1282    #[test]
1283    fn test_surface_sa_v_ratio_increases_small_r() {
1284        let p1 = NanoparticleSurface::new(10.0, 1.0, 1e9);
1285        let p2 = NanoparticleSurface::new(1.0, 1.0, 1e9);
1286        assert!(p2.sa_to_volume_ratio() > p1.sa_to_volume_ratio());
1287    }
1288
1289    #[test]
1290    fn test_laplace_pressure_positive() {
1291        let p = NanoparticleSurface::new(5.0, 1.5, 1e9);
1292        assert!(p.laplace_pressure_pa() > 0.0);
1293    }
1294
1295    #[test]
1296    fn test_surface_energy_ratio_small_particle() {
1297        let p = NanoparticleSurface::new(0.5, 1.0, 1e9);
1298        // Should be large fraction
1299        assert!(p.surface_energy_ratio() > 0.01);
1300    }
1301
1302    // --- Hall-Petch ---
1303
1304    #[test]
1305    fn test_hall_petch_classical_increases_fine_grain() {
1306        let hp = HallPetchModel::new(100.0, 500.0, 10.0);
1307        let sy_100 = hp.yield_strength_classical_mpa(100.0);
1308        let sy_10 = hp.yield_strength_classical_mpa(10.0);
1309        assert!(sy_10 > sy_100);
1310    }
1311
1312    #[test]
1313    fn test_hall_petch_breakdown_softens() {
1314        let hp = HallPetchModel::new(100.0, 500.0, 20.0);
1315        let sy_dc = hp.yield_strength_mpa(20.0);
1316        let sy_below = hp.yield_strength_mpa(5.0);
1317        assert!(sy_below < sy_dc);
1318    }
1319
1320    #[test]
1321    fn test_hall_petch_optimal_at_critical() {
1322        let hp = HallPetchModel::new(100.0, 500.0, 20.0);
1323        assert!((hp.optimal_grain_size_nm() - 20.0).abs() < 1e-12);
1324    }
1325
1326    // --- Oliver-Pharr ---
1327
1328    #[test]
1329    fn test_oliver_pharr_contact_depth_less_than_hmax() {
1330        let op = OliverPharr::new(2.0, 150.0, 0.08, 1.034, 24.5);
1331        assert!(op.contact_depth_nm() < op.h_max_nm);
1332    }
1333
1334    #[test]
1335    fn test_oliver_pharr_hardness_positive() {
1336        let op = OliverPharr::new(2.0, 150.0, 0.08, 1.034, 24.5);
1337        assert!(op.hardness_gpa() > 0.0);
1338    }
1339
1340    #[test]
1341    fn test_oliver_pharr_reduced_modulus_positive() {
1342        let op = OliverPharr::new(2.0, 150.0, 0.08, 1.034, 24.5);
1343        assert!(op.reduced_modulus_gpa() > 0.0);
1344    }
1345
1346    #[test]
1347    fn test_oliver_pharr_sample_modulus_positive() {
1348        let op = OliverPharr::new(2.0, 150.0, 0.08, 1.034, 24.5);
1349        let e = op.sample_youngs_modulus_gpa(0.25, 1140.0, 0.07);
1350        assert!(e > 0.0, "Sample modulus: {e}");
1351    }
1352
1353    // --- Polymer chains ---
1354
1355    #[test]
1356    fn test_fjc_rms_end_to_end() {
1357        let fjc = FreelyJointedChain::new(100, 1.0);
1358        let r = fjc.rms_end_to_end_nm();
1359        assert!((r - 10.0).abs() < 1e-10); // sqrt(100) * 1.0
1360    }
1361
1362    #[test]
1363    fn test_fjc_contour_length() {
1364        let fjc = FreelyJointedChain::new(50, 2.0);
1365        assert!((fjc.contour_length_nm() - 100.0).abs() < 1e-10);
1366    }
1367
1368    #[test]
1369    fn test_fjc_restoring_force_positive() {
1370        let fjc = FreelyJointedChain::new(1000, 0.5);
1371        let f = fjc.restoring_force_pn(100.0, 300.0);
1372        assert!(f > 0.0);
1373    }
1374
1375    #[test]
1376    fn test_wlc_rms_end_to_end_positive() {
1377        let wlc = WormLikeChain::new(50.0, 1000.0);
1378        assert!(wlc.rms_end_to_end_nm() > 0.0);
1379    }
1380
1381    #[test]
1382    fn test_wlc_restoring_force_positive() {
1383        let wlc = WormLikeChain::new(50.0, 1000.0);
1384        let f = wlc.restoring_force_pn(200.0, 300.0);
1385        assert!(f > 0.0);
1386    }
1387
1388    #[test]
1389    fn test_wlc_bending_stiffness_positive() {
1390        let wlc = WormLikeChain::new(50.0, 1000.0);
1391        assert!(wlc.bending_stiffness_jm(300.0) > 0.0);
1392    }
1393
1394    // --- SA/V ratio ---
1395
1396    #[test]
1397    fn test_sa_to_v_sphere() {
1398        // 6/d for d=10 nm → 0.6 nm^{-1}
1399        let r = sa_to_volume_ratio_nm(NanoparticleShape::Sphere, 10.0, 1.0);
1400        assert!((r - 0.6).abs() < 1e-12);
1401    }
1402
1403    #[test]
1404    fn test_sa_to_v_cube() {
1405        let r = sa_to_volume_ratio_nm(NanoparticleShape::Cube, 10.0, 1.0);
1406        assert!((r - 0.6).abs() < 1e-12);
1407    }
1408
1409    #[test]
1410    fn test_surface_atom_fraction_clamped() {
1411        let f = surface_atom_fraction(0.1, 0.3); // tiny particle
1412        assert!(f <= 1.0);
1413    }
1414
1415    // --- Phonon thermal model ---
1416
1417    #[test]
1418    fn test_phonon_effective_conductivity_less_than_bulk() {
1419        let m = PhononThermalModel::silicon_300k();
1420        let k_eff = m.effective_conductivity_w_mk(30.0);
1421        assert!(k_eff < m.kappa_bulk_w_mk);
1422    }
1423
1424    #[test]
1425    fn test_phonon_knudsen_large_for_small_structure() {
1426        let m = PhononThermalModel::silicon_300k();
1427        let kn = m.knudsen_number(1.0); // 1 nm structure
1428        assert!(kn > 1.0);
1429    }
1430
1431    #[test]
1432    fn test_kapitza_resistance_positive() {
1433        let m = PhononThermalModel::silicon_300k();
1434        assert!(m.kapitza_resistance_m2kw() > 0.0);
1435    }
1436
1437    // --- Size-dependent modulus ---
1438
1439    #[test]
1440    fn test_size_dependent_modulus_bulk_limit() {
1441        // When particle is huge, f_surf → 0, so E_nano → E_bulk
1442        let e = size_dependent_modulus_gpa(200.0, 50.0, 1000.0, 0.25);
1443        assert!((e - 200.0).abs() < 1.0);
1444    }
1445
1446    // --- MWCNT ---
1447
1448    #[test]
1449    fn test_mwcnt_inner_diameter_positive() {
1450        let m = MwcntProperties::new(10.0, 3);
1451        assert!(m.inner_diameter_nm() > 0.0);
1452    }
1453
1454    #[test]
1455    fn test_mwcnt_axial_stiffness_positive() {
1456        let m = MwcntProperties::new(10.0, 3);
1457        assert!(m.axial_stiffness_nn() > 0.0);
1458    }
1459
1460    // --- Bundle sampling ---
1461
1462    #[test]
1463    fn test_cnt_bundle_sample_count() {
1464        let s = sample_cnt_bundle(50, 15);
1465        assert_eq!(s.n_cnt, 50);
1466    }
1467
1468    #[test]
1469    fn test_cnt_bundle_sample_modulus_range() {
1470        let s = sample_cnt_bundle(200, 20);
1471        assert!(s.mean_modulus_tpa > 0.8 && s.mean_modulus_tpa < 1.2);
1472    }
1473
1474    // --- Polycrystalline graphene ---
1475
1476    #[test]
1477    fn test_polycrystalline_graphene_modulus_less_than_pristine() {
1478        let pg = PolycrystallineGraphene::new(10.0, 0.5);
1479        let pristine = GrapheneElasticTensor::default_graphene().youngs_modulus_2d();
1480        assert!(pg.effective_modulus_2d_nm() < pristine + 1.0);
1481    }
1482
1483    #[test]
1484    fn test_gb_area_fraction_increases_small_grain() {
1485        let pg1 = PolycrystallineGraphene::new(100.0, 0.5);
1486        let pg2 = PolycrystallineGraphene::new(5.0, 0.5);
1487        assert!(pg2.grain_boundary_area_fraction() > pg1.grain_boundary_area_fraction());
1488    }
1489
1490    // --- Summary ---
1491
1492    #[test]
1493    fn test_nanomaterial_summary_from_cnt() {
1494        let cnt = CntProperties::new(10, 10);
1495        let summary = NanomaterialSummary::from_cnt(&cnt);
1496        assert!(summary.youngs_modulus_gpa > 0.0);
1497        assert!(!summary.name.is_empty());
1498    }
1499}