Skip to main content

oxiphysics_materials/
quantum_materials.rs

1// Copyright 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Quantum materials properties: band structure, Fermi statistics, carrier
5//! transport, thermoelectric effects, and optical response.
6//!
7//! All energies are in electron-volts (eV) unless stated otherwise.
8//! Temperature is in Kelvin.
9
10#![allow(dead_code)]
11#![allow(clippy::too_many_arguments)]
12
13use std::f64::consts::PI;
14
15/// Boltzmann constant in eV/K.
16pub const K_B_EV: f64 = 8.617_333_262e-5;
17/// Boltzmann constant in J/K.
18pub const K_B_J: f64 = 1.380_649e-23;
19/// Electron charge in Coulombs.
20pub const E_CHARGE: f64 = 1.602_176_634e-19;
21/// Free electron mass in kg.
22pub const M_ELECTRON: f64 = 9.109_383_701_5e-31;
23/// Reduced Planck constant in J·s.
24pub const HBAR: f64 = 1.054_571_817e-34;
25
26// ─────────────────────────────────────────────────────────────────────────────
27// Core struct
28// ─────────────────────────────────────────────────────────────────────────────
29
30/// Quantum material descriptor capturing electronic structure parameters.
31///
32/// All energy quantities are in eV.
33#[derive(Debug, Clone)]
34pub struct QuantumMaterial {
35    /// Band gap energy (eV). 0.0 for metals.
36    pub band_gap: f64,
37    /// Fermi energy measured from valence-band maximum (eV).
38    pub fermi_energy: f64,
39    /// Effective carrier mass as a fraction of the free electron mass (dimensionless).
40    pub effective_mass: f64,
41    /// Carrier mobility at 300 K in m²/(V·s).
42    pub mobility_300k: f64,
43    /// Optical scattering time (Drude relaxation time) in seconds.
44    pub scattering_time: f64,
45    /// Material name.
46    pub name: String,
47}
48
49impl QuantumMaterial {
50    /// Create a new `QuantumMaterial`.
51    ///
52    /// # Arguments
53    /// * `band_gap` — band gap in eV
54    /// * `fermi_energy` — Fermi energy in eV
55    /// * `effective_mass` — effective mass ratio m*/m_e
56    /// * `mobility_300k` — mobility at 300 K in m²/(V·s)
57    /// * `scattering_time` — Drude relaxation time in seconds
58    /// * `name` — material identifier
59    pub fn new(
60        band_gap: f64,
61        fermi_energy: f64,
62        effective_mass: f64,
63        mobility_300k: f64,
64        scattering_time: f64,
65        name: impl Into<String>,
66    ) -> Self {
67        Self {
68            band_gap,
69            fermi_energy,
70            effective_mass,
71            mobility_300k,
72            scattering_time,
73            name: name.into(),
74        }
75    }
76
77    /// Silicon preset (indirect band gap semiconductor).
78    pub fn silicon() -> Self {
79        Self::new(1.12, 0.56, 0.26, 0.135, 3.0e-13, "Silicon")
80    }
81
82    /// GaAs preset (direct band gap III-V semiconductor).
83    pub fn gaas() -> Self {
84        Self::new(1.42, 0.71, 0.063, 0.85, 1.0e-13, "GaAs")
85    }
86
87    /// Copper preset (metal, effectively zero band gap).
88    pub fn copper() -> Self {
89        Self::new(0.0, 7.04, 1.0, 3.2e-3, 2.5e-14, "Copper")
90    }
91}
92
93// ─────────────────────────────────────────────────────────────────────────────
94// Statistical functions
95// ─────────────────────────────────────────────────────────────────────────────
96
97/// Fermi-Dirac distribution function f(E, E_F, T).
98///
99/// Returns the probability that a state at energy `energy` (eV) is occupied
100/// at temperature `temp_k` (K) with Fermi level `fermi_energy` (eV).
101///
102/// At T → 0 K this becomes a Heaviside step: f = 1 for E < E_F, f = 0 for E > E_F.
103pub fn fermi_dirac(energy: f64, fermi_energy: f64, temp_k: f64) -> f64 {
104    if temp_k <= 0.0 {
105        if energy < fermi_energy {
106            1.0
107        } else if (energy - fermi_energy).abs() < 1e-12 {
108            0.5
109        } else {
110            0.0
111        }
112    } else {
113        let kbt = K_B_EV * temp_k;
114        let x = (energy - fermi_energy) / kbt;
115        // Numerically stable form to avoid overflow.
116        if x > 500.0 {
117            0.0
118        } else if x < -500.0 {
119            1.0
120        } else {
121            1.0 / (1.0 + x.exp())
122        }
123    }
124}
125
126/// Parabolic-band density of states (DOS) for a 3D semiconductor (states/eV/m³).
127///
128/// D(E) = (1/2π²) × (2 m* / ℏ²)^(3/2) × sqrt(E - E_c) for E > E_c
129///
130/// # Arguments
131/// * `energy` — energy of interest (eV), relative to the band edge
132/// * `effective_mass_ratio` — m*/m_e (dimensionless)
133///
134/// Returns zero for `energy` ≤ 0 (below the band edge).
135pub fn density_of_states(energy: f64, effective_mass_ratio: f64) -> f64 {
136    if energy <= 0.0 {
137        return 0.0;
138    }
139    let m_star = effective_mass_ratio * M_ELECTRON;
140    let energy_j = energy * E_CHARGE; // eV → J
141    // D(E) = (1/(2π²)) * (2 m* / ħ²)^(3/2) * sqrt(E)
142    let prefactor = (1.0 / (2.0 * PI * PI)) * (2.0 * m_star / (HBAR * HBAR)).powf(1.5);
143    let dos_per_j = prefactor * energy_j.sqrt();
144    // Convert states/J/m³ → states/eV/m³
145    dos_per_j * E_CHARGE
146}
147
148/// Electron carrier concentration n (m⁻³) using the Maxwell-Boltzmann approximation
149/// (non-degenerate semiconductor).
150///
151/// n = N_c × exp(-(E_c - E_F) / k_B T)
152///
153/// where N_c = 2 (2π m* k_B T / h²)^(3/2) is the effective density of states.
154///
155/// # Arguments
156/// * `band_gap` — band gap E_g (eV)
157/// * `fermi_energy` — Fermi level E_F measured from valence-band maximum (eV)
158/// * `effective_mass_ratio` — m*/m_e
159/// * `temp_k` — temperature (K)
160pub fn carrier_concentration(
161    band_gap: f64,
162    fermi_energy: f64,
163    effective_mass_ratio: f64,
164    temp_k: f64,
165) -> f64 {
166    if temp_k <= 0.0 {
167        return 0.0;
168    }
169    let m_star = effective_mass_ratio * M_ELECTRON;
170    let kbt_j = K_B_J * temp_k;
171    let kbt_ev = K_B_EV * temp_k;
172    // Effective DOS at conduction band edge.
173    let h = 2.0 * PI * HBAR;
174    let n_c = 2.0 * (2.0 * PI * m_star * kbt_j / (h * h)).powf(1.5);
175    // Conduction band edge relative to valence band maximum.
176    let e_c = band_gap;
177    // Activation exponent.
178    let delta = (e_c - fermi_energy) / kbt_ev;
179    if delta > 500.0 {
180        return 0.0;
181    }
182    n_c * (-delta).exp()
183}
184
185// ─────────────────────────────────────────────────────────────────────────────
186// Transport properties
187// ─────────────────────────────────────────────────────────────────────────────
188
189/// Phonon-scattering mobility as a function of temperature.
190///
191/// Uses the empirical power-law model: μ(T) = μ₃₀₀ × (300 / T)^α
192/// where α ≈ 1.5 for acoustic-phonon scattering (Boltzmann limit).
193///
194/// # Arguments
195/// * `mobility_300k` — mobility at 300 K (m²/(V·s))
196/// * `temp_k` — temperature (K)
197/// * `alpha` — scattering exponent (typically 1.5 for acoustic phonons)
198///
199/// Returns mobility in m²/(V·s).
200pub fn mobility_temperature(mobility_300k: f64, temp_k: f64, alpha: f64) -> f64 {
201    if temp_k <= 0.0 {
202        return 0.0;
203    }
204    mobility_300k * (300.0 / temp_k).powf(alpha)
205}
206
207/// Seebeck coefficient (thermopower) S in V/K for a parabolic-band semiconductor.
208///
209/// S = -(k_B / e) × \[(E_c - E_F)/(k_B T) + (r + 5/2)\]
210///
211/// where r is the scattering exponent (r = -1/2 for acoustic phonons).
212///
213/// # Arguments
214/// * `band_gap` — E_g (eV)
215/// * `fermi_energy` — E_F from valence-band maximum (eV)
216/// * `temp_k` — temperature (K)
217/// * `r_scatter` — scattering exponent (default: -0.5)
218pub fn seebeck_coefficient(band_gap: f64, fermi_energy: f64, temp_k: f64, r_scatter: f64) -> f64 {
219    if temp_k <= 0.0 {
220        return 0.0;
221    }
222    let kbt_ev = K_B_EV * temp_k;
223    let e_c = band_gap;
224    let eta = (e_c - fermi_energy) / kbt_ev; // reduced Fermi energy
225    // S = -(k_B/e) * [eta + (r + 5/2)]
226
227    -(K_B_EV / 1.0) * (eta + r_scatter + 2.5) // units: eV/K (numerically same as V/K since e cancels)
228}
229
230/// Hall coefficient R_H (m³/C) for an n-type semiconductor.
231///
232/// R_H = -r_H / (n × e)
233///
234/// where r_H is the Hall scattering factor (≈ 1 for acoustic phonons) and n is
235/// the electron concentration.
236///
237/// # Arguments
238/// * `carrier_density` — electron concentration (m⁻³)
239/// * `hall_factor` — Hall scattering factor r_H (dimensionless, typically ≈ 1)
240///
241/// Returns R_H in m³/C (negative for electrons, positive for holes).
242pub fn hall_coefficient(carrier_density: f64, hall_factor: f64) -> f64 {
243    if carrier_density <= 0.0 {
244        return 0.0;
245    }
246    -hall_factor / (carrier_density * E_CHARGE)
247}
248
249// ─────────────────────────────────────────────────────────────────────────────
250// Optical properties
251// ─────────────────────────────────────────────────────────────────────────────
252
253/// Drude-model optical conductivity σ(ω) (S/m) at angular frequency ω (rad/s).
254///
255/// σ(ω) = σ₀ / (1 + (ω τ)²)
256///
257/// where σ₀ = n e² τ / m* is the DC conductivity.
258///
259/// # Arguments
260/// * `carrier_density` — carrier concentration (m⁻³)
261/// * `effective_mass_ratio` — m*/m_e
262/// * `scattering_time` — Drude relaxation time τ (s)
263/// * `omega` — angular frequency (rad/s)
264///
265/// Returns the real part of the optical conductivity in S/m.
266pub fn optical_conductivity(
267    carrier_density: f64,
268    effective_mass_ratio: f64,
269    scattering_time: f64,
270    omega: f64,
271) -> f64 {
272    let m_star = effective_mass_ratio * M_ELECTRON;
273    let sigma0 = carrier_density * E_CHARGE * E_CHARGE * scattering_time / m_star;
274    let omega_tau = omega * scattering_time;
275    sigma0 / (1.0 + omega_tau * omega_tau)
276}
277
278/// Plasma frequency ω_p (rad/s) of a free-carrier system.
279///
280/// ω_p = sqrt(n e² / (ε₀ m*))
281///
282/// # Arguments
283/// * `carrier_density` — n (m⁻³)
284/// * `effective_mass_ratio` — m*/m_e
285/// * `epsilon_r` — relative permittivity of the host lattice
286pub fn plasma_frequency(carrier_density: f64, effective_mass_ratio: f64, epsilon_r: f64) -> f64 {
287    let epsilon_0 = 8.854_187_817e-12_f64; // F/m
288    let m_star = effective_mass_ratio * M_ELECTRON;
289    (carrier_density * E_CHARGE * E_CHARGE / (epsilon_0 * epsilon_r * m_star)).sqrt()
290}
291
292// ─────────────────────────────────────────────────────────────────────────────
293// Tests
294// ─────────────────────────────────────────────────────────────────────────────
295
296#[cfg(test)]
297mod tests {
298    use super::*;
299
300    const EPS: f64 = 1e-10;
301
302    // 1. Fermi-Dirac at T=0: below Fermi level → 1.
303    #[test]
304    fn test_fermi_dirac_t0_below() {
305        let f = fermi_dirac(0.5, 1.0, 0.0);
306        assert!(
307            (f - 1.0).abs() < EPS,
308            "FD at T=0, E<E_F should be 1, got {f}"
309        );
310    }
311
312    // 2. Fermi-Dirac at T=0: above Fermi level → 0.
313    #[test]
314    fn test_fermi_dirac_t0_above() {
315        let f = fermi_dirac(1.5, 1.0, 0.0);
316        assert!(f.abs() < EPS, "FD at T=0, E>E_F should be 0, got {f}");
317    }
318
319    // 3. Fermi-Dirac at T=0: exactly at Fermi level → 0.5.
320    #[test]
321    fn test_fermi_dirac_t0_at_ef() {
322        let f = fermi_dirac(1.0, 1.0, 0.0);
323        assert!(
324            (f - 0.5).abs() < EPS,
325            "FD at T=0, E=E_F should be 0.5, got {f}"
326        );
327    }
328
329    // 4. Fermi-Dirac at finite T: at Fermi level → exactly 0.5.
330    #[test]
331    fn test_fermi_dirac_at_fermi_level_finite_t() {
332        let f = fermi_dirac(1.0, 1.0, 300.0);
333        assert!(
334            (f - 0.5).abs() < EPS,
335            "FD at E=E_F for any T should be 0.5, got {f}"
336        );
337    }
338
339    // 5. Fermi-Dirac: high T thermal smearing, f at E_F+5kT ≈ 0.0067.
340    #[test]
341    fn test_fermi_dirac_thermal_tail() {
342        let kbt = K_B_EV * 1000.0;
343        let ef = 1.0;
344        let e = ef + 5.0 * kbt;
345        let f = fermi_dirac(e, ef, 1000.0);
346        let expected = 1.0 / (1.0 + 5.0_f64.exp());
347        assert!(
348            (f - expected).abs() < 1e-12,
349            "FD thermal tail mismatch: {f} vs {expected}"
350        );
351    }
352
353    // 6. Fermi-Dirac: very negative exponent → 1.
354    #[test]
355    fn test_fermi_dirac_large_negative() {
356        let f = fermi_dirac(-10.0, 1.0, 0.01); // Very cold, way below E_F
357        assert!(f > 0.99, "FD way below E_F at low T should be ~1, got {f}");
358    }
359
360    // 7. DOS is zero at band edge (E=0).
361    #[test]
362    fn test_dos_zero_at_band_edge() {
363        let d = density_of_states(0.0, 0.26);
364        assert!(d.abs() < EPS, "DOS at band edge should be 0, got {d}");
365    }
366
367    // 8. DOS increases with energy (parabolic band).
368    #[test]
369    fn test_dos_increases_with_energy() {
370        let d1 = density_of_states(0.1, 0.26);
371        let d2 = density_of_states(0.5, 0.26);
372        assert!(
373            d2 > d1,
374            "DOS should increase with energy in parabolic band, d1={d1}, d2={d2}"
375        );
376    }
377
378    // 9. DOS scales with effective mass (heavier mass → more states).
379    #[test]
380    fn test_dos_scales_with_mass() {
381        let d_light = density_of_states(0.3, 0.1);
382        let d_heavy = density_of_states(0.3, 1.0);
383        assert!(
384            d_heavy > d_light,
385            "Heavier effective mass should give larger DOS"
386        );
387    }
388
389    // 10. Carrier concentration increases with temperature.
390    #[test]
391    fn test_carrier_concentration_increases_with_temp() {
392        let n_low = carrier_concentration(1.12, 0.56, 0.26, 200.0);
393        let n_high = carrier_concentration(1.12, 0.56, 0.26, 400.0);
394        assert!(
395            n_high > n_low,
396            "Carrier conc should increase with T: n_low={n_low}, n_high={n_high}"
397        );
398    }
399
400    // 11. Carrier concentration at T=0 is zero.
401    #[test]
402    fn test_carrier_concentration_t0() {
403        let n = carrier_concentration(1.12, 0.56, 0.26, 0.0);
404        assert!(n.abs() < EPS, "Carrier conc at T=0 should be 0, got {n}");
405    }
406
407    // 12. Carrier concentration decreases with larger band gap.
408    #[test]
409    fn test_carrier_concentration_band_gap_effect() {
410        let n_small_gap = carrier_concentration(0.5, 0.25, 0.26, 300.0);
411        let n_large_gap = carrier_concentration(2.0, 1.0, 0.26, 300.0);
412        assert!(
413            n_small_gap > n_large_gap,
414            "Smaller band gap should yield higher carrier density"
415        );
416    }
417
418    // 13. Mobility decreases with temperature (alpha > 0).
419    #[test]
420    fn test_mobility_decreases_with_temp() {
421        let mu_low = mobility_temperature(0.135, 200.0, 1.5);
422        let mu_high = mobility_temperature(0.135, 400.0, 1.5);
423        assert!(
424            mu_high < mu_low,
425            "Mobility should decrease with temperature"
426        );
427    }
428
429    // 14. Mobility at 300 K equals the given reference value.
430    #[test]
431    fn test_mobility_at_300k() {
432        let mu = mobility_temperature(0.135, 300.0, 1.5);
433        assert!(
434            (mu - 0.135).abs() < 1e-12,
435            "Mobility at 300K should be 0.135, got {mu}"
436        );
437    }
438
439    // 15. Mobility at T=0 returns 0.
440    #[test]
441    fn test_mobility_t0() {
442        let mu = mobility_temperature(0.135, 0.0, 1.5);
443        assert!(mu.abs() < EPS, "Mobility at T=0 should be 0, got {mu}");
444    }
445
446    // 16. Seebeck coefficient is non-zero for a typical semiconductor.
447    #[test]
448    fn test_seebeck_nonzero() {
449        let s = seebeck_coefficient(1.12, 0.56, 300.0, -0.5);
450        assert!(
451            s.abs() > 1e-6,
452            "Seebeck coefficient should be non-zero, got {s}"
453        );
454    }
455
456    // 17. Seebeck coefficient at T=0 returns 0.
457    #[test]
458    fn test_seebeck_t0() {
459        let s = seebeck_coefficient(1.12, 0.56, 0.0, -0.5);
460        assert!(s.abs() < EPS, "Seebeck at T=0 should be 0, got {s}");
461    }
462
463    // 18. Seebeck sign: for E_F below conduction band, S < 0 (n-type).
464    #[test]
465    fn test_seebeck_n_type_negative() {
466        // E_F (0.56 eV) < E_c (band_gap = 1.12 eV): n-type → negative Seebeck
467        let s = seebeck_coefficient(1.12, 0.56, 300.0, -0.5);
468        assert!(s < 0.0, "n-type Seebeck should be negative, got {s}");
469    }
470
471    // 19. Hall coefficient is negative for electrons.
472    #[test]
473    fn test_hall_coefficient_negative_for_electrons() {
474        let rh = hall_coefficient(1e22, 1.0);
475        assert!(
476            rh < 0.0,
477            "Hall coefficient for n-type should be negative, got {rh}"
478        );
479    }
480
481    // 20. Hall coefficient magnitude: R_H = 1 / (n e) for r_H = 1.
482    #[test]
483    fn test_hall_coefficient_magnitude() {
484        let n = 1e22_f64;
485        let rh = hall_coefficient(n, 1.0);
486        let expected = -1.0 / (n * E_CHARGE);
487        assert!(
488            (rh - expected).abs() < EPS.abs(),
489            "Hall coefficient mismatch: {rh} vs {expected}"
490        );
491    }
492
493    // 21. Hall coefficient for zero carrier density returns 0.
494    #[test]
495    fn test_hall_coefficient_zero_density() {
496        let rh = hall_coefficient(0.0, 1.0);
497        assert!(
498            rh.abs() < EPS,
499            "Hall coefficient for zero density should be 0, got {rh}"
500        );
501    }
502
503    // 22. Optical conductivity at DC (ω=0) equals σ₀ = n e² τ / m*.
504    #[test]
505    fn test_optical_conductivity_dc() {
506        let n = 8.49e28_f64; // copper-like carrier density
507        let m_ratio = 1.0;
508        let tau = 2.5e-14;
509        let m_star = m_ratio * M_ELECTRON;
510        let sigma0 = n * E_CHARGE * E_CHARGE * tau / m_star;
511        let sigma = optical_conductivity(n, m_ratio, tau, 0.0);
512        assert!(
513            (sigma - sigma0).abs() / sigma0 < 1e-10,
514            "DC conductivity mismatch"
515        );
516    }
517
518    // 23. Optical conductivity decreases at high frequency.
519    #[test]
520    fn test_optical_conductivity_decreases_at_high_freq() {
521        let n = 8.49e28_f64;
522        let sigma_dc = optical_conductivity(n, 1.0, 2.5e-14, 0.0);
523        let sigma_hf = optical_conductivity(n, 1.0, 2.5e-14, 1e15);
524        assert!(
525            sigma_hf < sigma_dc,
526            "Conductivity should decrease at high frequency"
527        );
528    }
529
530    // 24. Plasma frequency is positive for finite carrier density.
531    #[test]
532    fn test_plasma_frequency_positive() {
533        let wp = plasma_frequency(8.49e28, 1.0, 1.0);
534        assert!(wp > 0.0, "Plasma frequency should be positive, got {wp}");
535    }
536
537    // 25. QuantumMaterial presets can be constructed without panicking.
538    #[test]
539    fn test_presets_construction() {
540        let si = QuantumMaterial::silicon();
541        let gaas = QuantumMaterial::gaas();
542        let cu = QuantumMaterial::copper();
543        assert!(si.band_gap > 0.0);
544        assert!(gaas.band_gap > 0.0);
545        assert!(cu.band_gap == 0.0);
546    }
547
548    // 26. Higher effective mass raises the carrier concentration (heavier N_c).
549    #[test]
550    fn test_carrier_concentration_mass_effect() {
551        let n_light = carrier_concentration(1.12, 0.56, 0.1, 300.0);
552        let n_heavy = carrier_concentration(1.12, 0.56, 1.0, 300.0);
553        assert!(
554            n_heavy > n_light,
555            "Heavier effective mass should increase N_c and hence n"
556        );
557    }
558
559    // 27. Fermi-Dirac is monotonically decreasing in energy at finite T.
560    #[test]
561    fn test_fermi_dirac_monotone() {
562        let ef = 1.0;
563        let t = 300.0;
564        let f1 = fermi_dirac(0.5, ef, t);
565        let f2 = fermi_dirac(1.0, ef, t);
566        let f3 = fermi_dirac(1.5, ef, t);
567        assert!(
568            f1 > f2 && f2 > f3,
569            "FD must be decreasing in energy: f1={f1} f2={f2} f3={f3}"
570        );
571    }
572
573    // 28. DOS has correct sqrt(E) dependence.
574    #[test]
575    fn test_dos_sqrt_scaling() {
576        let d1 = density_of_states(1.0, 0.26);
577        let d4 = density_of_states(4.0, 0.26);
578        let ratio = d4 / d1;
579        // sqrt(4)/sqrt(1) = 2
580        assert!(
581            (ratio - 2.0).abs() < 1e-6,
582            "DOS should scale as sqrt(E): ratio={ratio}"
583        );
584    }
585
586    // 29. Optical conductivity at ω = 1/τ equals σ₀/2 (half-power point).
587    #[test]
588    fn test_optical_conductivity_half_power() {
589        let n = 1e28_f64;
590        let tau = 1e-14;
591        let omega = 1.0 / tau; // ω τ = 1
592        let sigma = optical_conductivity(n, 1.0, tau, omega);
593        let sigma0 = optical_conductivity(n, 1.0, tau, 0.0);
594        assert!(
595            (sigma - sigma0 / 2.0).abs() / sigma0 < 1e-12,
596            "At ωτ=1, σ should be σ₀/2"
597        );
598    }
599
600    // 30. Silicon seebeck magnitude is in physically reasonable range (> 100 µV/K).
601    #[test]
602    fn test_seebeck_silicon_reasonable() {
603        // Typical silicon at moderate doping has |S| ~ 300-900 µV/K
604        let s = seebeck_coefficient(1.12, 0.56, 300.0, -0.5);
605        assert!(
606            s.abs() > 1e-4,
607            "Silicon Seebeck should be > 100 µV/K, got {s} V/K"
608        );
609    }
610}