Skip to main content

oxiphysics_materials/
semiconductor_materials.rs

1// Copyright 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Semiconductor material models.
5//!
6//! Covers band gap (direct/indirect), carrier concentration (intrinsic/extrinsic),
7//! mobility models (Caughey-Thomas), resistivity, Hall effect, Fermi level,
8//! Debye length, PN junction characteristics, Schottky barrier, photovoltaic
9//! (Shockley-Queisser limit), and thermoelectric effects (Seebeck/Peltier/ZT).
10//!
11//! All quantities are in SI units unless otherwise noted.
12
13#![allow(dead_code)]
14#![allow(clippy::too_many_arguments)]
15
16use std::f64::consts::PI;
17
18// ---------------------------------------------------------------------------
19// Physical constants
20// ---------------------------------------------------------------------------
21
22/// Boltzmann constant (J/K).
23const K_B: f64 = 1.380649e-23;
24/// Elementary charge (C).
25const Q_E: f64 = 1.602176634e-19;
26/// Electron rest mass (kg).
27const M_E: f64 = 9.1093837015e-31;
28/// Planck constant (J·s).
29const H_PLANCK: f64 = 6.62607015e-34;
30/// Reduced Planck constant (J·s).
31const H_BAR: f64 = 1.054571817e-34;
32/// Vacuum permittivity (F/m).
33const EPSILON_0: f64 = 8.854187817e-12;
34/// Speed of light (m/s).
35const C_LIGHT: f64 = 2.99792458e8;
36/// Stefan-Boltzmann constant (W/m²·K⁴).
37const SIGMA_SB: f64 = 5.670374419e-8;
38
39// ---------------------------------------------------------------------------
40// Band gap
41// ---------------------------------------------------------------------------
42
43/// Type of band gap transition.
44#[derive(Clone, Copy, Debug, PartialEq)]
45pub enum BandGapType {
46    /// Direct band gap (e.g., GaAs).
47    Direct,
48    /// Indirect band gap (e.g., Si).
49    Indirect,
50}
51
52/// Band gap model with Varshni temperature dependence.
53///
54/// E_g(T) = E_g(0) - α T² / (T + β)
55#[derive(Clone, Debug)]
56pub struct BandGapModel {
57    /// Band gap at T = 0 K (eV).
58    pub eg0_ev: f64,
59    /// Varshni α parameter (eV/K).
60    pub alpha: f64,
61    /// Varshni β parameter (K).
62    pub beta: f64,
63    /// Band gap type.
64    pub gap_type: BandGapType,
65}
66
67impl BandGapModel {
68    /// Create a new band gap model.
69    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    /// Silicon band gap model (Varshni parameters).
79    pub fn silicon() -> Self {
80        Self::new(1.166, 4.73e-4, 636.0, BandGapType::Indirect)
81    }
82
83    /// Gallium arsenide band gap model.
84    pub fn gaas() -> Self {
85        Self::new(1.519, 5.405e-4, 204.0, BandGapType::Direct)
86    }
87
88    /// Germanium band gap model.
89    pub fn germanium() -> Self {
90        Self::new(0.7437, 4.774e-4, 235.0, BandGapType::Indirect)
91    }
92
93    /// Band gap at temperature `t_kelvin` (eV).
94    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    /// Band gap at temperature `t_kelvin` (Joules).
99    pub fn band_gap_j(&self, t_kelvin: f64) -> f64 {
100        self.band_gap_ev(t_kelvin) * Q_E
101    }
102
103    /// Returns whether this is a direct gap semiconductor.
104    pub fn is_direct(&self) -> bool {
105        self.gap_type == BandGapType::Direct
106    }
107}
108
109// ---------------------------------------------------------------------------
110// Effective density of states
111// ---------------------------------------------------------------------------
112
113/// Compute the effective density of states in the conduction band (1/m³).
114///
115/// N_C = 2 (2π m_e* k_B T / h²)^(3/2)
116pub 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
121/// Compute the effective density of states in the valence band (1/m³).
122///
123/// N_V = 2 (2π m_h* k_B T / h²)^(3/2)
124pub 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
129// ---------------------------------------------------------------------------
130// Intrinsic carrier concentration
131// ---------------------------------------------------------------------------
132
133/// Intrinsic carrier concentration n_i (1/m³).
134///
135/// n_i = sqrt(N_C * N_V) * exp(-E_g / (2 k_B T))
136pub 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
140/// Convenience: intrinsic carrier concentration for silicon at given T.
141pub fn intrinsic_carrier_si(t_kelvin: f64) -> f64 {
142    let bg = BandGapModel::silicon();
143    let eg_j = bg.band_gap_j(t_kelvin);
144    // Effective mass ratios for Si: m_de* ≈ 1.08, m_dh* ≈ 0.81
145    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
150// ---------------------------------------------------------------------------
151// Extrinsic carrier concentration
152// ---------------------------------------------------------------------------
153
154/// Extrinsic carrier concentrations for an n-type semiconductor.
155///
156/// Returns (n, p) where n is electron concentration, p is hole concentration.
157/// Assumes complete ionisation: n ≈ N_D, p = n_i² / n.
158pub 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
164/// Extrinsic carrier concentrations for a p-type semiconductor.
165///
166/// Returns (n, p) where p ≈ N_A, n = n_i² / p.
167pub 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
173/// Compensated semiconductor carrier concentration.
174///
175/// For N_D > N_A: n ≈ N_D - N_A, p = n_i² / n.
176/// For N_A > N_D: p ≈ N_A - N_D, n = n_i² / p.
177pub 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
189// ---------------------------------------------------------------------------
190// Fermi level
191// ---------------------------------------------------------------------------
192
193/// Intrinsic Fermi level position relative to conduction band edge (eV).
194///
195/// E_i = (E_c + E_v)/2 + (k_B T / 2) ln(N_V / N_C)
196/// Here we return E_F - E_v for intrinsic: E_g/2 + (k_B T / 2) ln(N_V / N_C).
197pub 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
202/// Fermi level for n-type semiconductor (eV above valence band edge).
203///
204/// E_F - E_v = E_g + k_B T ln(n / N_C) ... but measured from E_v:
205/// E_F = E_c + k_B T ln(n / N_C) = (E_v + E_g) + k_B T ln(n / N_C)
206/// Relative to E_v: E_F - E_v = E_g + k_B T ln(n / N_C).
207pub 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
212/// Fermi level for p-type semiconductor (eV above valence band edge).
213///
214/// E_F - E_v = -k_B T ln(p / N_V).
215pub 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// ---------------------------------------------------------------------------
221// Mobility models (Caughey-Thomas)
222// ---------------------------------------------------------------------------
223
224/// Caughey-Thomas mobility model parameters.
225///
226/// μ = μ_min + (μ_max - μ_min) / (1 + (N / N_ref)^α)
227#[derive(Clone, Debug)]
228pub struct CaugheyThomasMobility {
229    /// Minimum mobility (m²/V·s).
230    pub mu_min: f64,
231    /// Maximum (lattice) mobility (m²/V·s).
232    pub mu_max: f64,
233    /// Reference doping concentration (1/m³).
234    pub n_ref: f64,
235    /// Exponent α.
236    pub alpha: f64,
237}
238
239impl CaugheyThomasMobility {
240    /// Create a new Caughey-Thomas model.
241    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    /// Electron mobility model for silicon at 300 K.
251    ///
252    /// Parameters from Caughey & Thomas (1967) converted to SI.
253    pub fn si_electron() -> Self {
254        Self::new(
255            0.0065, // 65 cm²/V·s → 0.0065 m²/V·s
256            0.1414, // 1414 cm²/V·s
257            9.2e22, // 9.2e16 cm⁻³ → 9.2e22 m⁻³
258            0.711,
259        )
260    }
261
262    /// Hole mobility model for silicon at 300 K.
263    pub fn si_hole() -> Self {
264        Self::new(
265            0.00474, // 47.4 cm²/V·s
266            0.0471,  // 471 cm²/V·s
267            2.23e23, // 2.23e17 cm⁻³ → 2.23e23 m⁻³
268            0.719,
269        )
270    }
271
272    /// Compute mobility at doping concentration `n_doping` (1/m³).
273    ///
274    /// Returns mobility in m²/V·s.
275    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
280/// High-field saturation velocity model.
281///
282/// μ_eff = μ_low / (1 + (μ_low E / v_sat))
283/// where `e_field` is the electric field (V/m) and `v_sat` is saturation
284/// velocity (m/s).
285pub 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
289/// Drift velocity under electric field (m/s).
290pub fn drift_velocity(mobility: f64, e_field: f64) -> f64 {
291    mobility * e_field
292}
293
294// ---------------------------------------------------------------------------
295// Resistivity and conductivity
296// ---------------------------------------------------------------------------
297
298/// Electrical conductivity σ = q (n μ_n + p μ_p) (S/m).
299pub fn conductivity(n: f64, mu_n: f64, p: f64, mu_p: f64) -> f64 {
300    Q_E * (n * mu_n + p * mu_p)
301}
302
303/// Resistivity ρ = 1/σ (Ω·m).
304pub 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
308/// Sheet resistance for a thin layer of thickness `t` (Ω/□).
309pub fn sheet_resistance(rho: f64, thickness: f64) -> f64 {
310    rho / thickness
311}
312
313// ---------------------------------------------------------------------------
314// Hall effect
315// ---------------------------------------------------------------------------
316
317/// Hall coefficient R_H for a predominantly n-type semiconductor (m³/C).
318///
319/// R_H = -1 / (n q) for electrons.
320pub fn hall_coefficient_n_type(n: f64) -> f64 {
321    -1.0 / (n * Q_E)
322}
323
324/// Hall coefficient R_H for a predominantly p-type semiconductor (m³/C).
325///
326/// R_H = +1 / (p q) for holes.
327pub fn hall_coefficient_p_type(p: f64) -> f64 {
328    1.0 / (p * Q_E)
329}
330
331/// Hall voltage for a sample of thickness `d` carrying current `i_current`
332/// in magnetic field `b_field` (T).
333///
334/// V_H = R_H * I * B / d.
335pub 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
339/// Hall mobility μ_H = |R_H| σ (m²/V·s).
340pub fn hall_mobility(r_h: f64, sigma: f64) -> f64 {
341    r_h.abs() * sigma
342}
343
344/// Determine carrier type and concentration from Hall coefficient.
345///
346/// Returns (carrier_concentration, is_n_type).
347pub 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
353// ---------------------------------------------------------------------------
354// Debye length
355// ---------------------------------------------------------------------------
356
357/// Extrinsic Debye length (m).
358///
359/// L_D = sqrt(ε_s ε_0 k_B T / (q² n))
360/// where `eps_r` is relative permittivity and `n_carrier` is the majority
361/// carrier concentration (1/m³).
362pub 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
366/// Intrinsic Debye length using intrinsic carrier concentration.
367pub fn intrinsic_debye_length(eps_r: f64, t_kelvin: f64, ni: f64) -> f64 {
368    debye_length(eps_r, t_kelvin, ni)
369}
370
371// ---------------------------------------------------------------------------
372// PN junction
373// ---------------------------------------------------------------------------
374
375/// PN junction parameters.
376#[derive(Clone, Debug)]
377pub struct PnJunction {
378    /// Donor concentration in n-region (1/m³).
379    pub n_d: f64,
380    /// Acceptor concentration in p-region (1/m³).
381    pub n_a: f64,
382    /// Intrinsic carrier concentration (1/m³).
383    pub ni: f64,
384    /// Relative permittivity of the semiconductor.
385    pub eps_r: f64,
386    /// Temperature (K).
387    pub temperature: f64,
388    /// Junction area (m²).
389    pub area: f64,
390}
391
392impl PnJunction {
393    /// Create a new PN junction.
394    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    /// Typical silicon PN junction at 300 K.
406    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    /// Thermal voltage V_T = k_B T / q (V).
412    pub fn thermal_voltage(&self) -> f64 {
413        K_B * self.temperature / Q_E
414    }
415
416    /// Built-in potential V_bi (V).
417    ///
418    /// V_bi = (k_B T / q) ln(N_A N_D / n_i²)
419    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    /// Depletion width W at reverse bias `v_r` (V, positive for reverse).
424    ///
425    /// W = sqrt(2 ε_s ε_0 (V_bi + V_R) (1/N_A + 1/N_D) / q)
426    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    /// Depletion width on the n-side (m).
433    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    /// Depletion width on the p-side (m).
439    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    /// Maximum electric field at the junction (V/m).
445    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    /// Junction capacitance per unit area (F/m²).
452    ///
453    /// C_j = ε_s ε_0 / W
454    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    /// Total junction capacitance (F).
461    pub fn junction_capacitance(&self, v_r: f64) -> f64 {
462        self.junction_capacitance_per_area(v_r) * self.area
463    }
464
465    /// Diode saturation current I_0 (A).
466    ///
467    /// I_0 = A q n_i² (D_n/(L_n N_A) + D_p/(L_p N_D))
468    /// Simplified using typical Si diffusion coefficients.
469    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    /// Ideal diode current I(V) = I_0 (exp(V/V_T) - 1) (A).
474    pub fn diode_current(&self, voltage: f64, i0: f64) -> f64 {
475        i0 * ((voltage / self.thermal_voltage()).exp() - 1.0)
476    }
477
478    /// Diode current with ideality factor n.
479    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    /// Breakdown voltage estimate (V) using critical field `e_crit` (V/m).
484    ///
485    /// V_BD ≈ ε_s ε_0 E_crit² / (2 q N_B) where N_B = min(N_A, N_D).
486    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// ---------------------------------------------------------------------------
494// Schottky barrier
495// ---------------------------------------------------------------------------
496
497/// Schottky barrier diode model.
498#[derive(Clone, Debug)]
499pub struct SchottkyBarrier {
500    /// Metal work function (eV).
501    pub phi_m_ev: f64,
502    /// Semiconductor electron affinity (eV).
503    pub chi_s_ev: f64,
504    /// Temperature (K).
505    pub temperature: f64,
506    /// Effective Richardson constant (A/m²·K²).
507    pub a_star: f64,
508    /// Contact area (m²).
509    pub area: f64,
510    /// Semiconductor relative permittivity.
511    pub eps_r: f64,
512    /// Doping concentration (1/m³).
513    pub n_doping: f64,
514}
515
516impl SchottkyBarrier {
517    /// Create a new Schottky barrier model.
518    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    /// Barrier height φ_B = φ_m - χ_s (eV).
539    pub fn barrier_height_ev(&self) -> f64 {
540        self.phi_m_ev - self.chi_s_ev
541    }
542
543    /// Thermal voltage (V).
544    pub fn thermal_voltage(&self) -> f64 {
545        K_B * self.temperature / Q_E
546    }
547
548    /// Saturation current density J_0 = A* T² exp(-q φ_B / k_B T) (A/m²).
549    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    /// Saturation current I_0 = J_0 * A (A).
558    pub fn saturation_current(&self) -> f64 {
559        self.saturation_current_density() * self.area
560    }
561
562    /// Forward current at voltage `v` (A).
563    ///
564    /// I = I_0 (exp(V / V_T) - 1)
565    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    /// Schottky depletion width at reverse bias `v_r` (m).
571    pub fn depletion_width(&self, v_r: f64) -> f64 {
572        let phi_b = self.barrier_height_ev() * Q_E / Q_E; // in V
573        let eps = self.eps_r * EPSILON_0;
574        (2.0 * eps * (phi_b + v_r) / (Q_E * self.n_doping)).sqrt()
575    }
576
577    /// Schottky barrier capacitance per unit area (F/m²).
578    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    /// Image-force barrier lowering Δφ (eV).
584    ///
585    /// Δφ = sqrt(q E_max / (4π ε_s ε_0))
586    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// ---------------------------------------------------------------------------
593// Photovoltaic: Shockley-Queisser limit
594// ---------------------------------------------------------------------------
595
596/// Shockley-Queisser single-junction photovoltaic model.
597#[derive(Clone, Debug)]
598pub struct ShockleyQueisser {
599    /// Band gap (eV).
600    pub eg_ev: f64,
601    /// Cell temperature (K).
602    pub temperature: f64,
603    /// Sun temperature (K), default 5778 K.
604    pub t_sun: f64,
605    /// Concentration factor (1 for 1-sun).
606    pub concentration: f64,
607}
608
609impl ShockleyQueisser {
610    /// Create a new Shockley-Queisser model.
611    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    /// With concentration ratio.
621    pub fn with_concentration(mut self, c: f64) -> Self {
622        self.concentration = c;
623        self
624    }
625
626    /// Thermal voltage (V).
627    pub fn thermal_voltage(&self) -> f64 {
628        K_B * self.temperature / Q_E
629    }
630
631    /// Approximate short-circuit current density (A/m²).
632    ///
633    /// Integrates Planck spectrum above E_g. Uses simplified analytical form:
634    /// J_sc ≈ q * (2π / (c² h³)) * (k_B T_sun)³ * Σ_{n≥0} ...
635    /// We use the blackbody photon flux approximation.
636    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        // Approximate photon flux above E_g using analytical integral.
641        // Φ ≈ (2π/(c²h³)) (k_B T_sun)³ [x² + 2x + 2] exp(-x)
642        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        // Scale by geometric factor (sun solid angle / 2π) ≈ 2.18e-5 for 1 sun
645        let geometric = 2.18e-5 * self.concentration;
646        Q_E * flux * geometric
647    }
648
649    /// Open-circuit voltage (V).
650    ///
651    /// V_oc ≈ E_g/q - (k_B T / q) ln(J_0 / J_sc)
652    /// Simplified: V_oc ≈ E_g/q - k_B T/q * ln(...)
653    pub fn open_circuit_voltage(&self) -> f64 {
654        let jsc = self.short_circuit_current_density();
655        let vt = self.thermal_voltage();
656        // Radiative saturation current density
657        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    /// Radiative saturation current density J_0 (A/m²).
665    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    /// Fill factor estimate (empirical formula by Green).
674    ///
675    /// FF ≈ (v_oc - ln(v_oc + 0.72)) / (v_oc + 1)
676    /// where v_oc = V_oc / V_T (normalised).
677    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    /// Maximum power conversion efficiency η.
688    ///
689    /// η = J_sc * V_oc * FF / P_in
690    /// where P_in = σ T_sun⁴ * (geometric factor).
691    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    /// Maximum power density (W/m²).
704    pub fn max_power_density(&self) -> f64 {
705        self.short_circuit_current_density() * self.open_circuit_voltage() * self.fill_factor()
706    }
707}
708
709// ---------------------------------------------------------------------------
710// Thermoelectric effects
711// ---------------------------------------------------------------------------
712
713/// Thermoelectric material parameters.
714#[derive(Clone, Debug)]
715pub struct ThermoelectricMaterial {
716    /// Seebeck coefficient S (V/K).
717    pub seebeck: f64,
718    /// Electrical conductivity σ (S/m).
719    pub sigma: f64,
720    /// Thermal conductivity κ (W/m·K).
721    pub kappa: f64,
722    /// Temperature (K).
723    pub temperature: f64,
724}
725
726impl ThermoelectricMaterial {
727    /// Create a new thermoelectric material.
728    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    /// Typical Bi₂Te₃ at 300 K.
738    pub fn bi2te3() -> Self {
739        Self::new(200e-6, 1.0e5, 1.5, 300.0)
740    }
741
742    /// Thermoelectric figure of merit ZT = S² σ T / κ.
743    pub fn zt(&self) -> f64 {
744        self.seebeck * self.seebeck * self.sigma * self.temperature / self.kappa
745    }
746
747    /// Power factor S² σ (W/m·K²).
748    pub fn power_factor(&self) -> f64 {
749        self.seebeck * self.seebeck * self.sigma
750    }
751
752    /// Peltier coefficient Π = S T (V).
753    pub fn peltier_coefficient(&self) -> f64 {
754        self.seebeck * self.temperature
755    }
756
757    /// Thomson coefficient μ_T = T dS/dT.
758    ///
759    /// Approximation: given Seebeck at two temperatures.
760    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    /// Seebeck voltage for temperature difference ΔT (V).
767    pub fn seebeck_voltage(&self, delta_t: f64) -> f64 {
768        self.seebeck * delta_t
769    }
770
771    /// Peltier heat flux Q = Π I = S T I (W) for current `i_current` (A).
772    pub fn peltier_heat(&self, i_current: f64) -> f64 {
773        self.peltier_coefficient() * i_current
774    }
775
776    /// Maximum cooling ΔT for a thermoelectric cooler.
777    ///
778    /// ΔT_max = ZT_c² / 2 * T_c  (simplified).
779    /// More precisely: ΔT_max ≈ 0.5 * ZT * T_c for ZT not too large.
780    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    /// Maximum COP of thermoelectric cooler.
785    ///
786    /// COP_max = (T_c / ΔT) * ((1 + ZT_m)^0.5 - T_h/T_c) / ((1 + ZT_m)^0.5 + 1)
787    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    /// Thermoelectric generator efficiency.
798    ///
799    /// η = (ΔT/T_h) * (√(1+ZT) - 1) / (√(1+ZT) + T_c/T_h)
800    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    /// Electrical resistivity ρ = 1/σ (Ω·m).
812    pub fn resistivity(&self) -> f64 {
813        1.0 / self.sigma
814    }
815
816    /// Lorenz number estimate L = κ / (σ T) (W·Ω/K²).
817    pub fn lorenz_number(&self) -> f64 {
818        self.kappa / (self.sigma * self.temperature)
819    }
820}
821
822/// Thermoelectric module (n-p couple) power output.
823///
824/// Returns (power, voltage, current) for given ΔT and load resistance.
825pub 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
838/// Maximum thermoelectric module power (at matched load R_L = R_int).
839pub 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
844// ---------------------------------------------------------------------------
845// Additional utility functions
846// ---------------------------------------------------------------------------
847
848/// Einstein relation: D = μ k_B T / q (m²/s).
849pub fn einstein_diffusivity(mobility: f64, t_kelvin: f64) -> f64 {
850    mobility * K_B * t_kelvin / Q_E
851}
852
853/// Diffusion length L = √(D τ) (m).
854pub fn diffusion_length(diffusivity: f64, lifetime: f64) -> f64 {
855    (diffusivity * lifetime).sqrt()
856}
857
858/// Generation rate for photon absorption (1/m³·s).
859///
860/// G = α Φ exp(-α x) where α is absorption coefficient (1/m),
861/// Φ is photon flux (1/m²·s), and x is depth (m).
862pub fn generation_rate(alpha: f64, photon_flux: f64, depth: f64) -> f64 {
863    alpha * photon_flux * (-alpha * depth).exp()
864}
865
866/// Recombination rate (Shockley-Read-Hall) for a single trap level.
867///
868/// R_SRH = (n p - n_i²) / (τ_p (n + n_1) + τ_n (p + p_1))
869pub 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
878/// Auger recombination rate (1/m³·s).
879///
880/// R_Auger = C_n n² p + C_p n p²
881pub 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
885/// Radiative recombination rate (1/m³·s).
886///
887/// R_rad = B (n p - n_i²)
888pub fn recombination_radiative(n: f64, p: f64, ni: f64, b_coeff: f64) -> f64 {
889    b_coeff * (n * p - ni * ni)
890}
891
892/// Total recombination lifetime combining SRH, Auger, and radiative.
893///
894/// 1/τ_eff = 1/τ_SRH + 1/τ_Auger + 1/τ_rad
895pub 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
899/// Absorption coefficient for direct band gap (simplified parabolic model).
900///
901/// α = A √(E - E_g)   for E > E_g
902pub 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
910/// Absorption coefficient for indirect band gap (simplified model).
911///
912/// α = B (E - E_g ± E_phonon)² / E  (phonon-assisted transitions).
913pub fn absorption_indirect(
914    photon_energy_ev: f64,
915    eg_ev: f64,
916    e_phonon_ev: f64,
917    b_coeff: f64,
918) -> f64 {
919    // Phonon absorption process
920    let e_abs = photon_energy_ev - eg_ev + e_phonon_ev;
921    // Phonon emission process
922    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
933/// Built-in potential for a heterojunction (eV).
934///
935/// V_bi = (χ₁ - χ₂) + (E_g2 - E_g1) + k_B T/q ln(N_A N_D / (n_i1 n_i2))
936pub 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
951/// Metal-semiconductor contact: Ohmic or Schottky determination.
952///
953/// Returns true if the contact is Schottky (rectifying) for n-type.
954/// Schottky condition: φ_m > χ_s for n-type.
955pub fn is_schottky_n_type(phi_m_ev: f64, chi_s_ev: f64) -> bool {
956    phi_m_ev > chi_s_ev
957}
958
959/// Metal-semiconductor contact: Ohmic or Schottky for p-type.
960///
961/// Schottky condition: φ_m < χ_s + E_g for p-type.
962pub 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// ---------------------------------------------------------------------------
967// Tests
968// ---------------------------------------------------------------------------
969
970#[cfg(test)]
971mod tests {
972    use super::*;
973
974    const TOL: f64 = 1e-6;
975
976    // 1. Band gap: Si at 300 K ≈ 1.12 eV
977    #[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    // 2. Band gap: GaAs at 0 K
989    #[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    // 3. Band gap type identification
1001    #[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    // 4. Effective DOS positive and increases with temperature
1010    #[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    // 5. Intrinsic carrier concentration: Si at 300 K ≈ 1e16 m⁻³ (1e10 cm⁻³)
1019    #[test]
1020    fn test_intrinsic_carrier_si() {
1021        let ni = intrinsic_carrier_si(300.0);
1022        // ni for Si at 300 K is about 1.0e10 cm⁻³ = 1.0e16 m⁻³
1023        assert!(
1024            ni > 1e14 && ni < 1e18,
1025            "Si ni at 300K should be ~1e16 m⁻³, got {:.6e}",
1026            ni
1027        );
1028    }
1029
1030    // 6. Extrinsic n-type: n ≈ N_D, p << n
1031    #[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    // 7. Extrinsic p-type: p ≈ N_A, n << p
1041    #[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    // 8. Caughey-Thomas: low doping → high mobility, high doping → low mobility
1051    #[test]
1052    fn test_caughey_thomas_mobility() {
1053        let ct = CaugheyThomasMobility::si_electron();
1054        let mu_low = ct.mobility(1e18); // low doping
1055        let mu_high = ct.mobility(1e26); // very high doping
1056        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    // 9. Conductivity is positive
1067    #[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    // 10. Resistivity = 1/σ
1078    #[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    // 11. Hall coefficient sign: n-type → negative
1089    #[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    // 12. Hall carrier analysis roundtrip
1098    #[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    // 13. Debye length positive and decreases with higher carrier concentration
1111    #[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    // 14. PN junction built-in potential > 0
1125    #[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    // 15. PN junction depletion width increases with reverse bias
1137    #[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    // 16. PN junction capacitance decreases with reverse bias
1151    #[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    // 17. Diode current: forward > 0, reverse ≈ -I_0
1160    #[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    // 18. Schottky barrier height
1174    #[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    // 19. Schottky: forward current increases exponentially
1186    #[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    // 20. Shockley-Queisser: Jsc > 0 for reasonable band gap
1198    #[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    // 21. Shockley-Queisser: efficiency between 0 and 1
1206    #[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    // 22. Thermoelectric ZT for Bi2Te3 ≈ 0.8
1218    #[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    // 23. Seebeck voltage proportional to ΔT
1230    #[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    // 24. Peltier coefficient Π = S T
1242    #[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    // 25. Einstein relation: D = μ k_B T / q
1254    #[test]
1255    fn test_einstein_relation() {
1256        let mu = 0.14; // electron mobility in Si (m²/V·s)
1257        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    // 26. Diffusion length positive
1269    #[test]
1270    fn test_diffusion_length() {
1271        let d = 36e-4; // typical Si electron diffusivity (m²/s)
1272        let tau = 1e-6; // lifetime (s)
1273        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    // 27. Thermoelectric generator efficiency bounded by Carnot
1280    #[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    // 28. Thermoelectric max power at matched load
1296    #[test]
1297    fn test_thermoelectric_max_power() {
1298        let s_np = 400e-6; // combined Seebeck (V/K)
1299        let r_int = 1.0; // internal resistance (Ω)
1300        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    // 29. SRH recombination: equilibrium → R = 0
1307    #[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    // 30. Compensated doping: N_D = N_A → intrinsic behavior
1319    #[test]
1320    fn test_compensated_doping() {
1321        let ni = 1e16;
1322        let n_d = 1e22;
1323        // Slight excess n-type
1324        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}