const CHARGE: f64 = 1.602e-19; const PLANCK: f64 = 6.626e-34; const SPEED_OF_LIGHT: f64 = 2.998e8; const BOLTZMANN: f64 = 1.380_649e-23;
#[derive(Debug, Clone)]
pub struct AbsorptionMaterial {
pub name: &'static str,
pub bandgap_ev: f64,
pub alpha_table: Vec<(f64, f64)>, }
impl AbsorptionMaterial {
pub fn crystalline_silicon() -> Self {
Self {
name: "c-Si",
bandgap_ev: 1.12,
alpha_table: vec![
(300.0, 2.0e6),
(400.0, 1.0e5),
(500.0, 1.0e4),
(600.0, 3.0e3),
(700.0, 1.0e3),
(800.0, 3.0e2),
(900.0, 1.0e2),
(1000.0, 1.5e1),
(1050.0, 3.0),
(1100.0, 0.1),
(1200.0, 0.0),
],
}
}
pub fn gaas() -> Self {
Self {
name: "GaAs",
bandgap_ev: 1.42,
alpha_table: vec![
(300.0, 2.0e6),
(400.0, 2.0e6),
(500.0, 1.5e6),
(600.0, 1.0e6),
(700.0, 8.0e5),
(800.0, 1.0e5),
(830.0, 1.0e4),
(870.0, 1.0e2),
(900.0, 0.0),
],
}
}
pub fn perovskite_mapbi3() -> Self {
Self {
name: "MAPbI3",
bandgap_ev: 1.6,
alpha_table: vec![
(300.0, 2.0e6),
(400.0, 1.5e6),
(500.0, 5.0e5),
(600.0, 1.0e5),
(700.0, 2.0e4),
(750.0, 1.0e3),
(780.0, 0.0),
],
}
}
pub fn cigs() -> Self {
Self {
name: "CIGS",
bandgap_ev: 1.15,
alpha_table: vec![
(300.0, 2.0e5),
(400.0, 1.5e5),
(500.0, 1.0e5),
(600.0, 8.0e4),
(700.0, 6.0e4),
(800.0, 4.0e4),
(900.0, 2.0e4),
(1000.0, 5.0e3),
(1080.0, 0.0),
],
}
}
pub fn bandgap_wavelength_nm(&self) -> f64 {
PLANCK * SPEED_OF_LIGHT / (self.bandgap_ev * CHARGE) * 1e9
}
pub fn alpha_at_nm(&self, lambda_nm: f64) -> f64 {
if lambda_nm > self.bandgap_wavelength_nm() {
return 0.0;
}
let table = &self.alpha_table;
if table.is_empty() {
return 0.0;
}
if lambda_nm <= table[0].0 {
return table[0].1 * 1e2; }
if lambda_nm >= table[table.len() - 1].0 {
return 0.0;
}
for i in 0..table.len() - 1 {
let (l0, a0) = table[i];
let (l1, a1) = table[i + 1];
if lambda_nm >= l0 && lambda_nm <= l1 {
let t = (lambda_nm - l0) / (l1 - l0);
let alpha_cm = a0 + t * (a1 - a0);
return alpha_cm * 1e2; }
}
0.0
}
pub fn absorptance(&self, lambda_nm: f64, thickness_m: f64) -> f64 {
let alpha = self.alpha_at_nm(lambda_nm);
1.0 - (-alpha * thickness_m).exp()
}
pub fn absorptance_double_pass(&self, lambda_nm: f64, thickness_m: f64) -> f64 {
let alpha = self.alpha_at_nm(lambda_nm);
1.0 - (-2.0 * alpha * thickness_m).exp()
}
pub fn jsc_am15g(&self, thickness_m: f64, reflectance: f64) -> f64 {
let wavelengths_nm = [400.0, 500.0, 600.0, 700.0, 800.0, 900.0, 1000.0, 1100.0];
let flux_nm = [
1.5e18, 3.5e18, 3.8e18, 3.0e18, 2.5e18, 2.0e18, 1.5e18, 0.5e18,
];
let n = wavelengths_nm.len();
let mut jsc = 0.0f64;
for i in 0..n - 1 {
let lam = (wavelengths_nm[i] + wavelengths_nm[i + 1]) / 2.0;
let flux = (flux_nm[i] + flux_nm[i + 1]) / 2.0;
let dl_nm = wavelengths_nm[i + 1] - wavelengths_nm[i];
let a = self.absorptance(lam, thickness_m);
jsc += CHARGE * flux * (1.0 - reflectance) * a * dl_nm;
}
jsc
}
pub fn jsc_with_reflection_loss(&self, thickness_m: f64, r: f64) -> f64 {
self.jsc_am15g(thickness_m, r)
}
pub fn absorption_depth_m(&self, lambda_nm: f64) -> f64 {
let alpha = self.alpha_at_nm(lambda_nm);
if alpha < 1e-10 {
f64::INFINITY
} else {
1.0 / alpha
}
}
pub fn generation_rate(&self, lambda_nm: f64, z_m: f64, phi0_photons_m2_s: f64) -> f64 {
let alpha = self.alpha_at_nm(lambda_nm);
alpha * phi0_photons_m2_s * (-alpha * z_m).exp()
}
}
pub fn lambert_beer_with_reflection(alpha: f64, thickness_m: f64, r: f64) -> f64 {
(1.0 - r) * (-alpha * thickness_m).exp()
}
pub fn effective_absorptance(alpha: f64, thickness_m: f64, r: f64) -> f64 {
(1.0 - r) * (1.0 - (-alpha * thickness_m).exp())
}
#[derive(Debug, Clone)]
pub struct ThinFilmAbsorber {
pub n: f64,
pub k: f64,
pub thickness: f64,
pub substrate_n: f64,
}
impl ThinFilmAbsorber {
pub fn new(n: f64, k: f64, thickness: f64, substrate_n: f64) -> Self {
Self {
n,
k,
thickness,
substrate_n,
}
}
pub fn reflectance_transmittance(&self, lambda_m: f64) -> (f64, f64) {
use num_complex::Complex64;
use std::f64::consts::PI;
let n0 = 1.0_f64; let n_film = Complex64::new(self.n, self.k);
let n_sub = self.substrate_n;
let delta: Complex64 = Complex64::new(
2.0 * PI * self.n * self.thickness / lambda_m,
2.0 * PI * self.k * self.thickness / lambda_m,
);
let cos_d = delta.cos();
let sin_d = delta.sin();
let i = Complex64::new(0.0, 1.0);
let n0c = Complex64::new(n0, 0.0);
let n3c = Complex64::new(n_sub, 0.0);
let m11 = cos_d;
let m12 = i * sin_d / n_film;
let m21 = i * n_film * sin_d;
let m22 = cos_d;
let numer_r = n0c * m11 + n0c * n3c * m12 - m21 - n3c * m22;
let denom = n0c * m11 + n0c * n3c * m12 + m21 + n3c * m22;
if denom.norm() < 1e-30 {
return (0.0, 0.0);
}
let r_amp = numer_r / denom;
let reflectance = r_amp.norm_sqr().clamp(0.0, 1.0);
let numer_t = 2.0 * n0c;
let t_amp = numer_t / denom;
let transmittance = ((n_sub / n0) * t_amp.norm_sqr()).clamp(0.0, 1.0 - reflectance);
(reflectance, transmittance)
}
pub fn absorptance(&self, lambda_m: f64) -> f64 {
let (r, t) = self.reflectance_transmittance(lambda_m);
(1.0 - r - t).max(0.0)
}
}
pub fn spatial_generation_profile(z_pts: &[f64], alpha: f64, phi0: f64) -> Vec<f64> {
z_pts
.iter()
.map(|&z| alpha * phi0 * (-alpha * z).exp())
.collect()
}
pub fn integrated_photon_current(
spectrum: &[(f64, f64)],
alpha_fn: impl Fn(f64) -> f64,
thickness: f64,
) -> f64 {
if spectrum.len() < 2 {
return 0.0;
}
let mut current = 0.0_f64;
for i in 0..spectrum.len() - 1 {
let (l0, phi0) = spectrum[i];
let (l1, phi1) = spectrum[i + 1];
let dl = l1 - l0;
let lam = (l0 + l1) / 2.0;
let phi = (phi0 + phi1) / 2.0;
let alpha = alpha_fn(lam);
let absorptance = 1.0 - (-alpha * thickness).exp();
current += CHARGE * phi * absorptance * dl;
}
current
}
pub fn light_trapping_enhancement(n: f64) -> f64 {
4.0 * n * n
}
pub fn lambertian_effective_alpha(alpha: f64, n: f64) -> f64 {
alpha * light_trapping_enhancement(n)
}
pub fn lambertian_absorptance(alpha: f64, n: f64, thickness_m: f64) -> f64 {
1.0 - (-alpha * 4.0 * n * n * thickness_m).exp()
}
#[derive(Debug, Clone)]
pub struct MultiJunction {
pub layers: Vec<(f64, f64)>,
}
impl MultiJunction {
pub fn new(layers: Vec<(f64, f64)>) -> Self {
Self { layers }
}
pub fn photon_current_each_layer(&self, photon_flux: f64) -> Vec<f64> {
self.layers
.iter()
.map(|&(eg, _thickness)| {
let above_gap_fraction = (1.0 - eg / 4.0).max(0.0);
CHARGE * photon_flux * above_gap_fraction
})
.collect()
}
pub fn limiting_current(&self, photon_flux: f64) -> f64 {
self.photon_current_each_layer(photon_flux)
.into_iter()
.fold(f64::INFINITY, f64::min)
}
}
pub fn photon_recycling_factor(q: f64, alpha: f64, thickness: f64, n: f64) -> f64 {
let four_n2 = 4.0 * n * n;
let escape_prob = 1.0 / four_n2 + (1.0 - 1.0 / four_n2) * (-alpha * four_n2 * thickness).exp();
1.0 / (1.0 - q * (1.0 - escape_prob))
}
pub fn photon_recycling_voc_boost(f_recycle: f64, temperature_k: f64) -> f64 {
let kbt_e = BOLTZMANN * temperature_k / CHARGE;
if f_recycle >= 1.0 {
return f64::INFINITY;
}
kbt_e * (1.0 / (1.0 - f_recycle)).ln()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn si_bandgap_wavelength_near_1100nm() {
let si = AbsorptionMaterial::crystalline_silicon();
let lg = si.bandgap_wavelength_nm();
assert!((lg - 1100.0).abs() < 50.0, "λ_g={lg:.0}nm");
}
#[test]
fn si_absorption_high_at_uv() {
let si = AbsorptionMaterial::crystalline_silicon();
let alpha = si.alpha_at_nm(400.0);
assert!(alpha >= 1e7, "α(400nm)={alpha:.2e} m⁻¹");
}
#[test]
fn si_absorption_zero_beyond_bandgap() {
let si = AbsorptionMaterial::crystalline_silicon();
let alpha = si.alpha_at_nm(1200.0);
assert!(alpha == 0.0);
}
#[test]
fn absorptance_increases_with_thickness() {
let si = AbsorptionMaterial::crystalline_silicon();
let a1 = si.absorptance(700.0, 10e-6);
let a2 = si.absorptance(700.0, 100e-6);
assert!(
a2 > a1,
"Thicker absorbs more: A(10µm)={a1:.3} A(100µm)={a2:.3}"
);
}
#[test]
fn double_pass_greater_than_single_pass() {
let si = AbsorptionMaterial::crystalline_silicon();
let a1 = si.absorptance(800.0, 50e-6);
let a2 = si.absorptance_double_pass(800.0, 50e-6);
assert!(a2 > a1);
}
#[test]
fn jsc_positive_for_si() {
let si = AbsorptionMaterial::crystalline_silicon();
let jsc = si.jsc_am15g(300e-6, 0.05);
assert!(jsc > 0.0, "J_sc={jsc:.2}A/m²");
}
#[test]
fn absorption_depth_smaller_at_uv() {
let si = AbsorptionMaterial::crystalline_silicon();
let d400 = si.absorption_depth_m(400.0);
let d700 = si.absorption_depth_m(700.0);
assert!(d400 < d700, "UV penetrates less than red");
}
#[test]
fn generation_rate_decreases_with_depth() {
let si = AbsorptionMaterial::crystalline_silicon();
let g0 = si.generation_rate(600.0, 0.0, 1e21);
let g1 = si.generation_rate(600.0, 1e-6, 1e21);
assert!(g0 > g1, "G should decrease with depth");
}
#[test]
fn cigs_bandgap_near_1100nm() {
let mat = AbsorptionMaterial::cigs();
let lg = mat.bandgap_wavelength_nm();
assert!(lg > 900.0 && lg < 1200.0, "λ_g(CIGS)={lg:.0}nm");
}
#[test]
fn gaas_bandgap_near_870nm() {
let mat = AbsorptionMaterial::gaas();
let lg = mat.bandgap_wavelength_nm();
assert!((lg - 873.0).abs() < 20.0, "λ_g(GaAs)={lg:.0}nm");
}
#[test]
fn lambert_beer_with_reflection_decreases() {
let t0 = lambert_beer_with_reflection(1e5, 0.0, 0.05);
let t1 = lambert_beer_with_reflection(1e5, 100e-6, 0.05);
assert!(t0 > t1);
}
#[test]
fn effective_absorptance_bounded() {
let a = effective_absorptance(1e6, 100e-6, 0.1);
assert!((0.0..=1.0).contains(&a), "A_eff={a:.4}");
}
#[test]
fn spatial_profile_decreasing() {
let z = vec![0.0, 1e-7, 2e-7, 5e-7];
let profile = spatial_generation_profile(&z, 1e6, 1e21);
for i in 0..profile.len() - 1 {
assert!(profile[i] > profile[i + 1], "Profile not monotone at i={i}");
}
}
#[test]
fn spatial_profile_at_zero_is_alpha_phi() {
let profile = spatial_generation_profile(&[0.0], 1e6, 1e21);
let expected = 1e6 * 1e21;
assert!((profile[0] - expected).abs() / expected < 1e-10);
}
#[test]
fn light_trapping_si() {
let factor = light_trapping_enhancement(3.5);
assert!((factor - 49.0).abs() < 0.01, "4n²={factor:.2}");
}
#[test]
fn lambertian_absorptance_greater_than_single_pass() {
let alpha = 1e3_f64; let n = 3.5_f64;
let t = 100e-6_f64;
let a_single = 1.0 - (-alpha * t).exp();
let a_lt = lambertian_absorptance(alpha, n, t);
assert!(a_lt > a_single, "LT absorptance should exceed single-pass");
}
#[test]
fn thin_film_reflectance_bounded() {
let film = ThinFilmAbsorber::new(2.0, 0.1, 100e-9, 1.5);
let (r, t) = film.reflectance_transmittance(550e-9);
assert!((0.0..=1.0).contains(&r), "R={r:.4}");
assert!((0.0..=1.0).contains(&t), "T={t:.4}");
}
#[test]
fn thin_film_absorptance_bounded() {
let film = ThinFilmAbsorber::new(2.0, 0.5, 300e-9, 1.5);
let a = film.absorptance(500e-9);
assert!((0.0..=1.0).contains(&a), "A={a:.4}");
}
#[test]
fn multi_junction_limiting_current() {
let mj = MultiJunction::new(vec![(1.9, 1e-6), (1.4, 1e-6), (1.0, 1e-6)]);
let jlim = mj.limiting_current(1e21);
assert!(jlim > 0.0);
}
#[test]
fn photon_recycling_factor_bounded() {
let f = photon_recycling_factor(0.99, 1e4, 300e-6, 3.5);
assert!(f > 1.0, "f_recycle={f:.4} should be > 1 in thick limit");
}
#[test]
fn photon_recycling_voc_boost_positive() {
let dv = photon_recycling_voc_boost(0.9, 300.0);
assert!(dv > 0.0 && dv < 1.0, "ΔV_oc={dv:.4} V");
}
#[test]
fn integrated_photon_current_positive() {
let spectrum: Vec<(f64, f64)> =
(0..20).map(|i| (400e-9 + i as f64 * 10e-9, 3e21)).collect();
let jph = integrated_photon_current(&spectrum, |_| 1e5, 100e-6);
assert!(jph > 0.0);
}
#[test]
fn jsc_with_reflection_less_than_no_reflection() {
let si = AbsorptionMaterial::crystalline_silicon();
let jsc_no_r = si.jsc_am15g(300e-6, 0.0);
let jsc_r = si.jsc_am15g(300e-6, 0.1);
assert!(jsc_no_r > jsc_r, "Reflection reduces J_sc");
}
}