use num_complex::Complex64;
use std::f64::consts::PI;
use crate::error::OxiPhotonError;
use crate::solar::SolarSpectrum;
const ELECTRON_CHARGE: f64 = 1.602_176_634e-19; const PLANCK_H: f64 = 6.626_070_15e-34; const SPEED_OF_LIGHT: f64 = 2.997_924_58e8;
#[derive(Debug, Clone)]
pub struct ArBackReflectorDesign {
pub ar_n: f64,
pub ar_thickness_nm: f64,
pub jsc_ma_cm2: f64,
pub mean_absorption: f64,
pub wavelengths_nm: Vec<f64>,
pub absorption_spectrum: Vec<f64>,
}
#[derive(Debug, Clone)]
pub struct DesignParams {
pub ar_n: f64,
pub ar_d_m: f64,
pub absorber_n: f64,
pub absorber_k: f64,
pub absorber_thickness_m: f64,
pub back_n: f64,
pub back_k: f64,
pub back_thickness_m: f64,
}
pub fn evaluate_design(
params: &DesignParams,
wavelengths_nm: &[f64],
photon_flux: &[f64],
) -> Result<ArBackReflectorDesign, OxiPhotonError> {
if wavelengths_nm.len() < 2 {
return Err(OxiPhotonError::InvalidLayer(
"evaluate_design requires at least 2 wavelength points".into(),
));
}
if wavelengths_nm.len() != photon_flux.len() {
return Err(OxiPhotonError::InvalidLayer(format!(
"wavelengths_nm ({}) and photon_flux ({}) must have the same length",
wavelengths_nm.len(),
photon_flux.len()
)));
}
let n_wl = wavelengths_nm.len();
let mut absorption_spectrum = Vec::with_capacity(n_wl);
let stack = TmmStack {
n0: 1.0,
k0: 0.0,
n1: params.ar_n,
k1: 0.0,
d1_m: params.ar_d_m,
n2: params.absorber_n,
k2: params.absorber_k,
d2_m: params.absorber_thickness_m,
n3: params.back_n,
k3: params.back_k,
d3_m: params.back_thickness_m,
n4: 1.0,
k4: 0.0,
};
for &wl_nm in wavelengths_nm {
let wl_m = wl_nm * 1e-9;
let a = tmm_stack_absorptance(&stack, wl_m);
absorption_spectrum.push(a);
}
let mut jsc_a_m2 = 0.0_f64;
for i in 0..n_wl - 1 {
let a_mid = 0.5 * (absorption_spectrum[i] + absorption_spectrum[i + 1]);
let phi_mid = 0.5 * (photon_flux[i] + photon_flux[i + 1]);
let dwl_nm = wavelengths_nm[i + 1] - wavelengths_nm[i];
let dwl_m = dwl_nm * 1e-9;
jsc_a_m2 += ELECTRON_CHARGE * a_mid * phi_mid * dwl_m;
}
let jsc_ma_cm2 = jsc_a_m2 * 0.1;
let mean_absorption = if n_wl > 0 {
absorption_spectrum.iter().sum::<f64>() / n_wl as f64
} else {
0.0
};
Ok(ArBackReflectorDesign {
ar_n: params.ar_n,
ar_thickness_nm: params.ar_d_m * 1e9,
jsc_ma_cm2,
mean_absorption,
wavelengths_nm: wavelengths_nm.to_vec(),
absorption_spectrum,
})
}
pub fn optimize_ar_and_back_reflector(
absorber_n: f64,
absorber_k: f64,
absorber_thickness_m: f64,
back_n: f64,
back_k: f64,
back_thickness_m: f64,
am15g: &SolarSpectrum,
) -> Result<ArBackReflectorDesign, OxiPhotonError> {
let wavelengths_nm: Vec<f64> = (0..=90).map(|i| 300.0 + i as f64 * 10.0).collect();
let photon_flux: Vec<f64> = wavelengths_nm
.iter()
.map(|&wl_nm| {
let wl_m = wl_nm * 1e-9;
let irrad = am15g.irradiance_at(wl_m); irrad * wl_m / (PLANCK_H * SPEED_OF_LIGHT)
})
.collect();
let n_ar_values: Vec<f64> = (0..27).map(|i| 1.20 + i as f64 * 0.05).collect();
let d_ar_nm_values: Vec<f64> = (0..31).map(|i| 50.0 + i as f64 * 5.0).collect();
let mut best_jsc = f64::NEG_INFINITY;
let mut best_n_ar = n_ar_values[0];
let mut best_d_ar_nm = d_ar_nm_values[0];
for &n_ar in &n_ar_values {
for &d_ar_nm in &d_ar_nm_values {
let d_ar_m = d_ar_nm * 1e-9;
let params = DesignParams {
ar_n: n_ar,
ar_d_m: d_ar_m,
absorber_n,
absorber_k,
absorber_thickness_m,
back_n,
back_k,
back_thickness_m,
};
let design = evaluate_design(¶ms, &wavelengths_nm, &photon_flux)?;
if design.jsc_ma_cm2 > best_jsc {
best_jsc = design.jsc_ma_cm2;
best_n_ar = n_ar;
best_d_ar_nm = d_ar_nm;
}
}
}
if !best_jsc.is_finite() {
return Err(OxiPhotonError::NumericalError(
"optimize_ar_and_back_reflector: no finite Jsc found during grid search".into(),
));
}
let best_d_ar_m = best_d_ar_nm * 1e-9;
let best_params = DesignParams {
ar_n: best_n_ar,
ar_d_m: best_d_ar_m,
absorber_n,
absorber_k,
absorber_thickness_m,
back_n,
back_k,
back_thickness_m,
};
evaluate_design(&best_params, &wavelengths_nm, &photon_flux)
}
struct TmmStack {
n0: f64,
k0: f64,
n1: f64,
k1: f64,
d1_m: f64,
n2: f64,
k2: f64,
d2_m: f64,
n3: f64,
k3: f64,
d3_m: f64,
n4: f64,
k4: f64,
}
fn tmm_stack_absorptance(stack: &TmmStack, lambda_m: f64) -> f64 {
let nc0 = Complex64::new(stack.n0, -stack.k0);
let nc1 = Complex64::new(stack.n1, -stack.k1);
let nc2 = Complex64::new(stack.n2, -stack.k2);
let nc3 = Complex64::new(stack.n3, -stack.k3);
let nc4 = Complex64::new(stack.n4, -stack.k4);
let im = Complex64::new(0.0, 1.0);
let m = {
let m1 = char_matrix(nc1, stack.d1_m, lambda_m, im);
let m2 = char_matrix(nc2, stack.d2_m, lambda_m, im);
let m3 = char_matrix(nc3, stack.d3_m, lambda_m, im);
mat2_mul(mat2_mul(m1, m2), m3)
};
let denom = nc0 * m[0][0] + nc0 * nc4 * m[0][1] + m[1][0] + nc4 * m[1][1];
if denom.norm() < 1e-30 {
return 0.0;
}
let numer_r = nc0 * m[0][0] + nc0 * nc4 * m[0][1] - m[1][0] - nc4 * m[1][1];
let r_amp = numer_r / denom;
let r_sq = r_amp.norm_sqr();
let reflectance = if r_sq.is_finite() {
r_sq.clamp(0.0, 1.0)
} else {
1.0
};
let t_amp = (Complex64::new(2.0, 0.0) * nc0) / denom;
let transmittance = if nc0.re > 1e-10 {
let t_raw = nc4.re / nc0.re * t_amp.norm_sqr();
if t_raw.is_finite() {
t_raw.clamp(0.0, 1.0 - reflectance)
} else {
0.0
}
} else {
0.0
};
(1.0 - reflectance - transmittance).max(0.0)
}
fn char_matrix(nc: Complex64, d_m: f64, lambda_m: f64, im: Complex64) -> [[Complex64; 2]; 2] {
let delta = Complex64::new(2.0 * PI, 0.0) * nc * d_m / lambda_m;
let cos_d = delta.cos();
let sin_d = delta.sin();
[[cos_d, im * sin_d / nc], [im * nc * sin_d, cos_d]]
}
fn mat2_mul(a: [[Complex64; 2]; 2], b: [[Complex64; 2]; 2]) -> [[Complex64; 2]; 2] {
[
[
a[0][0] * b[0][0] + a[0][1] * b[1][0],
a[0][0] * b[0][1] + a[0][1] * b[1][1],
],
[
a[1][0] * b[0][0] + a[1][1] * b[1][0],
a[1][0] * b[0][1] + a[1][1] * b[1][1],
],
]
}
#[cfg(test)]
mod tests {
use super::*;
struct LayerSpec {
n: f64,
k: f64,
d_m: f64,
}
#[test]
fn absorptance_bounded_for_various_stacks() {
let cases = [
(
LayerSpec {
n: 1.5,
k: 0.0,
d_m: 100e-9,
},
LayerSpec {
n: 3.5,
k: 0.01,
d_m: 200e-9,
},
LayerSpec {
n: 0.05,
k: 4.0,
d_m: 100e-9,
},
),
(
LayerSpec {
n: 2.0,
k: 0.0,
d_m: 75e-9,
},
LayerSpec {
n: 4.0,
k: 0.1,
d_m: 50e-9,
},
LayerSpec {
n: 1.5,
k: 0.0,
d_m: 50e-9,
},
),
(
LayerSpec {
n: 1.2,
k: 0.0,
d_m: 50e-9,
},
LayerSpec {
n: 2.0,
k: 0.5,
d_m: 300e-9,
},
LayerSpec {
n: 0.05,
k: 4.0,
d_m: 200e-9,
},
),
];
for (ar, abs, back) in &cases {
let stack = TmmStack {
n0: 1.0,
k0: 0.0,
n1: ar.n,
k1: ar.k,
d1_m: ar.d_m,
n2: abs.n,
k2: abs.k,
d2_m: abs.d_m,
n3: back.n,
k3: back.k,
d3_m: back.d_m,
n4: 1.0,
k4: 0.0,
};
let a = tmm_stack_absorptance(&stack, 550e-9);
assert!(
(0.0..=1.0).contains(&a),
"A={a:.4} out of range for ar.n={}, abs.k={}, back.k={}",
ar.n,
abs.k,
back.k
);
}
}
#[test]
fn energy_conservation_lossless_stack() {
let stack = TmmStack {
n0: 1.0,
k0: 0.0,
n1: 1.5,
k1: 0.0,
d1_m: 100e-9,
n2: 2.0,
k2: 0.0,
d2_m: 200e-9,
n3: 1.2,
k3: 0.0,
d3_m: 50e-9,
n4: 1.0,
k4: 0.0,
};
let a = tmm_stack_absorptance(&stack, 550e-9);
assert!(a < 1e-10, "Lossless stack should have A≈0, got {a:.2e}");
}
#[test]
fn zero_thickness_ar_matches_bare() {
let stack_zero_ar = TmmStack {
n0: 1.0,
k0: 0.0,
n1: 1.5,
k1: 0.0,
d1_m: 0.0, n2: 3.5,
k2: 0.02,
d2_m: 200e-9,
n3: 0.05,
k3: 4.0,
d3_m: 100e-9,
n4: 1.0,
k4: 0.0,
};
let stack_bare = TmmStack {
n0: 1.0,
k0: 0.0,
n1: 1.0,
k1: 0.0,
d1_m: 0.0, n2: 3.5,
k2: 0.02,
d2_m: 200e-9,
n3: 0.05,
k3: 4.0,
d3_m: 100e-9,
n4: 1.0,
k4: 0.0,
};
let a_zero_ar = tmm_stack_absorptance(&stack_zero_ar, 600e-9);
let a_bare = tmm_stack_absorptance(&stack_bare, 600e-9);
assert!(
(a_zero_ar - a_bare).abs() < 0.05,
"Zero-AR A={a_zero_ar:.4} vs bare A={a_bare:.4}"
);
}
#[test]
fn evaluate_design_returns_finite_jsc() {
let wls: Vec<f64> = (0..=90).map(|i| 300.0 + i as f64 * 10.0).collect();
let spec = SolarSpectrum::am15g();
let flux: Vec<f64> = wls
.iter()
.map(|&wl_nm| {
let wl_m = wl_nm * 1e-9;
spec.irradiance_at(wl_m) * wl_m / (PLANCK_H * SPEED_OF_LIGHT)
})
.collect();
let params = DesignParams {
ar_n: 1.9,
ar_d_m: 75e-9,
absorber_n: 3.5,
absorber_k: 0.01,
absorber_thickness_m: 200e-6,
back_n: 0.05,
back_k: 4.0,
back_thickness_m: 100e-9,
};
let design = evaluate_design(¶ms, &wls, &flux).expect("evaluate_design failed");
assert!(
design.jsc_ma_cm2 > 0.0 && design.jsc_ma_cm2 < 58.0,
"Jsc={:.2} mA/cm²",
design.jsc_ma_cm2
);
assert_eq!(design.wavelengths_nm.len(), wls.len());
assert_eq!(design.absorption_spectrum.len(), wls.len());
}
}