use num_complex::Complex64;
const HBAR: f64 = 1.054_571_817e-34; const KB: f64 = 1.380_649e-23; const E_CHARGE: f64 = 1.602_176_634e-19; const EPS0: f64 = 8.854_187_817e-12; const SPEED_OF_LIGHT: f64 = 2.997_924_58e8; const FINE_STRUCTURE: f64 = 7.297_352_569_3e-3;
#[derive(Debug, Clone)]
pub struct GrapheneSheet {
pub fermi_energy_ev: f64,
pub relaxation_time_ps: f64,
pub temperature_k: f64,
}
impl GrapheneSheet {
pub fn new(fermi_energy_ev: f64, relaxation_time_ps: f64, temperature_k: f64) -> Self {
Self {
fermi_energy_ev,
relaxation_time_ps,
temperature_k,
}
}
#[inline]
fn fermi_energy_j(&self) -> f64 {
self.fermi_energy_ev * E_CHARGE
}
#[inline]
fn gamma(&self) -> f64 {
1.0 / (self.relaxation_time_ps * 1e-12)
}
pub fn intraband(&self, omega: f64) -> Complex64 {
let ef = self.fermi_energy_j();
let gamma = self.gamma();
let kt = KB * self.temperature_k;
let x = ef / (2.0 * kt);
let _thermal_factor = if x > 300.0 {
2.0 * ef
} else {
2.0 * kt * x.cosh().ln().mul_add(1.0, (2.0_f64).ln())
};
let thermal_factor = 2.0 * kt * ((2.0_f64 * x.cosh()).ln());
let prefactor = E_CHARGE * E_CHARGE / (std::f64::consts::PI * HBAR * HBAR);
let denom = Complex64::new(omega, gamma);
Complex64::new(0.0, 1.0) * prefactor * thermal_factor / denom
}
pub fn interband(&self, omega: f64) -> Complex64 {
let ef = self.fermi_energy_j();
let kt = KB * self.temperature_k;
let hbar_omega_half = HBAR * omega / 2.0;
let t1 = ((hbar_omega_half + ef) / (2.0 * kt)).tanh();
let t2 = ((hbar_omega_half - ef) / (2.0 * kt)).tanh();
let real_part = (E_CHARGE * E_CHARGE / (4.0 * HBAR)) * (t1 + t2);
let hbar_omega = HBAR * omega;
let two_ef = 2.0 * ef;
let im_part = if (hbar_omega - two_ef).abs() < 1e-30 {
0.0
} else {
(E_CHARGE * E_CHARGE / (4.0 * std::f64::consts::PI * HBAR))
* ((hbar_omega - two_ef).abs() / (hbar_omega + two_ef).abs()).ln()
};
Complex64::new(real_part, im_part)
}
pub fn surface_conductivity(&self, omega: f64) -> Complex64 {
self.intraband(omega) + self.interband(omega)
}
pub fn sheet_permittivity(&self, omega: f64, thickness_m: f64) -> Complex64 {
let sigma = self.surface_conductivity(omega);
let i_omega_eps0_d = Complex64::new(0.0, omega * EPS0 * thickness_m);
Complex64::new(1.0, 0.0) + sigma / i_omega_eps0_d
}
pub fn plasmon_wavevector(&self, omega: f64, eps_above: f64, eps_below: f64) -> Complex64 {
let sigma = self.surface_conductivity(omega);
let numerator = Complex64::new(0.0, omega * EPS0 * (eps_above + eps_below));
numerator / (2.0 * sigma)
}
pub fn plasmon_group_velocity(&self, omega: f64) -> f64 {
let deps = 1e-3 * omega.abs().max(1.0);
let k1 = self.plasmon_wavevector(omega - deps, 1.0, 1.0);
let k2 = self.plasmon_wavevector(omega + deps, 1.0, 1.0);
let dk = (k2 - k1).re;
if dk.abs() < 1e-30 {
0.0
} else {
2.0 * deps / dk
}
}
pub fn universal_absorption() -> f64 {
std::f64::consts::PI * FINE_STRUCTURE
}
pub fn dc_conductivity(&self) -> f64 {
let ef = self.fermi_energy_j();
let tau = self.relaxation_time_ps * 1e-12;
E_CHARGE * E_CHARGE * ef * tau / (std::f64::consts::PI * HBAR * HBAR)
}
}
#[derive(Debug, Clone)]
pub struct HexagonalBN {
pub n_layers: u32,
}
const CM1_TO_RADS: f64 = 2.0 * std::f64::consts::PI * SPEED_OF_LIGHT * 100.0;
const HBN_TO_UPPER: f64 = 1370.0 * CM1_TO_RADS; const HBN_LO_UPPER: f64 = 1610.0 * CM1_TO_RADS; const HBN_GAMMA_UPPER: f64 = 5.0 * CM1_TO_RADS;
const HBN_TO_LOWER: f64 = 760.0 * CM1_TO_RADS;
const HBN_LO_LOWER: f64 = 825.0 * CM1_TO_RADS;
const HBN_GAMMA_LOWER: f64 = 4.0 * CM1_TO_RADS;
const HBN_EPS_INF_INPLANE: f64 = 4.87;
const HBN_EPS_INF_OUTOFPLANE: f64 = 2.95;
impl HexagonalBN {
pub fn new(n_layers: u32) -> Self {
Self { n_layers }
}
pub fn permittivity_in_plane(&self, omega: f64) -> Complex64 {
let to = HBN_TO_UPPER;
let lo = HBN_LO_UPPER;
let gamma = HBN_GAMMA_UPPER;
let eps_inf = HBN_EPS_INF_INPLANE;
let screening = if self.n_layers == 1 { 0.95 } else { 1.0 };
let omega2 = omega * omega;
let denom = Complex64::new(to * to - omega2, -gamma * omega);
let osc = Complex64::new(lo * lo - to * to, 0.0) / denom;
Complex64::new(eps_inf * screening, 0.0) * (Complex64::new(1.0, 0.0) + osc)
}
pub fn permittivity_out_of_plane(&self, omega: f64) -> Complex64 {
let to = HBN_TO_LOWER;
let lo = HBN_LO_LOWER;
let gamma = HBN_GAMMA_LOWER;
let eps_inf = HBN_EPS_INF_OUTOFPLANE;
let omega2 = omega * omega;
let denom = Complex64::new(to * to - omega2, -gamma * omega);
let osc = Complex64::new(lo * lo - to * to, 0.0) / denom;
Complex64::new(eps_inf, 0.0) * (Complex64::new(1.0, 0.0) + osc)
}
pub fn hyperbolic_frequency_range(&self) -> Vec<(f64, f64)> {
vec![
(HBN_TO_LOWER, HBN_LO_LOWER), (HBN_TO_UPPER, HBN_LO_UPPER), ]
}
pub fn is_hyperbolic(&self, omega: f64) -> bool {
self.hyperbolic_frequency_range()
.iter()
.any(|&(lo, hi)| omega >= lo && omega <= hi)
}
pub fn hpp_wavevector(&self, omega: f64, kz: f64) -> Complex64 {
let eps_par = self.permittivity_in_plane(omega);
let eps_perp = self.permittivity_out_of_plane(omega);
let k0sq = (omega / SPEED_OF_LIGHT).powi(2);
let kz_sq = Complex64::new(kz * kz, 0.0);
let arg = eps_perp * (Complex64::new(k0sq, 0.0) - kz_sq / eps_par);
arg.sqrt()
}
}
#[derive(Debug, Clone)]
pub struct MoS2 {
pub n_layers: u32,
pub strain: f64,
}
impl MoS2 {
pub fn new(n_layers: u32, strain: f64) -> Self {
Self { n_layers, strain }
}
pub fn bandgap_ev(&self) -> f64 {
let base = match self.n_layers {
1 => 1.80,
2 => 1.65,
_ => 1.20,
};
base - 0.050 * self.strain * 100.0
}
pub fn a_exciton_energy_ev(&self) -> f64 {
let base = match self.n_layers {
1 => 1.88,
2 => 1.72,
_ => 1.85, };
base - 0.050 * self.strain * 100.0
}
pub fn b_exciton_energy_ev(&self) -> f64 {
self.a_exciton_energy_ev() + 0.150 }
pub fn permittivity(&self, omega: f64) -> Complex64 {
let eps_inf = Complex64::new(15.0, 0.0);
let ea = self.a_exciton_energy_ev() * E_CHARGE / HBAR;
let eb = self.b_exciton_energy_ev() * E_CHARGE / HBAR;
let gamma_a = 0.025 * E_CHARGE / HBAR;
let gamma_b = 0.040 * E_CHARGE / HBAR;
let fa = 2.5;
let fb = 1.5;
let lorentz = |omega_0: f64, gamma: f64, f: f64| -> Complex64 {
let denom = Complex64::new(omega_0 * omega_0 - omega * omega, -gamma * omega);
Complex64::new(f * omega_0 * omega_0, 0.0) / denom
};
eps_inf + lorentz(ea, gamma_a, fa) + lorentz(eb, gamma_b, fb)
}
pub fn is_direct_bandgap(&self) -> bool {
self.n_layers == 1
}
pub fn valley_polarization(&self, helicity: f64) -> f64 {
if !self.is_direct_bandgap() {
return 0.0;
}
let intrinsic_efficiency = 0.30;
helicity.abs().min(1.0) * intrinsic_efficiency
}
}
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BPDirection {
Armchair,
Zigzag,
}
#[derive(Debug, Clone)]
pub struct BlackPhosphorus {
pub n_layers: u32,
pub direction: BPDirection,
}
impl BlackPhosphorus {
pub fn new(n_layers: u32, direction: BPDirection) -> Self {
Self {
n_layers,
direction,
}
}
pub fn bandgap_ev(&self) -> f64 {
match self.n_layers {
1 => 2.00,
2 => 1.30,
3 => 0.90,
4 => 0.60,
5 => 0.45,
_ => 0.30,
}
}
pub fn permittivity(&self, omega: f64) -> Complex64 {
let eg_j = self.bandgap_ev() * E_CHARGE;
let omega_0 = eg_j / HBAR;
let (f, gamma_ev) = match self.direction {
BPDirection::Armchair => (8.0, 0.080), BPDirection::Zigzag => (3.0, 0.120), };
let gamma = gamma_ev * E_CHARGE / HBAR;
let eps_inf = Complex64::new(8.3, 0.0);
let denom = Complex64::new(omega_0 * omega_0 - omega * omega, -gamma * omega);
eps_inf + Complex64::new(f * omega_0 * omega_0, 0.0) / denom
}
pub fn effective_mass_ratio(&self) -> f64 {
match self.direction {
BPDirection::Armchair => 0.15,
BPDirection::Zigzag => 0.70,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::f64::consts::PI;
const OMEGA_TEST: f64 = 2.0 * PI * 10.0e12;
#[test]
fn test_graphene_dc_conductivity() {
let g1 = GrapheneSheet::new(0.2, 1.0, 300.0);
let g2 = GrapheneSheet::new(0.4, 1.0, 300.0);
let g3 = GrapheneSheet::new(0.2, 2.0, 300.0);
let dc1 = g1.dc_conductivity();
let dc2 = g2.dc_conductivity();
let dc3 = g3.dc_conductivity();
assert!(dc1 > 0.0, "DC conductivity must be positive");
let ratio_ef = dc2 / dc1;
assert!(
(ratio_ef - 2.0).abs() < 1e-10,
"σ_DC should scale linearly with E_F, got ratio {ratio_ef}"
);
let ratio_tau = dc3 / dc1;
assert!(
(ratio_tau - 2.0).abs() < 1e-10,
"σ_DC should scale linearly with τ, got ratio {ratio_tau}"
);
}
#[test]
fn test_graphene_universal_absorption() {
let abs = GrapheneSheet::universal_absorption();
assert!(
(abs - 0.02293).abs() < 5e-4,
"Universal absorption should be ≈ 2.3%, got {abs:.5}"
);
}
#[test]
fn test_graphene_intraband_drude() {
let g = GrapheneSheet::new(0.3, 1.0, 300.0);
let low_omega = 2.0 * PI * 1.0e12; let sigma = g.intraband(low_omega);
assert!(
sigma.im > 0.0,
"Im(σ_intra) should be positive for positive E_F at low ω, got {:.4e}",
sigma.im
);
}
#[test]
fn test_graphene_plasmon_wavevector() {
let g = GrapheneSheet::new(0.3, 10.0, 300.0); let omega = 2.0 * PI * 30.0e12; let k_sp = g.plasmon_wavevector(omega, 1.0, 1.0);
let k0 = omega / SPEED_OF_LIGHT;
let confinement = k_sp.norm() / k0;
assert!(
confinement > 10.0,
"Graphene plasmon should be highly confined at 30 THz with τ=10ps: \
|k_sp|/k₀ = {confinement:.2}, expected > 10"
);
}
#[test]
fn test_hbn_hyperbolic_range() {
let hbn = HexagonalBN::new(10);
let ranges = hbn.hyperbolic_frequency_range();
assert!(
!ranges.is_empty(),
"hBN should have at least one hyperbolic frequency range"
);
for (lo, hi) in &ranges {
assert!(
hi > lo,
"Each range must have hi > lo, got ({lo:.3e}, {hi:.3e})"
);
}
}
#[test]
fn test_hbn_is_hyperbolic() {
let hbn = HexagonalBN::new(10);
let omega_mid_upper = 1490.0 * CM1_TO_RADS;
assert!(
hbn.is_hyperbolic(omega_mid_upper),
"hBN should be hyperbolic at ~1490 cm⁻¹ (upper band)"
);
let omega_mid_lower = 793.0 * CM1_TO_RADS;
assert!(
hbn.is_hyperbolic(omega_mid_lower),
"hBN should be hyperbolic at ~793 cm⁻¹ (lower band)"
);
let omega_outside = 500.0 * CM1_TO_RADS;
assert!(
!hbn.is_hyperbolic(omega_outside),
"hBN should NOT be hyperbolic at 500 cm⁻¹"
);
}
#[test]
fn test_mos2_monolayer_direct_gap() {
let mos2 = MoS2::new(1, 0.0);
assert!(
mos2.is_direct_bandgap(),
"MoS₂ monolayer must have a direct bandgap"
);
let eg = mos2.bandgap_ev();
assert!(
(eg - 1.80).abs() < 0.05,
"Monolayer MoS₂ gap ≈ 1.80 eV, got {eg:.3}"
);
}
#[test]
fn test_mos2_bulk_indirect() {
let mos2 = MoS2::new(3, 0.0);
assert!(
!mos2.is_direct_bandgap(),
"MoS₂ with 3+ layers must be indirect bandgap"
);
}
#[test]
fn test_mos2_bandgap_decreases_with_layers() {
let eg1 = MoS2::new(1, 0.0).bandgap_ev();
let eg2 = MoS2::new(2, 0.0).bandgap_ev();
let eg3 = MoS2::new(3, 0.0).bandgap_ev();
assert!(
eg1 > eg2 && eg2 > eg3,
"Bandgap must decrease with layer count: {eg1:.3} > {eg2:.3} > {eg3:.3}"
);
}
#[test]
fn test_graphene_sheet_permittivity() {
let g = GrapheneSheet::new(0.3, 1.0, 300.0);
let d_graphene = 0.335e-9; let eps = g.sheet_permittivity(OMEGA_TEST, d_graphene);
let diff = (eps - Complex64::new(1.0, 0.0)).norm();
assert!(
diff > 0.01,
"Sheet permittivity should deviate from vacuum, diff = {diff:.4}"
);
}
#[test]
fn test_bp_anisotropy() {
let bp_ac = BlackPhosphorus::new(1, BPDirection::Armchair);
let bp_zz = BlackPhosphorus::new(1, BPDirection::Zigzag);
assert!(
bp_ac.effective_mass_ratio() < bp_zz.effective_mass_ratio(),
"Armchair m* should be lighter than zigzag m*"
);
}
#[test]
fn test_bp_bandgap_layer_dependence() {
let eg1 = BlackPhosphorus::new(1, BPDirection::Armchair).bandgap_ev();
let eg2 = BlackPhosphorus::new(2, BPDirection::Armchair).bandgap_ev();
let eg_bulk = BlackPhosphorus::new(10, BPDirection::Armchair).bandgap_ev();
assert!(
eg1 > eg2 && eg2 > eg_bulk,
"BP bandgap must decrease with layers: {eg1:.2} > {eg2:.2} > {eg_bulk:.2}"
);
}
}