1#![allow(dead_code)]
19#![allow(clippy::too_many_arguments)]
20
21use rand::RngExt;
22
23pub const K_B: f64 = 1.380_649e-23;
29
30pub const H_PLANCK: f64 = 6.626_070_15e-34;
32
33pub const H_BAR: f64 = H_PLANCK / (2.0 * std::f64::consts::PI);
35
36pub const E_CHARGE: f64 = 1.602_176_634e-19;
38
39pub const CC_BOND_NM: f64 = 0.142;
41
42pub const A_GRAPHENE: f64 = 0.246;
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum CntChirality {
52 Armchair,
54 Zigzag,
56 Chiral,
58}
59
60impl CntChirality {
61 pub fn is_metallic(&self) -> bool {
63 matches!(self, CntChirality::Armchair)
64 }
65}
66
67pub 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
79pub 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
86pub 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
97pub 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#[derive(Debug, Clone)]
111pub struct CntProperties {
112 pub n: u32,
114 pub m: u32,
116 pub chirality: CntChirality,
118 pub diameter_nm: f64,
120 pub chiral_angle: f64,
122 pub youngs_modulus_tpa: f64,
124 pub poisson_ratio: f64,
126 pub tensile_strength_gpa: f64,
128 pub thermal_conductivity_axial: f64,
130 pub band_gap_ev: f64,
132}
133
134impl CntProperties {
135 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 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 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 let tensile_strength_gpa = 100.0 - 5.0 / (diameter_nm + 0.5);
161
162 let thermal_conductivity_axial = 3500.0 * (1.0 - (-diameter_nm).exp());
164
165 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 pub fn wall_thickness_nm(&self) -> f64 {
190 0.34
191 }
192
193 pub fn axial_stiffness_n(&self) -> f64 {
195 let e_pa = self.youngs_modulus_tpa * 1e12; let h = self.wall_thickness_nm() * 1e-9; let d = self.diameter_nm * 1e-9; e_pa * std::f64::consts::PI * d * h
199 }
200
201 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 let i = std::f64::consts::PI * r * r * r * h;
208 e_pa * i
209 }
210
211 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#[derive(Debug, Clone, Copy)]
228pub struct GrapheneElasticTensor {
229 pub c11: f64,
231 pub c12: f64,
233 pub c66: f64,
235}
236
237impl GrapheneElasticTensor {
238 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 pub fn youngs_modulus_2d(&self) -> f64 {
250 (self.c11 * self.c11 - self.c12 * self.c12) / self.c11
251 }
252
253 pub fn youngs_modulus_3d(&self, thickness_m: f64) -> f64 {
255 self.youngs_modulus_2d() / thickness_m
256 }
257
258 pub fn poisson_ratio(&self) -> f64 {
260 self.c12 / self.c11
261 }
262
263 pub fn biaxial_modulus(&self) -> f64 {
265 self.c11 + self.c12
266 }
267
268 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 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#[derive(Debug, Clone, Copy)]
294pub struct QuantumDot {
295 pub radius_nm: f64,
297 pub electron_mass_ratio: f64,
299 pub bulk_bandgap_ev: f64,
301}
302
303impl QuantumDot {
304 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 pub fn confinement_energy_ev(&self) -> f64 {
317 let m_star = self.electron_mass_ratio * 9.109_383_7e-31; let r = self.radius_nm * 1e-9; let pi = std::f64::consts::PI;
320 H_BAR * H_BAR * pi * pi / (2.0 * m_star * r * r * E_CHARGE) }
322
323 pub fn effective_bandgap_ev(&self) -> f64 {
325 self.bulk_bandgap_ev + self.confinement_energy_ev()
326 }
327
328 pub fn emission_wavelength_nm(&self) -> f64 {
330 let e_j = self.effective_bandgap_ev() * E_CHARGE; H_PLANCK * 3e8 / e_j * 1e9 }
333
334 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#[derive(Debug, Clone)]
348pub struct Nanocomposite {
349 pub e_matrix_gpa: f64,
351 pub e_filler_gpa: f64,
353 pub nu_matrix: f64,
355 pub nu_filler: f64,
357 pub volume_fraction: f64,
359 pub aspect_ratio: f64,
361}
362
363impl Nanocomposite {
364 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 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 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 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 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 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 pub fn bounds_valid(&self) -> bool {
433 self.voigt_modulus_gpa() >= self.reuss_modulus_gpa() - 1e-9
434 }
435}
436
437#[derive(Debug, Clone, Copy)]
446pub struct NanoparticleSurface {
447 pub radius_nm: f64,
449 pub surface_energy_j_m2: f64,
451 pub cohesive_energy_j_m3: f64,
453}
454
455impl NanoparticleSurface {
456 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 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 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 pub fn sa_to_volume_ratio(&self) -> f64 {
479 self.surface_area_m2() / self.volume_m3()
480 }
481
482 pub fn total_surface_energy_j(&self) -> f64 {
484 self.surface_energy_j_m2 * self.surface_area_m2()
485 }
486
487 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 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 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#[derive(Debug, Clone, Copy)]
525pub struct HallPetchModel {
526 pub sigma_0_mpa: f64,
528 pub k_hp_mpa_sqrt_nm: f64,
530 pub d_critical_nm: f64,
532}
533
534impl HallPetchModel {
535 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 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 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 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 pub fn optimal_grain_size_nm(&self) -> f64 {
567 self.d_critical_nm
568 }
569}
570
571#[derive(Debug, Clone)]
579pub struct OliverPharr {
580 pub p_max_mn: f64,
582 pub h_max_nm: f64,
584 pub stiffness_mn_per_nm: f64,
586 pub beta: f64,
588 pub c0: f64,
592}
593
594impl OliverPharr {
595 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 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 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 pub fn contact_area_nm2(&self) -> f64 {
629 let hc = self.contact_depth_nm();
630 self.c0 * hc * hc
631 }
632
633 pub fn hardness_gpa(&self) -> f64 {
637 let a_m2 = self.contact_area_nm2() * 1e-18; let p_n = self.p_max_mn * 1e-3; p_n / (a_m2 + 1e-30) / 1e9 }
641
642 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; let s_n_per_m = self.stiffness_mn_per_nm * 1e-3 / 1e-9; 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 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#[derive(Debug, Clone, Copy)]
680pub struct FreelyJointedChain {
681 pub n_segments: u64,
683 pub b_nm: f64,
685}
686
687impl FreelyJointedChain {
688 pub fn new(n_segments: u64, b_nm: f64) -> Self {
690 Self { n_segments, b_nm }
691 }
692
693 pub fn rms_end_to_end_nm(&self) -> f64 {
695 (self.n_segments as f64).sqrt() * self.b_nm
696 }
697
698 pub fn contour_length_nm(&self) -> f64 {
700 self.n_segments as f64 * self.b_nm
701 }
702
703 pub fn radius_of_gyration_nm(&self) -> f64 {
705 self.rms_end_to_end_nm() / 6.0_f64.sqrt()
706 }
707
708 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 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 }
722}
723
724#[derive(Debug, Clone, Copy)]
726pub struct WormLikeChain {
727 pub persistence_length_nm: f64,
729 pub contour_length_nm: f64,
731}
732
733impl WormLikeChain {
734 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 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 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 }
761
762 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#[derive(Debug, Clone, Copy, PartialEq)]
774pub enum NanoparticleShape {
775 Sphere,
777 Cube,
779 Cylinder,
781 Disk,
783}
784
785pub 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
818pub 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#[derive(Debug, Clone, Copy)]
834pub struct PhononThermalModel {
835 pub kappa_bulk_w_mk: f64,
837 pub mfp_bulk_nm: f64,
839 pub v_sound_m_s: f64,
841 pub cv_j_m3_k: f64,
843}
844
845impl PhononThermalModel {
846 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 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 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 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 pub fn knudsen_number(&self, d_nm: f64) -> f64 {
885 self.mfp_bulk_nm / d_nm
886 }
887
888 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
897pub 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#[derive(Debug, Clone)]
922pub struct MwcntProperties {
923 pub n_walls: u32,
925 pub outer_diameter_nm: f64,
927 pub interlayer_spacing_nm: f64,
929 pub youngs_modulus_tpa: f64,
931}
932
933impl MwcntProperties {
934 pub fn new(outer_diameter_nm: f64, n_walls: u32) -> Self {
936 let interlayer_spacing_nm = 0.34;
937 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 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 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 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 }
966}
967
968#[derive(Debug, Clone)]
974pub struct CntBundleSample {
975 pub n_cnt: usize,
977 pub mean_modulus_tpa: f64,
979 pub std_modulus_tpa: f64,
981 pub mean_diameter_nm: f64,
983}
984
985pub 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#[derive(Debug, Clone, Copy)]
1022pub struct PolycrystallineGraphene {
1023 pub grain_size_nm: f64,
1025 pub grain_boundary_energy_j_m2: f64,
1027 pub modulus_reduction_factor: f64,
1029}
1030
1031impl PolycrystallineGraphene {
1032 pub fn new(grain_size_nm: f64, grain_boundary_energy_j_m2: f64) -> Self {
1034 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 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 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#[derive(Debug, Clone)]
1064pub struct NanomaterialSummary {
1065 pub name: String,
1067 pub youngs_modulus_gpa: f64,
1069 pub tensile_strength_gpa: f64,
1071 pub thermal_conductivity_w_mk: f64,
1073 pub characteristic_dim_nm: f64,
1075}
1076
1077impl NanomaterialSummary {
1078 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#[cfg(test)]
1095mod tests {
1096 use super::*;
1097
1098 const PI: f64 = std::f64::consts::PI;
1099
1100 #[test]
1103 fn test_cnt_diameter_armchair_10_10() {
1104 let d = cnt_diameter_nm(10, 10);
1105 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 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 assert!(cnt_is_metallic(6, 3));
1136 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 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 #[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 #[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 #[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 #[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 #[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 assert!(p.surface_energy_ratio() > 0.01);
1300 }
1301
1302 #[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 #[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 #[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); }
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 #[test]
1397 fn test_sa_to_v_sphere() {
1398 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); assert!(f <= 1.0);
1413 }
1414
1415 #[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); 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 #[test]
1440 fn test_size_dependent_modulus_bulk_limit() {
1441 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 #[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 #[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 #[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 #[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}