1#![allow(dead_code)]
14#![allow(clippy::too_many_arguments)]
15
16use std::f64::consts::PI;
17
18const K_B: f64 = 1.380649e-23;
24const Q_E: f64 = 1.602176634e-19;
26const M_E: f64 = 9.1093837015e-31;
28const H_PLANCK: f64 = 6.62607015e-34;
30const H_BAR: f64 = 1.054571817e-34;
32const EPSILON_0: f64 = 8.854187817e-12;
34const C_LIGHT: f64 = 2.99792458e8;
36const SIGMA_SB: f64 = 5.670374419e-8;
38
39#[derive(Clone, Copy, Debug, PartialEq)]
45pub enum BandGapType {
46 Direct,
48 Indirect,
50}
51
52#[derive(Clone, Debug)]
56pub struct BandGapModel {
57 pub eg0_ev: f64,
59 pub alpha: f64,
61 pub beta: f64,
63 pub gap_type: BandGapType,
65}
66
67impl BandGapModel {
68 pub fn new(eg0_ev: f64, alpha: f64, beta: f64, gap_type: BandGapType) -> Self {
70 Self {
71 eg0_ev,
72 alpha,
73 beta,
74 gap_type,
75 }
76 }
77
78 pub fn silicon() -> Self {
80 Self::new(1.166, 4.73e-4, 636.0, BandGapType::Indirect)
81 }
82
83 pub fn gaas() -> Self {
85 Self::new(1.519, 5.405e-4, 204.0, BandGapType::Direct)
86 }
87
88 pub fn germanium() -> Self {
90 Self::new(0.7437, 4.774e-4, 235.0, BandGapType::Indirect)
91 }
92
93 pub fn band_gap_ev(&self, t_kelvin: f64) -> f64 {
95 self.eg0_ev - self.alpha * t_kelvin * t_kelvin / (t_kelvin + self.beta)
96 }
97
98 pub fn band_gap_j(&self, t_kelvin: f64) -> f64 {
100 self.band_gap_ev(t_kelvin) * Q_E
101 }
102
103 pub fn is_direct(&self) -> bool {
105 self.gap_type == BandGapType::Direct
106 }
107}
108
109pub fn effective_dos_conduction(m_eff_ratio: f64, t_kelvin: f64) -> f64 {
117 let m_eff = m_eff_ratio * M_E;
118 2.0 * (2.0 * PI * m_eff * K_B * t_kelvin / (H_PLANCK * H_PLANCK)).powf(1.5)
119}
120
121pub fn effective_dos_valence(m_hole_ratio: f64, t_kelvin: f64) -> f64 {
125 let m_hole = m_hole_ratio * M_E;
126 2.0 * (2.0 * PI * m_hole * K_B * t_kelvin / (H_PLANCK * H_PLANCK)).powf(1.5)
127}
128
129pub fn intrinsic_carrier_concentration(nc: f64, nv: f64, eg_j: f64, t_kelvin: f64) -> f64 {
137 (nc * nv).sqrt() * (-eg_j / (2.0 * K_B * t_kelvin)).exp()
138}
139
140pub fn intrinsic_carrier_si(t_kelvin: f64) -> f64 {
142 let bg = BandGapModel::silicon();
143 let eg_j = bg.band_gap_j(t_kelvin);
144 let nc = effective_dos_conduction(1.08, t_kelvin);
146 let nv = effective_dos_valence(0.81, t_kelvin);
147 intrinsic_carrier_concentration(nc, nv, eg_j, t_kelvin)
148}
149
150pub fn extrinsic_n_type(n_d: f64, ni: f64) -> (f64, f64) {
159 let n = n_d;
160 let p = ni * ni / n;
161 (n, p)
162}
163
164pub fn extrinsic_p_type(n_a: f64, ni: f64) -> (f64, f64) {
168 let p = n_a;
169 let n = ni * ni / p;
170 (n, p)
171}
172
173pub fn extrinsic_compensated(n_d: f64, n_a: f64, ni: f64) -> (f64, f64) {
178 if n_d > n_a {
179 let n = n_d - n_a;
180 let p = ni * ni / n;
181 (n, p)
182 } else {
183 let p = n_a - n_d;
184 let n = ni * ni / p;
185 (n, p)
186 }
187}
188
189pub fn intrinsic_fermi_level_ev(eg_ev: f64, nc: f64, nv: f64, t_kelvin: f64) -> f64 {
198 let kt_ev = K_B * t_kelvin / Q_E;
199 eg_ev / 2.0 + kt_ev / 2.0 * (nv / nc).ln()
200}
201
202pub fn fermi_level_n_type_ev(eg_ev: f64, n: f64, nc: f64, t_kelvin: f64) -> f64 {
208 let kt_ev = K_B * t_kelvin / Q_E;
209 eg_ev + kt_ev * (n / nc).ln()
210}
211
212pub fn fermi_level_p_type_ev(p: f64, nv: f64, t_kelvin: f64) -> f64 {
216 let kt_ev = K_B * t_kelvin / Q_E;
217 -kt_ev * (p / nv).ln()
218}
219
220#[derive(Clone, Debug)]
228pub struct CaugheyThomasMobility {
229 pub mu_min: f64,
231 pub mu_max: f64,
233 pub n_ref: f64,
235 pub alpha: f64,
237}
238
239impl CaugheyThomasMobility {
240 pub fn new(mu_min: f64, mu_max: f64, n_ref: f64, alpha: f64) -> Self {
242 Self {
243 mu_min,
244 mu_max,
245 n_ref,
246 alpha,
247 }
248 }
249
250 pub fn si_electron() -> Self {
254 Self::new(
255 0.0065, 0.1414, 9.2e22, 0.711,
259 )
260 }
261
262 pub fn si_hole() -> Self {
264 Self::new(
265 0.00474, 0.0471, 2.23e23, 0.719,
269 )
270 }
271
272 pub fn mobility(&self, n_doping: f64) -> f64 {
276 self.mu_min + (self.mu_max - self.mu_min) / (1.0 + (n_doping / self.n_ref).powf(self.alpha))
277 }
278}
279
280pub fn high_field_mobility(mu_low: f64, e_field: f64, v_sat: f64) -> f64 {
286 mu_low / (1.0 + mu_low * e_field.abs() / v_sat)
287}
288
289pub fn drift_velocity(mobility: f64, e_field: f64) -> f64 {
291 mobility * e_field
292}
293
294pub fn conductivity(n: f64, mu_n: f64, p: f64, mu_p: f64) -> f64 {
300 Q_E * (n * mu_n + p * mu_p)
301}
302
303pub fn resistivity(n: f64, mu_n: f64, p: f64, mu_p: f64) -> f64 {
305 1.0 / conductivity(n, mu_n, p, mu_p)
306}
307
308pub fn sheet_resistance(rho: f64, thickness: f64) -> f64 {
310 rho / thickness
311}
312
313pub fn hall_coefficient_n_type(n: f64) -> f64 {
321 -1.0 / (n * Q_E)
322}
323
324pub fn hall_coefficient_p_type(p: f64) -> f64 {
328 1.0 / (p * Q_E)
329}
330
331pub fn hall_voltage(r_h: f64, i_current: f64, b_field: f64, thickness: f64) -> f64 {
336 r_h * i_current * b_field / thickness
337}
338
339pub fn hall_mobility(r_h: f64, sigma: f64) -> f64 {
341 r_h.abs() * sigma
342}
343
344pub fn hall_carrier_analysis(r_h: f64) -> (f64, bool) {
348 let is_n_type = r_h < 0.0;
349 let concentration = 1.0 / (r_h.abs() * Q_E);
350 (concentration, is_n_type)
351}
352
353pub fn debye_length(eps_r: f64, t_kelvin: f64, n_carrier: f64) -> f64 {
363 (eps_r * EPSILON_0 * K_B * t_kelvin / (Q_E * Q_E * n_carrier)).sqrt()
364}
365
366pub fn intrinsic_debye_length(eps_r: f64, t_kelvin: f64, ni: f64) -> f64 {
368 debye_length(eps_r, t_kelvin, ni)
369}
370
371#[derive(Clone, Debug)]
377pub struct PnJunction {
378 pub n_d: f64,
380 pub n_a: f64,
382 pub ni: f64,
384 pub eps_r: f64,
386 pub temperature: f64,
388 pub area: f64,
390}
391
392impl PnJunction {
393 pub fn new(n_d: f64, n_a: f64, ni: f64, eps_r: f64, temperature: f64, area: f64) -> Self {
395 Self {
396 n_d,
397 n_a,
398 ni,
399 eps_r,
400 temperature,
401 area,
402 }
403 }
404
405 pub fn silicon_default() -> Self {
407 let ni = intrinsic_carrier_si(300.0);
408 Self::new(1e22, 1e22, ni, 11.7, 300.0, 1e-6)
409 }
410
411 pub fn thermal_voltage(&self) -> f64 {
413 K_B * self.temperature / Q_E
414 }
415
416 pub fn built_in_potential(&self) -> f64 {
420 self.thermal_voltage() * (self.n_a * self.n_d / (self.ni * self.ni)).ln()
421 }
422
423 pub fn depletion_width(&self, v_r: f64) -> f64 {
427 let vbi = self.built_in_potential();
428 let eps = self.eps_r * EPSILON_0;
429 (2.0 * eps * (vbi + v_r) * (1.0 / self.n_a + 1.0 / self.n_d) / Q_E).sqrt()
430 }
431
432 pub fn depletion_width_n(&self, v_r: f64) -> f64 {
434 let w = self.depletion_width(v_r);
435 w * self.n_a / (self.n_a + self.n_d)
436 }
437
438 pub fn depletion_width_p(&self, v_r: f64) -> f64 {
440 let w = self.depletion_width(v_r);
441 w * self.n_d / (self.n_a + self.n_d)
442 }
443
444 pub fn max_electric_field(&self, v_r: f64) -> f64 {
446 let w = self.depletion_width(v_r);
447 let vbi = self.built_in_potential();
448 2.0 * (vbi + v_r) / w
449 }
450
451 pub fn junction_capacitance_per_area(&self, v_r: f64) -> f64 {
455 let eps = self.eps_r * EPSILON_0;
456 let w = self.depletion_width(v_r);
457 eps / w
458 }
459
460 pub fn junction_capacitance(&self, v_r: f64) -> f64 {
462 self.junction_capacitance_per_area(v_r) * self.area
463 }
464
465 pub fn saturation_current(&self, d_n: f64, l_n: f64, d_p: f64, l_p: f64) -> f64 {
470 self.area * Q_E * self.ni * self.ni * (d_n / (l_n * self.n_a) + d_p / (l_p * self.n_d))
471 }
472
473 pub fn diode_current(&self, voltage: f64, i0: f64) -> f64 {
475 i0 * ((voltage / self.thermal_voltage()).exp() - 1.0)
476 }
477
478 pub fn diode_current_ideality(&self, voltage: f64, i0: f64, ideality: f64) -> f64 {
480 i0 * ((voltage / (ideality * self.thermal_voltage())).exp() - 1.0)
481 }
482
483 pub fn breakdown_voltage(&self, e_crit: f64) -> f64 {
487 let n_b = self.n_a.min(self.n_d);
488 let eps = self.eps_r * EPSILON_0;
489 eps * e_crit * e_crit / (2.0 * Q_E * n_b)
490 }
491}
492
493#[derive(Clone, Debug)]
499pub struct SchottkyBarrier {
500 pub phi_m_ev: f64,
502 pub chi_s_ev: f64,
504 pub temperature: f64,
506 pub a_star: f64,
508 pub area: f64,
510 pub eps_r: f64,
512 pub n_doping: f64,
514}
515
516impl SchottkyBarrier {
517 pub fn new(
519 phi_m_ev: f64,
520 chi_s_ev: f64,
521 temperature: f64,
522 a_star: f64,
523 area: f64,
524 eps_r: f64,
525 n_doping: f64,
526 ) -> Self {
527 Self {
528 phi_m_ev,
529 chi_s_ev,
530 temperature,
531 a_star,
532 area,
533 eps_r,
534 n_doping,
535 }
536 }
537
538 pub fn barrier_height_ev(&self) -> f64 {
540 self.phi_m_ev - self.chi_s_ev
541 }
542
543 pub fn thermal_voltage(&self) -> f64 {
545 K_B * self.temperature / Q_E
546 }
547
548 pub fn saturation_current_density(&self) -> f64 {
550 let phi_b_j = self.barrier_height_ev() * Q_E;
551 self.a_star
552 * self.temperature
553 * self.temperature
554 * (-phi_b_j / (K_B * self.temperature)).exp()
555 }
556
557 pub fn saturation_current(&self) -> f64 {
559 self.saturation_current_density() * self.area
560 }
561
562 pub fn current(&self, voltage: f64) -> f64 {
566 let i0 = self.saturation_current();
567 i0 * ((voltage / self.thermal_voltage()).exp() - 1.0)
568 }
569
570 pub fn depletion_width(&self, v_r: f64) -> f64 {
572 let phi_b = self.barrier_height_ev() * Q_E / Q_E; let eps = self.eps_r * EPSILON_0;
574 (2.0 * eps * (phi_b + v_r) / (Q_E * self.n_doping)).sqrt()
575 }
576
577 pub fn capacitance_per_area(&self, v_r: f64) -> f64 {
579 let eps = self.eps_r * EPSILON_0;
580 eps / self.depletion_width(v_r)
581 }
582
583 pub fn barrier_lowering_ev(&self, e_max: f64) -> f64 {
587 let eps = self.eps_r * EPSILON_0;
588 (Q_E * e_max / (4.0 * PI * eps)).sqrt() / Q_E
589 }
590}
591
592#[derive(Clone, Debug)]
598pub struct ShockleyQueisser {
599 pub eg_ev: f64,
601 pub temperature: f64,
603 pub t_sun: f64,
605 pub concentration: f64,
607}
608
609impl ShockleyQueisser {
610 pub fn new(eg_ev: f64, temperature: f64) -> Self {
612 Self {
613 eg_ev,
614 temperature,
615 t_sun: 5778.0,
616 concentration: 1.0,
617 }
618 }
619
620 pub fn with_concentration(mut self, c: f64) -> Self {
622 self.concentration = c;
623 self
624 }
625
626 pub fn thermal_voltage(&self) -> f64 {
628 K_B * self.temperature / Q_E
629 }
630
631 pub fn short_circuit_current_density(&self) -> f64 {
637 let eg_j = self.eg_ev * Q_E;
638 let kt_sun = K_B * self.t_sun;
639 let x = eg_j / kt_sun;
640 let prefactor = 2.0 * PI / (C_LIGHT * C_LIGHT * H_PLANCK * H_PLANCK * H_PLANCK);
643 let flux = prefactor * kt_sun * kt_sun * kt_sun * (x * x + 2.0 * x + 2.0) * (-x).exp();
644 let geometric = 2.18e-5 * self.concentration;
646 Q_E * flux * geometric
647 }
648
649 pub fn open_circuit_voltage(&self) -> f64 {
654 let jsc = self.short_circuit_current_density();
655 let vt = self.thermal_voltage();
656 let j0 = self.radiative_saturation_current();
658 if j0 <= 0.0 || jsc <= 0.0 {
659 return 0.0;
660 }
661 vt * (jsc / j0 + 1.0).ln()
662 }
663
664 pub fn radiative_saturation_current(&self) -> f64 {
666 let eg_j = self.eg_ev * Q_E;
667 let kt = K_B * self.temperature;
668 let x = eg_j / kt;
669 let prefactor = 2.0 * PI * Q_E / (C_LIGHT * C_LIGHT * H_PLANCK * H_PLANCK * H_PLANCK);
670 prefactor * kt * kt * kt * (x * x + 2.0 * x + 2.0) * (-x).exp()
671 }
672
673 pub fn fill_factor(&self) -> f64 {
678 let voc = self.open_circuit_voltage();
679 let vt = self.thermal_voltage();
680 let v_norm = voc / vt;
681 if v_norm <= 0.0 {
682 return 0.0;
683 }
684 (v_norm - (v_norm + 0.72).ln()) / (v_norm + 1.0)
685 }
686
687 pub fn efficiency(&self) -> f64 {
692 let jsc = self.short_circuit_current_density();
693 let voc = self.open_circuit_voltage();
694 let ff = self.fill_factor();
695 let geometric = 2.18e-5 * self.concentration;
696 let p_in = SIGMA_SB * self.t_sun.powi(4) * geometric;
697 if p_in <= 0.0 {
698 return 0.0;
699 }
700 jsc * voc * ff / p_in
701 }
702
703 pub fn max_power_density(&self) -> f64 {
705 self.short_circuit_current_density() * self.open_circuit_voltage() * self.fill_factor()
706 }
707}
708
709#[derive(Clone, Debug)]
715pub struct ThermoelectricMaterial {
716 pub seebeck: f64,
718 pub sigma: f64,
720 pub kappa: f64,
722 pub temperature: f64,
724}
725
726impl ThermoelectricMaterial {
727 pub fn new(seebeck: f64, sigma: f64, kappa: f64, temperature: f64) -> Self {
729 Self {
730 seebeck,
731 sigma,
732 kappa,
733 temperature,
734 }
735 }
736
737 pub fn bi2te3() -> Self {
739 Self::new(200e-6, 1.0e5, 1.5, 300.0)
740 }
741
742 pub fn zt(&self) -> f64 {
744 self.seebeck * self.seebeck * self.sigma * self.temperature / self.kappa
745 }
746
747 pub fn power_factor(&self) -> f64 {
749 self.seebeck * self.seebeck * self.sigma
750 }
751
752 pub fn peltier_coefficient(&self) -> f64 {
754 self.seebeck * self.temperature
755 }
756
757 pub fn thomson_coefficient(s1: f64, t1: f64, s2: f64, t2: f64) -> f64 {
761 let ds_dt = (s2 - s1) / (t2 - t1);
762 let t_avg = (t1 + t2) / 2.0;
763 t_avg * ds_dt
764 }
765
766 pub fn seebeck_voltage(&self, delta_t: f64) -> f64 {
768 self.seebeck * delta_t
769 }
770
771 pub fn peltier_heat(&self, i_current: f64) -> f64 {
773 self.peltier_coefficient() * i_current
774 }
775
776 pub fn max_cooling_delta_t(&self, t_cold: f64) -> f64 {
781 0.5 * self.zt() * t_cold / (1.0 + 0.5 * self.zt())
782 }
783
784 pub fn max_cop(&self, t_hot: f64, t_cold: f64) -> f64 {
788 let zt_m = self.zt();
789 let gamma = (1.0 + zt_m).sqrt();
790 let dt = t_hot - t_cold;
791 if dt <= 0.0 {
792 return f64::INFINITY;
793 }
794 (t_cold / dt) * (gamma - t_hot / t_cold) / (gamma + 1.0)
795 }
796
797 pub fn generator_efficiency(&self, t_hot: f64, t_cold: f64) -> f64 {
801 let dt = t_hot - t_cold;
802 if t_hot <= 0.0 {
803 return 0.0;
804 }
805 let carnot = dt / t_hot;
806 let zt = self.zt();
807 let gamma = (1.0 + zt).sqrt();
808 carnot * (gamma - 1.0) / (gamma + t_cold / t_hot)
809 }
810
811 pub fn resistivity(&self) -> f64 {
813 1.0 / self.sigma
814 }
815
816 pub fn lorenz_number(&self) -> f64 {
818 self.kappa / (self.sigma * self.temperature)
819 }
820}
821
822pub fn thermoelectric_module_power(
826 seebeck_np: f64,
827 r_internal: f64,
828 r_load: f64,
829 delta_t: f64,
830) -> (f64, f64, f64) {
831 let v_oc = seebeck_np * delta_t;
832 let current = v_oc / (r_internal + r_load);
833 let voltage = current * r_load;
834 let power = current * voltage;
835 (power, voltage, current)
836}
837
838pub fn thermoelectric_max_power(seebeck_np: f64, r_internal: f64, delta_t: f64) -> f64 {
840 let v_oc = seebeck_np * delta_t;
841 v_oc * v_oc / (4.0 * r_internal)
842}
843
844pub fn einstein_diffusivity(mobility: f64, t_kelvin: f64) -> f64 {
850 mobility * K_B * t_kelvin / Q_E
851}
852
853pub fn diffusion_length(diffusivity: f64, lifetime: f64) -> f64 {
855 (diffusivity * lifetime).sqrt()
856}
857
858pub fn generation_rate(alpha: f64, photon_flux: f64, depth: f64) -> f64 {
863 alpha * photon_flux * (-alpha * depth).exp()
864}
865
866pub fn recombination_srh(n: f64, p: f64, ni: f64, tau_n: f64, tau_p: f64, n1: f64, p1: f64) -> f64 {
870 let numerator = n * p - ni * ni;
871 let denominator = tau_p * (n + n1) + tau_n * (p + p1);
872 if denominator.abs() < 1e-30 {
873 return 0.0;
874 }
875 numerator / denominator
876}
877
878pub fn recombination_auger(n: f64, p: f64, c_n: f64, c_p: f64) -> f64 {
882 c_n * n * n * p + c_p * n * p * p
883}
884
885pub fn recombination_radiative(n: f64, p: f64, ni: f64, b_coeff: f64) -> f64 {
889 b_coeff * (n * p - ni * ni)
890}
891
892pub fn effective_lifetime(tau_srh: f64, tau_auger: f64, tau_rad: f64) -> f64 {
896 1.0 / (1.0 / tau_srh + 1.0 / tau_auger + 1.0 / tau_rad)
897}
898
899pub fn absorption_direct(photon_energy_ev: f64, eg_ev: f64, a_coeff: f64) -> f64 {
903 if photon_energy_ev <= eg_ev {
904 0.0
905 } else {
906 a_coeff * (photon_energy_ev - eg_ev).sqrt()
907 }
908}
909
910pub fn absorption_indirect(
914 photon_energy_ev: f64,
915 eg_ev: f64,
916 e_phonon_ev: f64,
917 b_coeff: f64,
918) -> f64 {
919 let e_abs = photon_energy_ev - eg_ev + e_phonon_ev;
921 let e_emi = photon_energy_ev - eg_ev - e_phonon_ev;
923 let mut alpha = 0.0;
924 if e_abs > 0.0 {
925 alpha += b_coeff * e_abs * e_abs / photon_energy_ev;
926 }
927 if e_emi > 0.0 {
928 alpha += b_coeff * e_emi * e_emi / photon_energy_ev;
929 }
930 alpha
931}
932
933pub fn heterojunction_built_in(
937 chi1_ev: f64,
938 chi2_ev: f64,
939 eg1_ev: f64,
940 eg2_ev: f64,
941 n_a: f64,
942 n_d: f64,
943 ni1: f64,
944 ni2: f64,
945 t_kelvin: f64,
946) -> f64 {
947 let kt_ev = K_B * t_kelvin / Q_E;
948 (chi1_ev - chi2_ev) + (eg2_ev - eg1_ev) + kt_ev * (n_a * n_d / (ni1 * ni2)).ln()
949}
950
951pub fn is_schottky_n_type(phi_m_ev: f64, chi_s_ev: f64) -> bool {
956 phi_m_ev > chi_s_ev
957}
958
959pub fn is_schottky_p_type(phi_m_ev: f64, chi_s_ev: f64, eg_ev: f64) -> bool {
963 phi_m_ev < chi_s_ev + eg_ev
964}
965
966#[cfg(test)]
971mod tests {
972 use super::*;
973
974 const TOL: f64 = 1e-6;
975
976 #[test]
978 fn test_silicon_band_gap_300k() {
979 let bg = BandGapModel::silicon();
980 let eg = bg.band_gap_ev(300.0);
981 assert!(
982 (eg - 1.12).abs() < 0.02,
983 "Si band gap at 300K should be ~1.12 eV, got {:.6}",
984 eg
985 );
986 }
987
988 #[test]
990 fn test_gaas_band_gap_0k() {
991 let bg = BandGapModel::gaas();
992 let eg = bg.band_gap_ev(0.0);
993 assert!(
994 (eg - 1.519).abs() < TOL,
995 "GaAs band gap at 0K should be 1.519 eV, got {:.6}",
996 eg
997 );
998 }
999
1000 #[test]
1002 fn test_band_gap_type() {
1003 let si = BandGapModel::silicon();
1004 let gaas = BandGapModel::gaas();
1005 assert!(!si.is_direct());
1006 assert!(gaas.is_direct());
1007 }
1008
1009 #[test]
1011 fn test_effective_dos_temperature_dependence() {
1012 let nc_300 = effective_dos_conduction(1.08, 300.0);
1013 let nc_400 = effective_dos_conduction(1.08, 400.0);
1014 assert!(nc_300 > 0.0);
1015 assert!(nc_400 > nc_300, "DOS should increase with temperature");
1016 }
1017
1018 #[test]
1020 fn test_intrinsic_carrier_si() {
1021 let ni = intrinsic_carrier_si(300.0);
1022 assert!(
1024 ni > 1e14 && ni < 1e18,
1025 "Si ni at 300K should be ~1e16 m⁻³, got {:.6e}",
1026 ni
1027 );
1028 }
1029
1030 #[test]
1032 fn test_extrinsic_n_type() {
1033 let ni = 1e16;
1034 let n_d = 1e22;
1035 let (n, p) = extrinsic_n_type(n_d, ni);
1036 assert!((n - n_d).abs() < 1.0);
1037 assert!(p < n * 1e-6, "Minority carrier p should be << n");
1038 }
1039
1040 #[test]
1042 fn test_extrinsic_p_type() {
1043 let ni = 1e16;
1044 let n_a = 1e22;
1045 let (n, p) = extrinsic_p_type(n_a, ni);
1046 assert!((p - n_a).abs() < 1.0);
1047 assert!(n < p * 1e-6, "Minority carrier n should be << p");
1048 }
1049
1050 #[test]
1052 fn test_caughey_thomas_mobility() {
1053 let ct = CaugheyThomasMobility::si_electron();
1054 let mu_low = ct.mobility(1e18); let mu_high = ct.mobility(1e26); assert!(
1057 mu_low > mu_high,
1058 "Low doping mobility {:.6} should exceed high doping {:.6}",
1059 mu_low,
1060 mu_high
1061 );
1062 assert!(mu_low > 0.0);
1063 assert!(mu_high > ct.mu_min - 1e-10);
1064 }
1065
1066 #[test]
1068 fn test_conductivity_positive() {
1069 let sigma = conductivity(1e22, 0.14, 1e10, 0.05);
1070 assert!(
1071 sigma > 0.0,
1072 "Conductivity must be positive, got {:.6}",
1073 sigma
1074 );
1075 }
1076
1077 #[test]
1079 fn test_resistivity_inverse_conductivity() {
1080 let sigma = conductivity(1e22, 0.14, 1e10, 0.05);
1081 let rho = resistivity(1e22, 0.14, 1e10, 0.05);
1082 assert!(
1083 (rho - 1.0 / sigma).abs() < 1e-20,
1084 "Resistivity should be inverse of conductivity"
1085 );
1086 }
1087
1088 #[test]
1090 fn test_hall_coefficient_sign() {
1091 let rh_n = hall_coefficient_n_type(1e22);
1092 let rh_p = hall_coefficient_p_type(1e22);
1093 assert!(rh_n < 0.0, "n-type Hall coefficient should be negative");
1094 assert!(rh_p > 0.0, "p-type Hall coefficient should be positive");
1095 }
1096
1097 #[test]
1099 fn test_hall_carrier_analysis() {
1100 let n = 1e22;
1101 let rh = hall_coefficient_n_type(n);
1102 let (n_recovered, is_n) = hall_carrier_analysis(rh);
1103 assert!(is_n, "Should identify as n-type");
1104 assert!(
1105 (n_recovered - n).abs() / n < 1e-6,
1106 "Recovered concentration should match"
1107 );
1108 }
1109
1110 #[test]
1112 fn test_debye_length() {
1113 let ld1 = debye_length(11.7, 300.0, 1e20);
1114 let ld2 = debye_length(11.7, 300.0, 1e22);
1115 assert!(ld1 > 0.0);
1116 assert!(
1117 ld1 > ld2,
1118 "Higher carrier → shorter Debye length: {:.6e} vs {:.6e}",
1119 ld1,
1120 ld2
1121 );
1122 }
1123
1124 #[test]
1126 fn test_pn_junction_built_in() {
1127 let pn = PnJunction::silicon_default();
1128 let vbi = pn.built_in_potential();
1129 assert!(
1130 vbi > 0.0 && vbi < 1.5,
1131 "Si PN junction Vbi should be 0.5-0.9 V, got {:.6}",
1132 vbi
1133 );
1134 }
1135
1136 #[test]
1138 fn test_pn_depletion_width_bias() {
1139 let pn = PnJunction::silicon_default();
1140 let w0 = pn.depletion_width(0.0);
1141 let w5 = pn.depletion_width(5.0);
1142 assert!(
1143 w5 > w0,
1144 "Depletion width should increase with reverse bias: {:.6e} > {:.6e}",
1145 w5,
1146 w0
1147 );
1148 }
1149
1150 #[test]
1152 fn test_pn_capacitance_bias() {
1153 let pn = PnJunction::silicon_default();
1154 let c0 = pn.junction_capacitance(0.0);
1155 let c5 = pn.junction_capacitance(5.0);
1156 assert!(c0 > c5, "Capacitance should decrease with reverse bias");
1157 }
1158
1159 #[test]
1161 fn test_diode_current() {
1162 let pn = PnJunction::silicon_default();
1163 let i0 = 1e-12;
1164 let i_fwd = pn.diode_current(0.6, i0);
1165 let i_rev = pn.diode_current(-5.0, i0);
1166 assert!(i_fwd > 0.0, "Forward current must be positive");
1167 assert!(
1168 (i_rev + i0).abs() < i0 * 0.01,
1169 "Reverse current should approach -I0"
1170 );
1171 }
1172
1173 #[test]
1175 fn test_schottky_barrier_height() {
1176 let sb = SchottkyBarrier::new(4.8, 4.05, 300.0, 1.12e6, 1e-6, 11.7, 1e22);
1177 let phi_b = sb.barrier_height_ev();
1178 assert!(
1179 (phi_b - 0.75).abs() < 0.01,
1180 "Barrier height should be ~0.75 eV, got {:.6}",
1181 phi_b
1182 );
1183 }
1184
1185 #[test]
1187 fn test_schottky_current_exponential() {
1188 let sb = SchottkyBarrier::new(4.8, 4.05, 300.0, 1.12e6, 1e-6, 11.7, 1e22);
1189 let i1 = sb.current(0.3);
1190 let i2 = sb.current(0.4);
1191 assert!(
1192 i2 > i1 * 10.0,
1193 "100 mV increase should give ~50x more current"
1194 );
1195 }
1196
1197 #[test]
1199 fn test_sq_short_circuit_current() {
1200 let sq = ShockleyQueisser::new(1.4, 300.0);
1201 let jsc = sq.short_circuit_current_density();
1202 assert!(jsc > 0.0, "Jsc must be positive, got {:.6e}", jsc);
1203 }
1204
1205 #[test]
1207 fn test_sq_efficiency_bounds() {
1208 let sq = ShockleyQueisser::new(1.34, 300.0);
1209 let eta = sq.efficiency();
1210 assert!(
1211 eta > 0.0 && eta < 1.0,
1212 "Efficiency should be between 0 and 1, got {:.6}",
1213 eta
1214 );
1215 }
1216
1217 #[test]
1219 fn test_thermoelectric_zt() {
1220 let te = ThermoelectricMaterial::bi2te3();
1221 let zt = te.zt();
1222 assert!(
1223 zt > 0.5 && zt < 1.5,
1224 "Bi2Te3 ZT at 300K should be ~0.8, got {:.6}",
1225 zt
1226 );
1227 }
1228
1229 #[test]
1231 fn test_seebeck_voltage_linear() {
1232 let te = ThermoelectricMaterial::bi2te3();
1233 let v1 = te.seebeck_voltage(10.0);
1234 let v2 = te.seebeck_voltage(20.0);
1235 assert!(
1236 (v2 - 2.0 * v1).abs() < 1e-15,
1237 "Seebeck voltage should be linear in ΔT"
1238 );
1239 }
1240
1241 #[test]
1243 fn test_peltier_coefficient() {
1244 let te = ThermoelectricMaterial::bi2te3();
1245 let pi = te.peltier_coefficient();
1246 let expected = te.seebeck * te.temperature;
1247 assert!(
1248 (pi - expected).abs() < 1e-15,
1249 "Peltier coeff should equal S*T"
1250 );
1251 }
1252
1253 #[test]
1255 fn test_einstein_relation() {
1256 let mu = 0.14; let t = 300.0;
1258 let d = einstein_diffusivity(mu, t);
1259 let expected = mu * K_B * t / Q_E;
1260 assert!(
1261 (d - expected).abs() < 1e-15,
1262 "Einstein relation mismatch: got {:.6e}, expected {:.6e}",
1263 d,
1264 expected
1265 );
1266 }
1267
1268 #[test]
1270 fn test_diffusion_length() {
1271 let d = 36e-4; let tau = 1e-6; let l = diffusion_length(d, tau);
1274 assert!(l > 0.0, "Diffusion length must be positive");
1275 let expected = (d * tau).sqrt();
1276 assert!((l - expected).abs() < 1e-15, "Diffusion length mismatch");
1277 }
1278
1279 #[test]
1281 fn test_teg_efficiency_vs_carnot() {
1282 let te = ThermoelectricMaterial::bi2te3();
1283 let t_hot = 500.0;
1284 let t_cold = 300.0;
1285 let eta = te.generator_efficiency(t_hot, t_cold);
1286 let carnot = (t_hot - t_cold) / t_hot;
1287 assert!(
1288 eta > 0.0 && eta < carnot,
1289 "TEG efficiency {:.6} should be between 0 and Carnot {:.6}",
1290 eta,
1291 carnot
1292 );
1293 }
1294
1295 #[test]
1297 fn test_thermoelectric_max_power() {
1298 let s_np = 400e-6; let r_int = 1.0; let dt = 100.0;
1301 let p_max = thermoelectric_max_power(s_np, r_int, dt);
1302 let expected = (s_np * dt).powi(2) / (4.0 * r_int);
1303 assert!((p_max - expected).abs() < 1e-15, "Max power mismatch");
1304 }
1305
1306 #[test]
1308 fn test_srh_equilibrium() {
1309 let ni = 1e16;
1310 let r = recombination_srh(ni, ni, ni, 1e-6, 1e-6, ni, ni);
1311 assert!(
1312 r.abs() < 1e-6,
1313 "SRH recombination should be ~0 at equilibrium, got {:.6e}",
1314 r
1315 );
1316 }
1317
1318 #[test]
1320 fn test_compensated_doping() {
1321 let ni = 1e16;
1322 let n_d = 1e22;
1323 let (n, p) = extrinsic_compensated(n_d, n_d - 1e18, ni);
1325 assert!(n > p, "Slight n-type excess should give n > p");
1326 }
1327}