Skip to main content

oxiphysics_materials/
semiconductor.rs

1// Copyright 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Semiconductor physics: band gap, carrier transport, junctions, and device models.
5//!
6//! This module provides comprehensive semiconductor physics modeling including
7//! band structure, carrier concentrations, drift-diffusion transport, p-n junctions,
8//! Schottky barriers, recombination mechanisms, MOSFET models, and analysis tools.
9
10use std::f64::consts::PI;
11
12/// Boltzmann constant in eV/K.
13pub const K_BOLTZMANN_EV: f64 = 8.617333262e-5;
14
15/// Boltzmann constant in J/K.
16pub const K_BOLTZMANN_J: f64 = 1.380649e-23;
17
18/// Elementary charge in C.
19pub const Q_ELECTRON: f64 = 1.602176634e-19;
20
21/// Permittivity of free space in F/m.
22pub const EPSILON_0: f64 = 8.854187817e-12;
23
24/// Planck constant in J·s.
25pub const H_PLANCK: f64 = 6.62607015e-34;
26
27/// Reduced Planck constant in J·s.
28pub const HBAR: f64 = 1.054571817e-34;
29
30/// Free electron mass in kg.
31pub const M_ELECTRON: f64 = 9.1093837015e-31;
32
33/// Thermal voltage at temperature T: V_T = k_B * T / q.
34pub fn thermal_voltage(temperature_k: f64) -> f64 {
35    K_BOLTZMANN_EV * temperature_k
36}
37
38// ---------------------------------------------------------------------------
39// BandStructure
40// ---------------------------------------------------------------------------
41
42/// Band structure parameters of a semiconductor.
43///
44/// Characterizes the energy band structure including band gap,
45/// band edges, and effective masses.
46#[derive(Debug, Clone, Copy)]
47pub struct BandStructure {
48    /// Band gap energy in eV.
49    pub band_gap_ev: f64,
50    /// Conduction band edge energy in eV (relative to vacuum).
51    pub ec: f64,
52    /// Valence band edge energy in eV (relative to vacuum).
53    pub ev: f64,
54    /// Electron effective mass ratio (m*/m0).
55    pub me_eff: f64,
56    /// Hole effective mass ratio (m*/m0).
57    pub mh_eff: f64,
58    /// Dielectric constant (relative permittivity).
59    pub dielectric_constant: f64,
60}
61
62impl BandStructure {
63    /// Create a new band structure.
64    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    /// Silicon band structure at 300K.
82    pub fn silicon() -> Self {
83        Self::new(1.12, 4.05, 1.08, 0.56, 11.7)
84    }
85
86    /// Germanium band structure at 300K.
87    pub fn germanium() -> Self {
88        Self::new(0.66, 4.0, 0.55, 0.37, 16.0)
89    }
90
91    /// Gallium arsenide band structure at 300K.
92    pub fn gallium_arsenide() -> Self {
93        Self::new(1.42, 4.07, 0.067, 0.45, 12.9)
94    }
95
96    /// Silicon carbide (4H-SiC) band structure.
97    pub fn silicon_carbide() -> Self {
98        Self::new(3.26, 3.17, 0.39, 1.0, 9.7)
99    }
100
101    /// Gallium nitride band structure.
102    pub fn gallium_nitride() -> Self {
103        Self::new(3.4, 4.1, 0.2, 0.8, 8.9)
104    }
105
106    /// Temperature-dependent band gap using Varshni equation.
107    ///
108    /// Eg(T) = Eg(0) - alpha * T^2 / (T + beta)
109    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    /// Effective density of states in conduction band (Nc) in cm^-3.
115    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 // m^-3 to cm^-3
119    }
120
121    /// Effective density of states in valence band (Nv) in cm^-3.
122    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 // m^-3 to cm^-3
126    }
127
128    /// Absolute permittivity in F/m.
129    pub fn permittivity(&self) -> f64 {
130        self.dielectric_constant * EPSILON_0
131    }
132
133    /// Midgap energy (average of Ec and Ev).
134    pub fn midgap_energy(&self) -> f64 {
135        (self.ec + self.ev) / 2.0
136    }
137}
138
139// ---------------------------------------------------------------------------
140// CarrierConcentration
141// ---------------------------------------------------------------------------
142
143/// Doping type of a semiconductor.
144#[derive(Debug, Clone, Copy, PartialEq, Eq)]
145pub enum DopingType {
146    /// N-type: donor impurities (excess electrons).
147    NType,
148    /// P-type: acceptor impurities (excess holes).
149    PType,
150    /// Intrinsic: undoped semiconductor.
151    Intrinsic,
152}
153
154/// Carrier concentration calculations for a semiconductor.
155///
156/// Computes intrinsic carrier concentration, doped carrier densities,
157/// and Fermi level positions.
158#[derive(Debug, Clone, Copy)]
159pub struct CarrierConcentration {
160    /// Band structure of the material.
161    pub band: BandStructure,
162    /// Temperature in Kelvin.
163    pub temperature_k: f64,
164    /// Donor concentration in cm^-3.
165    pub nd: f64,
166    /// Acceptor concentration in cm^-3.
167    pub na: f64,
168}
169
170impl CarrierConcentration {
171    /// Create a new carrier concentration model.
172    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    /// Intrinsic carrier concentration ni in cm^-3.
182    ///
183    /// ni = sqrt(Nc * Nv) * exp(-Eg / (2 * k_B * T))
184    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    /// Fermi-Dirac integral of order 1/2 (approximation).
192    ///
193    /// F_{1/2}(eta) approximated using the Joyce-Dixon formula.
194    pub fn fermi_dirac_half(eta: f64) -> f64 {
195        if eta < -5.0 {
196            // Non-degenerate: F_{1/2}(eta) ≈ exp(eta) * sqrt(pi)/2
197            (PI.sqrt() / 2.0) * eta.exp()
198        } else if eta > 5.0 {
199            // Highly degenerate: F_{1/2}(eta) ≈ (2/3) * eta^{3/2}
200            (2.0 / 3.0) * eta.powf(1.5)
201        } else {
202            // Intermediate: Padé approximation
203            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    /// Electron concentration n0 in cm^-3 (assuming complete ionization).
210    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            // N-type
215            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            // P-type: n = ni^2 / p
219            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    /// Hole concentration p0 in cm^-3.
229    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    /// Doping type of the material.
236    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    /// Fermi level position relative to intrinsic Fermi level in eV.
247    ///
248    /// Ef - Ei = k_B * T * ln(n0 / ni)
249    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    /// Compensation ratio: min(Nd, Na) / max(Nd, Na).
257    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    /// Debye length in cm.
267    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        // L_D = sqrt(epsilon * V_T / (q * (n0 + p0)))
273        let total_carriers = (n0 + p0) * 1e6; // cm^-3 to m^-3
274        let ld = (epsilon * vt / (Q_ELECTRON * total_carriers)).sqrt();
275        ld * 100.0 // m to cm
276    }
277}
278
279// ---------------------------------------------------------------------------
280// DriftDiffusion
281// ---------------------------------------------------------------------------
282
283/// Drift-diffusion transport model for semiconductors.
284///
285/// Models carrier transport including mobility, diffusion, and velocity saturation.
286#[derive(Debug, Clone, Copy)]
287pub struct DriftDiffusion {
288    /// Electron mobility in cm^2/(V·s).
289    pub mu_n: f64,
290    /// Hole mobility in cm^2/(V·s).
291    pub mu_p: f64,
292    /// Temperature in K.
293    pub temperature_k: f64,
294    /// Saturation velocity for electrons in cm/s.
295    pub v_sat_n: f64,
296    /// Saturation velocity for holes in cm/s.
297    pub v_sat_p: f64,
298}
299
300impl DriftDiffusion {
301    /// Create a new drift-diffusion model.
302    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    /// Silicon at 300K with typical mobility values.
313    pub fn silicon_300k() -> Self {
314        Self::new(1400.0, 450.0, 300.0, 1.07e7, 8.37e6)
315    }
316
317    /// GaAs at 300K.
318    pub fn gaas_300k() -> Self {
319        Self::new(8500.0, 400.0, 300.0, 7.7e6, 7.7e6)
320    }
321
322    /// Electron diffusion coefficient via Einstein relation: D_n = mu_n * V_T.
323    pub fn diffusion_coefficient_n(&self) -> f64 {
324        self.mu_n * thermal_voltage(self.temperature_k)
325    }
326
327    /// Hole diffusion coefficient via Einstein relation: D_p = mu_p * V_T.
328    pub fn diffusion_coefficient_p(&self) -> f64 {
329        self.mu_p * thermal_voltage(self.temperature_k)
330    }
331
332    /// Verify Einstein relation: D/mu = k_B T / q.
333    pub fn einstein_ratio(&self) -> f64 {
334        thermal_voltage(self.temperature_k)
335    }
336
337    /// Electron drift velocity with saturation effects.
338    ///
339    /// v_d = mu * E / (1 + mu * |E| / v_sat)
340    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    /// Hole drift velocity with saturation effects.
347    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    /// Electron current density (drift + diffusion) in A/cm^2.
354    ///
355    /// J_n = q * n * mu_n * E + q * D_n * dn/dx
356    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        // Convert from C·cm^-3·cm^2/(V·s)·V/cm = A/cm^2
360        drift + diffusion
361    }
362
363    /// Hole current density (drift + diffusion) in A/cm^2.
364    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    /// Mobility with doping dependence (Caughey-Thomas model).
371    ///
372    /// mu = mu_min + (mu_max - mu_min) / (1 + (N / N_ref)^alpha)
373    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    /// Mean free path estimate from mobility in cm.
384    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; // m/s to cm/s
387        self.mu_n * M_ELECTRON * vth_cm / (Q_ELECTRON * 100.0)
388    }
389}
390
391// ---------------------------------------------------------------------------
392// PnJunction
393// ---------------------------------------------------------------------------
394
395/// P-N junction model.
396///
397/// Models the electrostatics and I-V characteristics of an abrupt p-n junction.
398#[derive(Debug, Clone, Copy)]
399pub struct PnJunction {
400    /// Band structure.
401    pub band: BandStructure,
402    /// Donor concentration (n-side) in cm^-3.
403    pub nd: f64,
404    /// Acceptor concentration (p-side) in cm^-3.
405    pub na: f64,
406    /// Temperature in K.
407    pub temperature_k: f64,
408    /// Junction area in cm^2.
409    pub area: f64,
410}
411
412impl PnJunction {
413    /// Create a new p-n junction.
414    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    /// Intrinsic carrier concentration.
425    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    /// Built-in potential Vbi in V.
431    ///
432    /// Vbi = (k_B T / q) * ln(Na * Nd / ni^2)
433    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    /// Depletion width W in cm.
440    ///
441    /// W = sqrt(2 * epsilon * (Vbi - V) * (1/Na + 1/Nd) / q)
442    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); // cm^-3 to m^-3
451        (factor * doping_factor).sqrt() * 100.0 // m to cm
452    }
453
454    /// Depletion width on the n-side in cm.
455    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    /// Depletion width on the p-side in cm.
461    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    /// Junction capacitance per unit area in F/cm^2.
467    ///
468    /// C_j = epsilon / W
469    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 // F/m / m -> F/cm^2
476    }
477
478    /// Total junction capacitance in F.
479    pub fn total_capacitance(&self, applied_voltage: f64) -> f64 {
480        self.junction_capacitance(applied_voltage) * self.area
481    }
482
483    /// Maximum electric field in V/cm.
484    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    /// Reverse saturation current I0 in A.
494    ///
495    /// Uses the Shockley diode equation parameters.
496    #[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    /// Current through the junction using Shockley diode equation.
505    ///
506    /// I = I0 * (exp(V / (n * V_T)) - 1)
507    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    /// Breakdown voltage estimate (empirical, for silicon).
513    ///
514    /// V_br ≈ 60 * (Eg / 1.1)^{3/2} * (Nb / 10^16)^{-3/4}
515    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// ---------------------------------------------------------------------------
522// SchottkyBarrier
523// ---------------------------------------------------------------------------
524
525/// Schottky barrier (metal-semiconductor) junction model.
526#[derive(Debug, Clone, Copy)]
527pub struct SchottkyBarrier {
528    /// Metal work function in eV.
529    pub metal_work_function: f64,
530    /// Semiconductor band structure.
531    pub band: BandStructure,
532    /// Doping concentration in cm^-3.
533    pub doping: f64,
534    /// Doping type.
535    pub doping_type: DopingType,
536    /// Temperature in K.
537    pub temperature_k: f64,
538    /// Ideality factor (1 for ideal).
539    pub ideality_factor: f64,
540    /// Junction area in cm^2.
541    pub area: f64,
542}
543
544impl SchottkyBarrier {
545    /// Create a new Schottky barrier.
546    #[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    /// Barrier height for n-type semiconductor: phi_B = phi_M - chi.
568    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    /// Built-in potential for the Schottky contact.
580    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    /// Depletion width in cm.
587    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 // m to cm
596    }
597
598    /// Richardson constant for the semiconductor in A/(cm^2·K^2).
599    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        // A* = 4 * pi * q * m* * k_B^2 / h^3
605        // Standard value for free electron: 120 A/(cm^2·K^2)
606        120.0 * me
607    }
608
609    /// Saturation current density in A/cm^2.
610    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    /// Current through the Schottky junction in A.
618    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// ---------------------------------------------------------------------------
627// RecombinationModel
628// ---------------------------------------------------------------------------
629
630/// Recombination mechanisms in semiconductors.
631///
632/// Models Shockley-Read-Hall (SRH), Auger, and radiative recombination.
633#[derive(Debug, Clone, Copy)]
634pub struct RecombinationModel {
635    /// Electron lifetime in s (for SRH).
636    pub tau_n: f64,
637    /// Hole lifetime in s (for SRH).
638    pub tau_p: f64,
639    /// Auger coefficient for electrons in cm^6/s.
640    pub cn: f64,
641    /// Auger coefficient for holes in cm^6/s.
642    pub cp: f64,
643    /// Radiative recombination coefficient in cm^3/s.
644    pub brad: f64,
645    /// Intrinsic carrier concentration in cm^-3.
646    pub ni: f64,
647}
648
649impl RecombinationModel {
650    /// Create a new recombination model.
651    #[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    /// Typical silicon recombination parameters at 300K.
664    pub fn silicon_300k(ni: f64) -> Self {
665        Self::new(
666            1e-5,    // tau_n = 10 us
667            1e-5,    // tau_p = 10 us
668            2.8e-31, // Cn
669            9.9e-32, // Cp
670            1.1e-14, // Brad (indirect gap, very small)
671            ni,
672        )
673    }
674
675    /// SRH recombination rate in cm^-3/s.
676    ///
677    /// R_SRH = (n*p - ni^2) / (tau_p*(n + ni) + tau_n*(p + ni))
678    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    /// Auger recombination rate in cm^-3/s.
690    ///
691    /// R_Auger = (Cn*n + Cp*p) * (n*p - ni^2)
692    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    /// Radiative recombination rate in cm^-3/s.
698    ///
699    /// R_rad = Brad * (n*p - ni^2)
700    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    /// Total recombination rate (sum of all mechanisms) in cm^-3/s.
706    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    /// Effective minority carrier lifetime in s.
711    ///
712    /// 1/tau_eff = 1/tau_SRH + 1/tau_Auger + 1/tau_rad
713    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        // Assuming low-level injection: delta_n ≈ delta carrier concentration
721        let majority = n.max(p);
722        majority / r_total.abs()
723    }
724
725    /// Surface recombination velocity model in cm/s.
726    ///
727    /// R_surface = S * (n_s - n_s0)
728    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// ---------------------------------------------------------------------------
738// MosfetModel
739// ---------------------------------------------------------------------------
740
741/// MOSFET (Metal-Oxide-Semiconductor Field-Effect Transistor) model.
742///
743/// Implements the square-law MOSFET model with subthreshold behavior.
744#[derive(Debug, Clone, Copy)]
745pub struct MosfetModel {
746    /// Threshold voltage Vth in V.
747    pub vth: f64,
748    /// Oxide capacitance per unit area Cox in F/cm^2.
749    pub cox: f64,
750    /// Channel mobility in cm^2/(V·s).
751    pub mobility: f64,
752    /// Channel width in cm.
753    pub width: f64,
754    /// Channel length in cm.
755    pub length: f64,
756    /// Temperature in K.
757    pub temperature_k: f64,
758    /// Subthreshold swing ideality factor (typically 1.0-1.5).
759    pub n_sub: f64,
760    /// Channel-length modulation parameter lambda in V^-1.
761    pub lambda: f64,
762}
763
764impl MosfetModel {
765    /// Create a new MOSFET model.
766    #[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    /// Typical NMOS transistor parameters.
790    pub fn typical_nmos() -> Self {
791        Self::new(
792            0.5,     // Vth = 0.5V
793            3.45e-7, // Cox for 10nm oxide
794            400.0,   // mobility
795            10.0e-4, // W = 10um
796            0.18e-4, // L = 0.18um
797            300.0,   // T = 300K
798            1.3,     // n_sub
799            0.05,    // lambda
800        )
801    }
802
803    /// Transconductance parameter K = mu * Cox * W / L in A/V^2.
804    pub fn transconductance_parameter(&self) -> f64 {
805        self.mobility * self.cox * self.width / self.length
806    }
807
808    /// Drain current in the linear (triode) region in A.
809    ///
810    /// Id = K * ((Vgs - Vth) * Vds - Vds^2 / 2) * (1 + lambda * Vds)
811    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); // Clamp to saturation boundary
818        k * (vov * vds_eff - vds_eff * vds_eff / 2.0) * (1.0 + self.lambda * vds)
819    }
820
821    /// Drain current in the saturation region in A.
822    ///
823    /// Id = (K / 2) * (Vgs - Vth)^2 * (1 + lambda * Vds)
824    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    /// Drain current combining linear and saturation regions.
834    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    /// Subthreshold current in A.
847    ///
848    /// I_sub = I0 * exp((Vgs - Vth) / (n * V_T)) * (1 - exp(-Vds / V_T))
849    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        // Subthreshold reference current
853        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    /// Subthreshold swing in mV/decade.
860    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    /// Transconductance gm = dId/dVgs in the saturation region, in A/V.
866    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    /// Output conductance gds = dId/dVds in saturation, in A/V.
875    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    /// Intrinsic voltage gain Av = gm / gds.
885    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    /// Threshold voltage with body effect.
894    ///
895    /// Vth = Vth0 + gamma * (sqrt(2*phi_f + Vsb) - sqrt(2*phi_f))
896    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
901// ---------------------------------------------------------------------------
902// SemiconductorAnalysis
903// ---------------------------------------------------------------------------
904
905/// Analysis tools for semiconductor properties.
906///
907/// Provides calculations for resistivity, sheet resistance, and Hall measurements.
908pub struct SemiconductorAnalysis;
909
910impl SemiconductorAnalysis {
911    /// Resistivity in Ohm·cm.
912    ///
913    /// rho = 1 / (q * (n * mu_n + p * mu_p))
914    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    /// Conductivity in (Ohm·cm)^-1.
923    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    /// Sheet resistance in Ohm/square.
928    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    /// Hall coefficient R_H in cm^3/C.
936    ///
937    /// For a single carrier type (n-type): R_H = -1 / (q * n)
938    /// For a single carrier type (p-type): R_H = 1 / (q * p)
939    pub fn hall_coefficient_n(n: f64) -> f64 {
940        -1.0 / (Q_ELECTRON * n)
941    }
942
943    /// Hall coefficient for p-type.
944    pub fn hall_coefficient_p(p: f64) -> f64 {
945        1.0 / (Q_ELECTRON * p)
946    }
947
948    /// Hall coefficient for a mixed carrier system.
949    ///
950    /// R_H = (p * mu_p^2 - n * mu_n^2) / (q * (p * mu_p + n * mu_n)^2)
951    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    /// Hall mobility mu_H = R_H * sigma in cm^2/(V·s).
961    pub fn hall_mobility(hall_coefficient: f64, conductivity: f64) -> f64 {
962        (hall_coefficient * conductivity).abs()
963    }
964
965    /// Carrier concentration from Hall measurement in cm^-3.
966    pub fn carrier_from_hall(hall_coefficient: f64) -> f64 {
967        1.0 / (Q_ELECTRON * hall_coefficient.abs())
968    }
969
970    /// Four-point probe resistivity measurement.
971    ///
972    /// For a thin sheet: rho = (pi / ln(2)) * t * V / I * correction_factor
973    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    /// Mobility from conductivity and carrier concentration.
983    pub fn mobility_from_conductivity(conductivity: f64, carrier_conc: f64) -> f64 {
984        conductivity / (Q_ELECTRON * carrier_conc)
985    }
986
987    /// Contact resistance for ohmic contacts using TLM model.
988    ///
989    /// R_total = 2 * R_c + R_sheet * d / W
990    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    /// Specific contact resistivity from contact resistance.
1000    pub fn specific_contact_resistivity(r_contact: f64, contact_length: f64, width: f64) -> f64 {
1001        r_contact * contact_length * width
1002    }
1003}
1004
1005// ---------------------------------------------------------------------------
1006// Tests
1007// ---------------------------------------------------------------------------
1008
1009#[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        // ~25.85 mV at 300K
1019        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        // Nc for Si ~ 2.8e19 cm^-3
1047        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        // ni for Si at 300K ~ 1e10 cm^-3
1057        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        // n * p should equal ni^2 for intrinsic
1069        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        // For n-type, Ef should be above Ei
1122        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        // At very high field, velocity should approach v_sat
1141        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        // At low field, velocity ≈ mu * E
1154        let e_field = 100.0; // V/cm, low field
1155        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        // Typical Vbi for Si ≈ 0.6-0.8V
1169        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        // Depletion width should be > 0
1179        assert!(w > 0.0, "Depletion width should be positive");
1180        // Typical: sub-micron to few microns
1181        assert!(w < 1.0, "Depletion width too large: {w} cm");
1182
1183        // Reverse bias should increase depletion width
1184        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; // 1 pA saturation current
1203        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        // Reverse current should be approximately -I0
1215        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        // Capacitance should decrease with reverse bias
1226        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        // Aluminum on n-Si: phi_M = 4.08 eV, chi = 4.05 eV
1233        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        // At equilibrium (n=ni, p=ni), recombination rate should be 0
1251        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        // Excess carriers: n = 1e16, p = 1e16
1263        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        // In subthreshold, current should be very small
1275        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        // In saturation, current should be higher but not proportionally
1294        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        // Ideal SS ≈ 60 mV/dec at 300K; with n_sub=1.3, ≈ 78 mV/dec
1302        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        // n-type Si: n=1e16, mu_n=1400, p negligible
1318        let rho = SemiconductorAnalysis::resistivity(1e16, 1400.0, 0.0, 0.0);
1319        // rho = 1 / (q * n * mu_n) ≈ 0.446 Ohm·cm
1320        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; // Ohm·cm
1327        let thickness = 1e-4; // 1 um
1328        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        // Varshni parameters for Si: alpha=4.73e-4, beta=636
1376        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        // Eg should increase as T decreases (for most semiconductors)
1379        assert!(
1380            eg_0k > eg_300k,
1381            "Band gap should be larger at 0K: {eg_0k} vs {eg_300k}"
1382        );
1383    }
1384}