1use std::f64::consts::PI;
11
12pub const K_BOLTZMANN_EV: f64 = 8.617333262e-5;
14
15pub const K_BOLTZMANN_J: f64 = 1.380649e-23;
17
18pub const Q_ELECTRON: f64 = 1.602176634e-19;
20
21pub const EPSILON_0: f64 = 8.854187817e-12;
23
24pub const H_PLANCK: f64 = 6.62607015e-34;
26
27pub const HBAR: f64 = 1.054571817e-34;
29
30pub const M_ELECTRON: f64 = 9.1093837015e-31;
32
33pub fn thermal_voltage(temperature_k: f64) -> f64 {
35 K_BOLTZMANN_EV * temperature_k
36}
37
38#[derive(Debug, Clone, Copy)]
47pub struct BandStructure {
48 pub band_gap_ev: f64,
50 pub ec: f64,
52 pub ev: f64,
54 pub me_eff: f64,
56 pub mh_eff: f64,
58 pub dielectric_constant: f64,
60}
61
62impl BandStructure {
63 pub fn new(
65 band_gap_ev: f64,
66 electron_affinity: f64,
67 me_eff: f64,
68 mh_eff: f64,
69 dielectric_constant: f64,
70 ) -> Self {
71 Self {
72 band_gap_ev,
73 ec: -electron_affinity,
74 ev: -electron_affinity - band_gap_ev,
75 me_eff,
76 mh_eff,
77 dielectric_constant,
78 }
79 }
80
81 pub fn silicon() -> Self {
83 Self::new(1.12, 4.05, 1.08, 0.56, 11.7)
84 }
85
86 pub fn germanium() -> Self {
88 Self::new(0.66, 4.0, 0.55, 0.37, 16.0)
89 }
90
91 pub fn gallium_arsenide() -> Self {
93 Self::new(1.42, 4.07, 0.067, 0.45, 12.9)
94 }
95
96 pub fn silicon_carbide() -> Self {
98 Self::new(3.26, 3.17, 0.39, 1.0, 9.7)
99 }
100
101 pub fn gallium_nitride() -> Self {
103 Self::new(3.4, 4.1, 0.2, 0.8, 8.9)
104 }
105
106 pub fn varshni_band_gap(&self, temperature_k: f64, alpha: f64, beta: f64) -> f64 {
110 self.band_gap_ev + alpha * 300.0 * 300.0 / (300.0 + beta)
111 - alpha * temperature_k * temperature_k / (temperature_k + beta)
112 }
113
114 pub fn nc(&self, temperature_k: f64) -> f64 {
116 let factor = 2.0 * PI * self.me_eff * M_ELECTRON * K_BOLTZMANN_J * temperature_k
117 / (H_PLANCK * H_PLANCK);
118 2.0 * factor.powf(1.5) * 1e-6 }
120
121 pub fn nv(&self, temperature_k: f64) -> f64 {
123 let factor = 2.0 * PI * self.mh_eff * M_ELECTRON * K_BOLTZMANN_J * temperature_k
124 / (H_PLANCK * H_PLANCK);
125 2.0 * factor.powf(1.5) * 1e-6 }
127
128 pub fn permittivity(&self) -> f64 {
130 self.dielectric_constant * EPSILON_0
131 }
132
133 pub fn midgap_energy(&self) -> f64 {
135 (self.ec + self.ev) / 2.0
136 }
137}
138
139#[derive(Debug, Clone, Copy, PartialEq, Eq)]
145pub enum DopingType {
146 NType,
148 PType,
150 Intrinsic,
152}
153
154#[derive(Debug, Clone, Copy)]
159pub struct CarrierConcentration {
160 pub band: BandStructure,
162 pub temperature_k: f64,
164 pub nd: f64,
166 pub na: f64,
168}
169
170impl CarrierConcentration {
171 pub fn new(band: BandStructure, temperature_k: f64, nd: f64, na: f64) -> Self {
173 Self {
174 band,
175 temperature_k,
176 nd,
177 na,
178 }
179 }
180
181 pub fn intrinsic_concentration(&self) -> f64 {
185 let nc = self.band.nc(self.temperature_k);
186 let nv = self.band.nv(self.temperature_k);
187 let vt = thermal_voltage(self.temperature_k);
188 (nc * nv).sqrt() * (-self.band.band_gap_ev / (2.0 * vt)).exp()
189 }
190
191 pub fn fermi_dirac_half(eta: f64) -> f64 {
195 if eta < -5.0 {
196 (PI.sqrt() / 2.0) * eta.exp()
198 } else if eta > 5.0 {
199 (2.0 / 3.0) * eta.powf(1.5)
201 } else {
202 let exp_eta = eta.exp();
204 let denom = 1.0 + 0.27 * exp_eta;
205 (PI.sqrt() / 2.0) * exp_eta / denom.sqrt()
206 }
207 }
208
209 pub fn electron_concentration(&self) -> f64 {
211 let ni = self.intrinsic_concentration();
212 let net_donor = self.nd - self.na;
213 if net_donor > 0.0 {
214 let discriminant = net_donor * net_donor + 4.0 * ni * ni;
216 (net_donor + discriminant.sqrt()) / 2.0
217 } else if net_donor < 0.0 {
218 let net_acceptor = -net_donor;
220 let discriminant = net_acceptor * net_acceptor + 4.0 * ni * ni;
221 let p = (net_acceptor + discriminant.sqrt()) / 2.0;
222 ni * ni / p
223 } else {
224 ni
225 }
226 }
227
228 pub fn hole_concentration(&self) -> f64 {
230 let ni = self.intrinsic_concentration();
231 let n0 = self.electron_concentration();
232 ni * ni / n0
233 }
234
235 pub fn doping_type(&self) -> DopingType {
237 if (self.nd - self.na).abs() < 1e-6 * (self.nd + self.na + 1.0) {
238 DopingType::Intrinsic
239 } else if self.nd > self.na {
240 DopingType::NType
241 } else {
242 DopingType::PType
243 }
244 }
245
246 pub fn fermi_level_shift(&self) -> f64 {
250 let ni = self.intrinsic_concentration();
251 let n0 = self.electron_concentration();
252 let vt = thermal_voltage(self.temperature_k);
253 vt * (n0 / ni).ln()
254 }
255
256 pub fn compensation_ratio(&self) -> f64 {
258 let max_doping = self.nd.max(self.na);
259 if max_doping < 1e-10 {
260 return 0.0;
261 }
262 let min_doping = self.nd.min(self.na);
263 min_doping / max_doping
264 }
265
266 pub fn debye_length(&self) -> f64 {
268 let vt = thermal_voltage(self.temperature_k);
269 let n0 = self.electron_concentration();
270 let p0 = self.hole_concentration();
271 let epsilon = self.band.permittivity();
272 let total_carriers = (n0 + p0) * 1e6; let ld = (epsilon * vt / (Q_ELECTRON * total_carriers)).sqrt();
275 ld * 100.0 }
277}
278
279#[derive(Debug, Clone, Copy)]
287pub struct DriftDiffusion {
288 pub mu_n: f64,
290 pub mu_p: f64,
292 pub temperature_k: f64,
294 pub v_sat_n: f64,
296 pub v_sat_p: f64,
298}
299
300impl DriftDiffusion {
301 pub fn new(mu_n: f64, mu_p: f64, temperature_k: f64, v_sat_n: f64, v_sat_p: f64) -> Self {
303 Self {
304 mu_n,
305 mu_p,
306 temperature_k,
307 v_sat_n,
308 v_sat_p,
309 }
310 }
311
312 pub fn silicon_300k() -> Self {
314 Self::new(1400.0, 450.0, 300.0, 1.07e7, 8.37e6)
315 }
316
317 pub fn gaas_300k() -> Self {
319 Self::new(8500.0, 400.0, 300.0, 7.7e6, 7.7e6)
320 }
321
322 pub fn diffusion_coefficient_n(&self) -> f64 {
324 self.mu_n * thermal_voltage(self.temperature_k)
325 }
326
327 pub fn diffusion_coefficient_p(&self) -> f64 {
329 self.mu_p * thermal_voltage(self.temperature_k)
330 }
331
332 pub fn einstein_ratio(&self) -> f64 {
334 thermal_voltage(self.temperature_k)
335 }
336
337 pub fn electron_drift_velocity(&self, electric_field: f64) -> f64 {
341 let e_abs = electric_field.abs();
342 let v_low = self.mu_n * e_abs;
343 v_low / (1.0 + v_low / self.v_sat_n) * electric_field.signum()
344 }
345
346 pub fn hole_drift_velocity(&self, electric_field: f64) -> f64 {
348 let e_abs = electric_field.abs();
349 let v_low = self.mu_p * e_abs;
350 v_low / (1.0 + v_low / self.v_sat_p) * electric_field.signum()
351 }
352
353 pub fn electron_current_density(&self, n: f64, electric_field: f64, dn_dx: f64) -> f64 {
357 let drift = Q_ELECTRON * n * self.mu_n * electric_field;
358 let diffusion = Q_ELECTRON * self.diffusion_coefficient_n() * dn_dx;
359 drift + diffusion
361 }
362
363 pub fn hole_current_density(&self, p: f64, electric_field: f64, dp_dx: f64) -> f64 {
365 let drift = Q_ELECTRON * p * self.mu_p * electric_field;
366 let diffusion = -Q_ELECTRON * self.diffusion_coefficient_p() * dp_dx;
367 drift + diffusion
368 }
369
370 pub fn caughey_thomas_mobility(
374 mu_min: f64,
375 mu_max: f64,
376 n_total: f64,
377 n_ref: f64,
378 alpha: f64,
379 ) -> f64 {
380 mu_min + (mu_max - mu_min) / (1.0 + (n_total / n_ref).powf(alpha))
381 }
382
383 pub fn mean_free_path_n(&self) -> f64 {
385 let vth = (3.0 * K_BOLTZMANN_J * self.temperature_k / M_ELECTRON).sqrt();
386 let vth_cm = vth * 100.0; self.mu_n * M_ELECTRON * vth_cm / (Q_ELECTRON * 100.0)
388 }
389}
390
391#[derive(Debug, Clone, Copy)]
399pub struct PnJunction {
400 pub band: BandStructure,
402 pub nd: f64,
404 pub na: f64,
406 pub temperature_k: f64,
408 pub area: f64,
410}
411
412impl PnJunction {
413 pub fn new(band: BandStructure, nd: f64, na: f64, temperature_k: f64, area: f64) -> Self {
415 Self {
416 band,
417 nd,
418 na,
419 temperature_k,
420 area,
421 }
422 }
423
424 pub fn ni(&self) -> f64 {
426 let cc = CarrierConcentration::new(self.band, self.temperature_k, 0.0, 0.0);
427 cc.intrinsic_concentration()
428 }
429
430 pub fn built_in_potential(&self) -> f64 {
434 let ni = self.ni();
435 let vt = thermal_voltage(self.temperature_k);
436 vt * (self.na * self.nd / (ni * ni)).ln()
437 }
438
439 pub fn depletion_width(&self, applied_voltage: f64) -> f64 {
443 let vbi = self.built_in_potential();
444 let v_eff = vbi - applied_voltage;
445 if v_eff <= 0.0 {
446 return 0.0;
447 }
448 let epsilon = self.band.permittivity();
449 let factor = 2.0 * epsilon * v_eff / Q_ELECTRON;
450 let doping_factor = 1.0 / (self.na * 1e6) + 1.0 / (self.nd * 1e6); (factor * doping_factor).sqrt() * 100.0 }
453
454 pub fn xn(&self, applied_voltage: f64) -> f64 {
456 let w = self.depletion_width(applied_voltage);
457 w * self.na / (self.na + self.nd)
458 }
459
460 pub fn xp(&self, applied_voltage: f64) -> f64 {
462 let w = self.depletion_width(applied_voltage);
463 w * self.nd / (self.na + self.nd)
464 }
465
466 pub fn junction_capacitance(&self, applied_voltage: f64) -> f64 {
470 let w = self.depletion_width(applied_voltage);
471 if w < 1e-20 {
472 return 0.0;
473 }
474 let epsilon = self.band.permittivity();
475 epsilon / (w * 1e-2) * 1e-4 }
477
478 pub fn total_capacitance(&self, applied_voltage: f64) -> f64 {
480 self.junction_capacitance(applied_voltage) * self.area
481 }
482
483 pub fn max_electric_field(&self, applied_voltage: f64) -> f64 {
485 let w = self.depletion_width(applied_voltage);
486 if w < 1e-20 {
487 return 0.0;
488 }
489 let vbi = self.built_in_potential();
490 2.0 * (vbi - applied_voltage) / w
491 }
492
493 #[allow(clippy::too_many_arguments)]
497 pub fn saturation_current(&self, dn: f64, dp: f64, ln: f64, lp: f64) -> f64 {
498 let ni = self.ni();
499 let term_n = Q_ELECTRON * dn * ni * ni / (ln * self.nd);
500 let term_p = Q_ELECTRON * dp * ni * ni / (lp * self.na);
501 self.area * (term_n + term_p)
502 }
503
504 pub fn shockley_current(&self, voltage: f64, i0: f64, ideality_factor: f64) -> f64 {
508 let vt = thermal_voltage(self.temperature_k);
509 i0 * ((voltage / (ideality_factor * vt)).exp() - 1.0)
510 }
511
512 pub fn breakdown_voltage(&self) -> f64 {
516 let nb = self.nd.min(self.na);
517 60.0 * (self.band.band_gap_ev / 1.1).powf(1.5) * (nb / 1e16).powf(-0.75)
518 }
519}
520
521#[derive(Debug, Clone, Copy)]
527pub struct SchottkyBarrier {
528 pub metal_work_function: f64,
530 pub band: BandStructure,
532 pub doping: f64,
534 pub doping_type: DopingType,
536 pub temperature_k: f64,
538 pub ideality_factor: f64,
540 pub area: f64,
542}
543
544impl SchottkyBarrier {
545 #[allow(clippy::too_many_arguments)]
547 pub fn new(
548 metal_work_function: f64,
549 band: BandStructure,
550 doping: f64,
551 doping_type: DopingType,
552 temperature_k: f64,
553 ideality_factor: f64,
554 area: f64,
555 ) -> Self {
556 Self {
557 metal_work_function,
558 band,
559 doping,
560 doping_type,
561 temperature_k,
562 ideality_factor,
563 area,
564 }
565 }
566
567 pub fn barrier_height(&self) -> f64 {
569 let electron_affinity = -self.band.ec;
570 match self.doping_type {
571 DopingType::NType => self.metal_work_function - electron_affinity,
572 DopingType::PType => {
573 electron_affinity + self.band.band_gap_ev - self.metal_work_function
574 }
575 DopingType::Intrinsic => self.metal_work_function - electron_affinity,
576 }
577 }
578
579 pub fn built_in_potential(&self) -> f64 {
581 let vt = thermal_voltage(self.temperature_k);
582 let nc = self.band.nc(self.temperature_k);
583 self.barrier_height() - vt * (nc / self.doping).ln()
584 }
585
586 pub fn depletion_width(&self, applied_voltage: f64) -> f64 {
588 let vbi = self.built_in_potential();
589 let v_eff = vbi - applied_voltage;
590 if v_eff <= 0.0 {
591 return 0.0;
592 }
593 let epsilon = self.band.permittivity();
594 let w = (2.0 * epsilon * v_eff / (Q_ELECTRON * self.doping * 1e6)).sqrt();
595 w * 100.0 }
597
598 pub fn richardson_constant(&self) -> f64 {
600 let me = match self.doping_type {
601 DopingType::NType | DopingType::Intrinsic => self.band.me_eff,
602 DopingType::PType => self.band.mh_eff,
603 };
604 120.0 * me
607 }
608
609 pub fn saturation_current_density(&self) -> f64 {
611 let a_star = self.richardson_constant();
612 let phi_b = self.barrier_height();
613 let vt = thermal_voltage(self.temperature_k);
614 a_star * self.temperature_k * self.temperature_k * (-phi_b / vt).exp()
615 }
616
617 pub fn current(&self, voltage: f64) -> f64 {
619 let js = self.saturation_current_density();
620 let vt = thermal_voltage(self.temperature_k);
621 let i0 = js * self.area;
622 i0 * ((voltage / (self.ideality_factor * vt)).exp() - 1.0)
623 }
624}
625
626#[derive(Debug, Clone, Copy)]
634pub struct RecombinationModel {
635 pub tau_n: f64,
637 pub tau_p: f64,
639 pub cn: f64,
641 pub cp: f64,
643 pub brad: f64,
645 pub ni: f64,
647}
648
649impl RecombinationModel {
650 #[allow(clippy::too_many_arguments)]
652 pub fn new(tau_n: f64, tau_p: f64, cn: f64, cp: f64, brad: f64, ni: f64) -> Self {
653 Self {
654 tau_n,
655 tau_p,
656 cn,
657 cp,
658 brad,
659 ni,
660 }
661 }
662
663 pub fn silicon_300k(ni: f64) -> Self {
665 Self::new(
666 1e-5, 1e-5, 2.8e-31, 9.9e-32, 1.1e-14, ni,
672 )
673 }
674
675 pub fn srh_rate(&self, n: f64, p: f64) -> f64 {
679 let np = n * p;
680 let ni2 = self.ni * self.ni;
681 let numerator = np - ni2;
682 let denominator = self.tau_p * (n + self.ni) + self.tau_n * (p + self.ni);
683 if denominator.abs() < 1e-50 {
684 return 0.0;
685 }
686 numerator / denominator
687 }
688
689 pub fn auger_rate(&self, n: f64, p: f64) -> f64 {
693 let ni2 = self.ni * self.ni;
694 (self.cn * n + self.cp * p) * (n * p - ni2)
695 }
696
697 pub fn radiative_rate(&self, n: f64, p: f64) -> f64 {
701 let ni2 = self.ni * self.ni;
702 self.brad * (n * p - ni2)
703 }
704
705 pub fn total_rate(&self, n: f64, p: f64) -> f64 {
707 self.srh_rate(n, p) + self.auger_rate(n, p) + self.radiative_rate(n, p)
708 }
709
710 pub fn effective_lifetime(&self, n: f64, p: f64) -> f64 {
714 let r_total = self.total_rate(n, p);
715 let ni2 = self.ni * self.ni;
716 let delta_n = (n * p - ni2).abs();
717 if delta_n < 1e-10 || r_total.abs() < 1e-50 {
718 return self.tau_n.min(self.tau_p);
719 }
720 let majority = n.max(p);
722 majority / r_total.abs()
723 }
724
725 pub fn surface_recombination_rate(
729 surface_velocity: f64,
730 n_surface: f64,
731 n_equilibrium: f64,
732 ) -> f64 {
733 surface_velocity * (n_surface - n_equilibrium)
734 }
735}
736
737#[derive(Debug, Clone, Copy)]
745pub struct MosfetModel {
746 pub vth: f64,
748 pub cox: f64,
750 pub mobility: f64,
752 pub width: f64,
754 pub length: f64,
756 pub temperature_k: f64,
758 pub n_sub: f64,
760 pub lambda: f64,
762}
763
764impl MosfetModel {
765 #[allow(clippy::too_many_arguments)]
767 pub fn new(
768 vth: f64,
769 cox: f64,
770 mobility: f64,
771 width: f64,
772 length: f64,
773 temperature_k: f64,
774 n_sub: f64,
775 lambda: f64,
776 ) -> Self {
777 Self {
778 vth,
779 cox,
780 mobility,
781 width,
782 length,
783 temperature_k,
784 n_sub,
785 lambda,
786 }
787 }
788
789 pub fn typical_nmos() -> Self {
791 Self::new(
792 0.5, 3.45e-7, 400.0, 10.0e-4, 0.18e-4, 300.0, 1.3, 0.05, )
801 }
802
803 pub fn transconductance_parameter(&self) -> f64 {
805 self.mobility * self.cox * self.width / self.length
806 }
807
808 pub fn linear_current(&self, vgs: f64, vds: f64) -> f64 {
812 if vgs <= self.vth || vds <= 0.0 {
813 return 0.0;
814 }
815 let k = self.transconductance_parameter();
816 let vov = vgs - self.vth;
817 let vds_eff = vds.min(vov); k * (vov * vds_eff - vds_eff * vds_eff / 2.0) * (1.0 + self.lambda * vds)
819 }
820
821 pub fn saturation_current(&self, vgs: f64, vds: f64) -> f64 {
825 if vgs <= self.vth {
826 return 0.0;
827 }
828 let k = self.transconductance_parameter();
829 let vov = vgs - self.vth;
830 (k / 2.0) * vov * vov * (1.0 + self.lambda * vds)
831 }
832
833 pub fn drain_current(&self, vgs: f64, vds: f64) -> f64 {
835 if vgs <= self.vth {
836 return self.subthreshold_current(vgs, vds);
837 }
838 let vov = vgs - self.vth;
839 if vds < vov {
840 self.linear_current(vgs, vds)
841 } else {
842 self.saturation_current(vgs, vds)
843 }
844 }
845
846 pub fn subthreshold_current(&self, vgs: f64, vds: f64) -> f64 {
850 let vt = thermal_voltage(self.temperature_k);
851 let k = self.transconductance_parameter();
852 let i0 = k * (self.n_sub * vt) * (self.n_sub * vt) / 2.0;
854 let exp_gate = ((vgs - self.vth) / (self.n_sub * vt)).exp();
855 let exp_drain = 1.0 - (-vds / vt).exp();
856 i0 * exp_gate * exp_drain
857 }
858
859 pub fn subthreshold_swing(&self) -> f64 {
861 let vt = thermal_voltage(self.temperature_k);
862 self.n_sub * vt * (10.0_f64).ln() * 1000.0
863 }
864
865 pub fn transconductance(&self, vgs: f64) -> f64 {
867 if vgs <= self.vth {
868 return 0.0;
869 }
870 let k = self.transconductance_parameter();
871 k * (vgs - self.vth)
872 }
873
874 pub fn output_conductance(&self, vgs: f64) -> f64 {
876 if vgs <= self.vth {
877 return 0.0;
878 }
879 let k = self.transconductance_parameter();
880 let vov = vgs - self.vth;
881 (k / 2.0) * vov * vov * self.lambda
882 }
883
884 pub fn intrinsic_gain(&self, vgs: f64) -> f64 {
886 let gds = self.output_conductance(vgs);
887 if gds.abs() < 1e-30 {
888 return f64::INFINITY;
889 }
890 self.transconductance(vgs) / gds
891 }
892
893 pub fn body_effect_vth(vth0: f64, gamma: f64, phi_f: f64, vsb: f64) -> f64 {
897 vth0 + gamma * ((2.0 * phi_f + vsb).sqrt() - (2.0 * phi_f).sqrt())
898 }
899}
900
901pub struct SemiconductorAnalysis;
909
910impl SemiconductorAnalysis {
911 pub fn resistivity(n: f64, mu_n: f64, p: f64, mu_p: f64) -> f64 {
915 let sigma = Q_ELECTRON * (n * mu_n + p * mu_p);
916 if sigma < 1e-50 {
917 return f64::INFINITY;
918 }
919 1.0 / sigma
920 }
921
922 pub fn conductivity(n: f64, mu_n: f64, p: f64, mu_p: f64) -> f64 {
924 Q_ELECTRON * (n * mu_n + p * mu_p)
925 }
926
927 pub fn sheet_resistance(resistivity: f64, thickness: f64) -> f64 {
929 if thickness < 1e-30 {
930 return f64::INFINITY;
931 }
932 resistivity / thickness
933 }
934
935 pub fn hall_coefficient_n(n: f64) -> f64 {
940 -1.0 / (Q_ELECTRON * n)
941 }
942
943 pub fn hall_coefficient_p(p: f64) -> f64 {
945 1.0 / (Q_ELECTRON * p)
946 }
947
948 pub fn hall_coefficient_mixed(n: f64, mu_n: f64, p: f64, mu_p: f64) -> f64 {
952 let numerator = p * mu_p * mu_p - n * mu_n * mu_n;
953 let denominator = Q_ELECTRON * (p * mu_p + n * mu_n).powi(2);
954 if denominator.abs() < 1e-50 {
955 return 0.0;
956 }
957 numerator / denominator
958 }
959
960 pub fn hall_mobility(hall_coefficient: f64, conductivity: f64) -> f64 {
962 (hall_coefficient * conductivity).abs()
963 }
964
965 pub fn carrier_from_hall(hall_coefficient: f64) -> f64 {
967 1.0 / (Q_ELECTRON * hall_coefficient.abs())
968 }
969
970 pub fn four_point_probe(
974 voltage: f64,
975 current: f64,
976 thickness: f64,
977 correction_factor: f64,
978 ) -> f64 {
979 (PI / (2.0_f64).ln()) * thickness * (voltage / current) * correction_factor
980 }
981
982 pub fn mobility_from_conductivity(conductivity: f64, carrier_conc: f64) -> f64 {
984 conductivity / (Q_ELECTRON * carrier_conc)
985 }
986
987 pub fn tlm_contact_resistance(
991 r_total: f64,
992 r_sheet: f64,
993 gap_distance: f64,
994 width: f64,
995 ) -> f64 {
996 (r_total - r_sheet * gap_distance / width) / 2.0
997 }
998
999 pub fn specific_contact_resistivity(r_contact: f64, contact_length: f64, width: f64) -> f64 {
1001 r_contact * contact_length * width
1002 }
1003}
1004
1005#[cfg(test)]
1010mod tests {
1011 use super::*;
1012
1013 const EPS: f64 = 1e-6;
1014
1015 #[test]
1016 fn test_thermal_voltage_300k() {
1017 let vt = thermal_voltage(300.0);
1018 assert!((vt - 0.02585).abs() < 1e-4);
1020 }
1021
1022 #[test]
1023 fn test_silicon_band_gap() {
1024 let si = BandStructure::silicon();
1025 assert!((si.band_gap_ev - 1.12).abs() < EPS);
1026 }
1027
1028 #[test]
1029 fn test_band_edges_consistency() {
1030 let si = BandStructure::silicon();
1031 let gap = si.ec - si.ev;
1032 assert!((gap - si.band_gap_ev).abs() < EPS);
1033 }
1034
1035 #[test]
1036 fn test_nc_nv_positive() {
1037 let si = BandStructure::silicon();
1038 assert!(si.nc(300.0) > 0.0);
1039 assert!(si.nv(300.0) > 0.0);
1040 }
1041
1042 #[test]
1043 fn test_silicon_nc_order_of_magnitude() {
1044 let si = BandStructure::silicon();
1045 let nc = si.nc(300.0);
1046 assert!(nc > 1e18, "Nc too small: {nc}");
1048 assert!(nc < 1e21, "Nc too large: {nc}");
1049 }
1050
1051 #[test]
1052 fn test_intrinsic_concentration_silicon_300k() {
1053 let si = BandStructure::silicon();
1054 let cc = CarrierConcentration::new(si, 300.0, 0.0, 0.0);
1055 let ni = cc.intrinsic_concentration();
1056 assert!(ni > 1e8, "ni too small: {ni}");
1058 assert!(ni < 1e12, "ni too large: {ni}");
1059 }
1060
1061 #[test]
1062 fn test_intrinsic_np_product() {
1063 let si = BandStructure::silicon();
1064 let cc = CarrierConcentration::new(si, 300.0, 0.0, 0.0);
1065 let n = cc.electron_concentration();
1066 let p = cc.hole_concentration();
1067 let ni = cc.intrinsic_concentration();
1068 let ratio = (n * p) / (ni * ni);
1070 assert!((ratio - 1.0).abs() < 0.01, "np != ni^2, ratio = {ratio}");
1071 }
1072
1073 #[test]
1074 fn test_n_type_doping() {
1075 let si = BandStructure::silicon();
1076 let cc = CarrierConcentration::new(si, 300.0, 1e16, 0.0);
1077 let n = cc.electron_concentration();
1078 let p = cc.hole_concentration();
1079 assert!(n > p, "n-type should have n >> p");
1080 assert!(
1081 (n - 1e16).abs() / 1e16 < 0.01,
1082 "n should ≈ Nd for heavy doping"
1083 );
1084 }
1085
1086 #[test]
1087 fn test_p_type_doping() {
1088 let si = BandStructure::silicon();
1089 let cc = CarrierConcentration::new(si, 300.0, 0.0, 1e17);
1090 let n = cc.electron_concentration();
1091 let p = cc.hole_concentration();
1092 assert!(p > n, "p-type should have p >> n");
1093 }
1094
1095 #[test]
1096 fn test_doping_type_classification() {
1097 let si = BandStructure::silicon();
1098 let cc_n = CarrierConcentration::new(si, 300.0, 1e16, 0.0);
1099 assert_eq!(cc_n.doping_type(), DopingType::NType);
1100
1101 let cc_p = CarrierConcentration::new(si, 300.0, 0.0, 1e16);
1102 assert_eq!(cc_p.doping_type(), DopingType::PType);
1103
1104 let cc_i = CarrierConcentration::new(si, 300.0, 0.0, 0.0);
1105 assert_eq!(cc_i.doping_type(), DopingType::Intrinsic);
1106 }
1107
1108 #[test]
1109 fn test_compensation_ratio() {
1110 let si = BandStructure::silicon();
1111 let cc = CarrierConcentration::new(si, 300.0, 1e16, 5e15);
1112 let kr = cc.compensation_ratio();
1113 assert!((kr - 0.5).abs() < EPS);
1114 }
1115
1116 #[test]
1117 fn test_fermi_level_shift_n_type() {
1118 let si = BandStructure::silicon();
1119 let cc = CarrierConcentration::new(si, 300.0, 1e16, 0.0);
1120 let shift = cc.fermi_level_shift();
1121 assert!(shift > 0.0, "Fermi level should be above Ei for n-type");
1123 }
1124
1125 #[test]
1126 fn test_einstein_relation() {
1127 let dd = DriftDiffusion::silicon_300k();
1128 let dn = dd.diffusion_coefficient_n();
1129 let ratio = dn / dd.mu_n;
1130 let vt = thermal_voltage(300.0);
1131 assert!(
1132 (ratio - vt).abs() < 1e-5,
1133 "Einstein relation violated: D/mu = {ratio}, V_T = {vt}"
1134 );
1135 }
1136
1137 #[test]
1138 fn test_drift_velocity_saturation() {
1139 let dd = DriftDiffusion::silicon_300k();
1140 let v = dd.electron_drift_velocity(1e6);
1142 assert!(
1143 v < dd.v_sat_n * 1.01,
1144 "Velocity exceeds saturation: {v} > {}",
1145 dd.v_sat_n
1146 );
1147 assert!(v > dd.v_sat_n * 0.5, "Velocity too low at high field: {v}");
1148 }
1149
1150 #[test]
1151 fn test_drift_velocity_low_field() {
1152 let dd = DriftDiffusion::silicon_300k();
1153 let e_field = 100.0; let v = dd.electron_drift_velocity(e_field);
1156 let v_expected = dd.mu_n * e_field;
1157 assert!(
1158 (v - v_expected).abs() / v_expected < 0.1,
1159 "Low-field velocity: {v}, expected: {v_expected}"
1160 );
1161 }
1162
1163 #[test]
1164 fn test_pn_junction_built_in_potential() {
1165 let si = BandStructure::silicon();
1166 let pn = PnJunction::new(si, 1e16, 1e16, 300.0, 1e-4);
1167 let vbi = pn.built_in_potential();
1168 assert!(vbi > 0.4, "Vbi too low: {vbi}");
1170 assert!(vbi < 1.0, "Vbi too high: {vbi}");
1171 }
1172
1173 #[test]
1174 fn test_pn_junction_depletion_width() {
1175 let si = BandStructure::silicon();
1176 let pn = PnJunction::new(si, 1e16, 1e16, 300.0, 1e-4);
1177 let w = pn.depletion_width(0.0);
1178 assert!(w > 0.0, "Depletion width should be positive");
1180 assert!(w < 1.0, "Depletion width too large: {w} cm");
1182
1183 let w_rev = pn.depletion_width(-5.0);
1185 assert!(w_rev > w, "Reverse bias should widen depletion region");
1186 }
1187
1188 #[test]
1189 fn test_pn_junction_xn_xp_sum() {
1190 let si = BandStructure::silicon();
1191 let pn = PnJunction::new(si, 1e16, 1e18, 300.0, 1e-4);
1192 let w = pn.depletion_width(0.0);
1193 let xn = pn.xn(0.0);
1194 let xp = pn.xp(0.0);
1195 assert!(((xn + xp) - w).abs() / w < 1e-10, "xn + xp should equal W");
1196 }
1197
1198 #[test]
1199 fn test_shockley_diode_forward_bias() {
1200 let si = BandStructure::silicon();
1201 let pn = PnJunction::new(si, 1e16, 1e16, 300.0, 1e-4);
1202 let i0 = 1e-12; let i_forward = pn.shockley_current(0.6, i0, 1.0);
1204 assert!(i_forward > 0.0, "Forward current should be positive");
1205 assert!(i_forward > i0 * 100.0, "Forward current should be >> I0");
1206 }
1207
1208 #[test]
1209 fn test_shockley_diode_reverse_bias() {
1210 let si = BandStructure::silicon();
1211 let pn = PnJunction::new(si, 1e16, 1e16, 300.0, 1e-4);
1212 let i0 = 1e-12;
1213 let i_reverse = pn.shockley_current(-5.0, i0, 1.0);
1214 assert!(i_reverse < 0.0, "Reverse current should be negative");
1216 assert!((i_reverse + i0).abs() < i0 * 0.01, "Reverse current ≈ -I0");
1217 }
1218
1219 #[test]
1220 fn test_junction_capacitance_reverse_bias() {
1221 let si = BandStructure::silicon();
1222 let pn = PnJunction::new(si, 1e16, 1e16, 300.0, 1e-4);
1223 let c0 = pn.junction_capacitance(0.0);
1224 let c_rev = pn.junction_capacitance(-5.0);
1225 assert!(c_rev < c0, "Capacitance should decrease with reverse bias");
1227 }
1228
1229 #[test]
1230 fn test_schottky_barrier_height() {
1231 let si = BandStructure::silicon();
1232 let sb = SchottkyBarrier::new(4.08, si, 1e16, DopingType::NType, 300.0, 1.0, 1e-4);
1234 let phi_b = sb.barrier_height();
1235 assert!((phi_b - 0.03).abs() < 0.01, "Barrier height: {phi_b}");
1236 }
1237
1238 #[test]
1239 fn test_schottky_current_forward() {
1240 let si = BandStructure::silicon();
1241 let sb = SchottkyBarrier::new(4.75, si, 1e16, DopingType::NType, 300.0, 1.05, 1e-4);
1242 let i_fwd = sb.current(0.3);
1243 assert!(i_fwd > 0.0, "Forward Schottky current should be positive");
1244 }
1245
1246 #[test]
1247 fn test_srh_recombination_equilibrium() {
1248 let ni = 1e10;
1249 let rm = RecombinationModel::silicon_300k(ni);
1250 let rate = rm.srh_rate(ni, ni);
1252 assert!(
1253 rate.abs() < 1.0,
1254 "SRH rate at equilibrium should ≈ 0: {rate}"
1255 );
1256 }
1257
1258 #[test]
1259 fn test_total_recombination_positive_injection() {
1260 let ni = 1e10;
1261 let rm = RecombinationModel::silicon_300k(ni);
1262 let rate = rm.total_rate(1e16, 1e16);
1264 assert!(
1265 rate > 0.0,
1266 "Recombination rate should be positive for injection"
1267 );
1268 }
1269
1270 #[test]
1271 fn test_mosfet_cutoff() {
1272 let mos = MosfetModel::typical_nmos();
1273 let id = mos.drain_current(0.0, 1.0);
1274 assert!(id < 1e-6, "Cutoff current should be very small: {id}");
1276 }
1277
1278 #[test]
1279 fn test_mosfet_saturation_current() {
1280 let mos = MosfetModel::typical_nmos();
1281 let id_sat = mos.saturation_current(1.5, 2.0);
1282 assert!(id_sat > 0.0, "Saturation current should be positive");
1283 }
1284
1285 #[test]
1286 fn test_mosfet_linear_vs_saturation() {
1287 let mos = MosfetModel::typical_nmos();
1288 let vgs = 1.5;
1289 let vds_linear = 0.2;
1290 let vds_sat = 2.0;
1291 let id_lin = mos.drain_current(vgs, vds_linear);
1292 let id_sat = mos.drain_current(vgs, vds_sat);
1293 assert!(id_sat > id_lin, "Saturation current > linear current");
1295 }
1296
1297 #[test]
1298 fn test_subthreshold_swing() {
1299 let mos = MosfetModel::typical_nmos();
1300 let ss = mos.subthreshold_swing();
1301 assert!(ss > 55.0, "SS too low: {ss} mV/dec");
1303 assert!(ss < 120.0, "SS too high: {ss} mV/dec");
1304 }
1305
1306 #[test]
1307 fn test_mosfet_transconductance() {
1308 let mos = MosfetModel::typical_nmos();
1309 let gm = mos.transconductance(1.5);
1310 assert!(gm > 0.0, "Transconductance should be positive above Vth");
1311 let gm_cutoff = mos.transconductance(0.3);
1312 assert_eq!(gm_cutoff, 0.0, "Transconductance should be 0 below Vth");
1313 }
1314
1315 #[test]
1316 fn test_resistivity_calculation() {
1317 let rho = SemiconductorAnalysis::resistivity(1e16, 1400.0, 0.0, 0.0);
1319 assert!(rho > 0.1, "Resistivity too low: {rho}");
1321 assert!(rho < 1.0, "Resistivity too high: {rho}");
1322 }
1323
1324 #[test]
1325 fn test_sheet_resistance() {
1326 let rho = 0.5; let thickness = 1e-4; let rs = SemiconductorAnalysis::sheet_resistance(rho, thickness);
1329 assert_eq!(rs, 5000.0);
1330 }
1331
1332 #[test]
1333 fn test_hall_coefficient_n_type() {
1334 let rh = SemiconductorAnalysis::hall_coefficient_n(1e16);
1335 assert!(rh < 0.0, "Hall coefficient for n-type should be negative");
1336 }
1337
1338 #[test]
1339 fn test_carrier_from_hall() {
1340 let rh = SemiconductorAnalysis::hall_coefficient_n(1e16);
1341 let n_measured = SemiconductorAnalysis::carrier_from_hall(rh);
1342 assert!(
1343 (n_measured - 1e16).abs() / 1e16 < 0.01,
1344 "Carrier from Hall: {n_measured}"
1345 );
1346 }
1347
1348 #[test]
1349 fn test_body_effect_increases_vth() {
1350 let vth0 = 0.5;
1351 let gamma = 0.4;
1352 let phi_f = 0.35;
1353 let vth_with_body = MosfetModel::body_effect_vth(vth0, gamma, phi_f, 1.0);
1354 assert!(
1355 vth_with_body > vth0,
1356 "Body effect should increase Vth: {} vs {}",
1357 vth_with_body,
1358 vth0
1359 );
1360 }
1361
1362 #[test]
1363 fn test_gaas_higher_mobility() {
1364 let si = DriftDiffusion::silicon_300k();
1365 let gaas = DriftDiffusion::gaas_300k();
1366 assert!(
1367 gaas.mu_n > si.mu_n,
1368 "GaAs should have higher electron mobility than Si"
1369 );
1370 }
1371
1372 #[test]
1373 fn test_varshni_band_gap() {
1374 let si = BandStructure::silicon();
1375 let eg_0k = si.varshni_band_gap(0.0, 4.73e-4, 636.0);
1377 let eg_300k = si.varshni_band_gap(300.0, 4.73e-4, 636.0);
1378 assert!(
1380 eg_0k > eg_300k,
1381 "Band gap should be larger at 0K: {eg_0k} vs {eg_300k}"
1382 );
1383 }
1384}