use std::f64::consts::PI;
use crate::error::OxiPhotonError;
use crate::solar::absorption::AbsorptionMaterial;
use crate::solar::light_trapping::lambertian_jsc_si;
use crate::solar::spectrum::SolarSpectrum;
const CHARGE_C: f64 = 1.602_176_634e-19; const PLANCK_JS: f64 = 6.626_070_15e-34; const SPEED_OF_LIGHT_MS: f64 = 2.997_924_58e8;
const N_SI: f64 = 3.5;
#[derive(Debug, Clone)]
pub struct TexturedAbsorptionResult {
pub jsc_ma_cm2: f64,
pub jsc_planar_ma_cm2: f64,
pub jsc_lambertian_ma_cm2: f64,
pub enhancement_factor: f64,
pub lambertian_fraction: f64,
pub wavelengths_nm: Vec<f64>,
pub absorption_spectrum: Vec<f64>,
}
pub fn evaluate_textured_absorption(
period_m: f64,
depth_m: f64,
duty_cycle: f64,
absorber_thickness_m: f64,
am15g: &SolarSpectrum,
n_orders: usize,
) -> Result<TexturedAbsorptionResult, OxiPhotonError> {
if period_m <= 0.0 {
return Err(OxiPhotonError::InvalidLayer(format!(
"period_m must be positive, got {period_m:.3e}"
)));
}
if depth_m < 0.0 {
return Err(OxiPhotonError::InvalidLayer(format!(
"depth_m must be non-negative, got {depth_m:.3e}"
)));
}
if !(0.0 < duty_cycle && duty_cycle < 1.0) {
return Err(OxiPhotonError::InvalidLayer(format!(
"duty_cycle must be in (0,1), got {duty_cycle}"
)));
}
if absorber_thickness_m <= 0.0 {
return Err(OxiPhotonError::InvalidLayer(format!(
"absorber_thickness_m must be positive, got {absorber_thickness_m:.3e}"
)));
}
let wavelengths_nm = wavelength_grid_nm(300.0, 1200.0, 20.0);
let nw = wavelengths_nm.len();
let si_material = AbsorptionMaterial::crystalline_silicon();
let mut abs_textured = vec![0.0_f64; nw];
let mut abs_planar = vec![0.0_f64; nw];
for (i, &wl_nm) in wavelengths_nm.iter().enumerate() {
let wl_m = wl_nm * 1.0e-9;
let delta_phi = 2.0 * PI * (N_SI - 1.0) * depth_m / wl_m;
let alpha = si_material.alpha_at_nm(wl_nm);
abs_textured[i] = absorption_sum(
period_m,
duty_cycle,
delta_phi,
alpha,
absorber_thickness_m,
wl_m,
n_orders,
);
abs_planar[i] = absorption_sum(
period_m,
duty_cycle,
delta_phi,
alpha,
absorber_thickness_m,
wl_m,
0, );
}
let jsc_textured = integrate_jsc(&wavelengths_nm, &abs_textured, am15g);
let jsc_planar = integrate_jsc(&wavelengths_nm, &abs_planar, am15g);
let thickness_nm = absorber_thickness_m * 1.0e9;
let jsc_lambertian = lambertian_jsc_si(thickness_nm);
let enhancement_factor = if jsc_planar > 1.0e-12 {
jsc_textured / jsc_planar
} else {
1.0
};
let lambertian_fraction = if jsc_lambertian > 1.0e-12 {
jsc_textured / jsc_lambertian
} else {
0.0
};
Ok(TexturedAbsorptionResult {
jsc_ma_cm2: jsc_textured,
jsc_planar_ma_cm2: jsc_planar,
jsc_lambertian_ma_cm2: jsc_lambertian,
enhancement_factor,
lambertian_fraction,
wavelengths_nm,
absorption_spectrum: abs_textured,
})
}
fn absorption_sum(
period_m: f64,
duty_cycle: f64,
delta_phi: f64,
alpha: f64,
thickness_m: f64,
wl_m: f64,
n_orders: usize,
) -> f64 {
let f = duty_cycle;
let sin_half = (delta_phi * 0.5).sin();
let sin_half_sq = sin_half * sin_half;
let mut total = 0.0_f64;
let orders_range = -(n_orders as i64)..=(n_orders as i64);
for m in orders_range {
let cm_sq = phase_grating_order_power(m, f, sin_half_sq, delta_phi);
if cm_sq <= 0.0 {
continue;
}
let sin_theta_m = (m as f64) * wl_m / (N_SI * period_m);
if sin_theta_m.abs() >= 1.0 {
continue;
}
let cos_theta_m = (1.0 - sin_theta_m * sin_theta_m).sqrt();
let a_m = absorptance_double_pass_oblique(alpha, thickness_m, cos_theta_m);
total += cm_sq * a_m;
}
total
}
fn phase_grating_order_power(m: i64, f: f64, sin_half_sq: f64, delta_phi: f64) -> f64 {
if m == 0 {
let cos_phi = delta_phi.cos();
(1.0 - f) * (1.0 - f) + f * f + 2.0 * f * (1.0 - f) * cos_phi
} else {
let sin_pi_mf = (PI * m as f64 * f).sin();
4.0 * sin_half_sq * sin_pi_mf * sin_pi_mf / (PI * m as f64) / (PI * m as f64)
}
}
fn absorptance_double_pass_oblique(alpha: f64, thickness_m: f64, cos_theta: f64) -> f64 {
if cos_theta <= 0.0 || !cos_theta.is_finite() {
return 0.0;
}
let exponent = -2.0 * alpha * thickness_m / cos_theta;
(1.0 - exponent.exp()).clamp(0.0, 1.0)
}
fn integrate_jsc(wavelengths_nm: &[f64], absorption: &[f64], am15g: &SolarSpectrum) -> f64 {
let nw = wavelengths_nm.len();
if nw < 2 {
return 0.0;
}
let mut jsc = 0.0_f64;
for i in 0..nw - 1 {
let wl_mid_nm = 0.5 * (wavelengths_nm[i] + wavelengths_nm[i + 1]);
let dwl_nm = wavelengths_nm[i + 1] - wavelengths_nm[i];
let wl_mid_m = wl_mid_nm * 1.0e-9;
let dwl_m = dwl_nm * 1.0e-9;
let irrad = am15g.irradiance_at(wl_mid_m);
if irrad <= 0.0 {
continue;
}
let a_mid = 0.5 * (absorption[i] + absorption[i + 1]);
let phi = irrad * wl_mid_m / (PLANCK_JS * SPEED_OF_LIGHT_MS);
jsc += CHARGE_C * a_mid * phi * dwl_m;
}
jsc * 0.1
}
fn wavelength_grid_nm(start: f64, end: f64, step: f64) -> Vec<f64> {
let n = ((end - start) / step).round() as usize + 1;
(0..n).map(|i| start + i as f64 * step).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn phase_grating_energy_conservation() {
let f = 0.5_f64;
let delta_phi = 1.5_f64;
let sin_half_sq = (delta_phi * 0.5).sin().powi(2);
let n_check = 200;
let mut sum = 0.0_f64;
for m in -(n_check as i64)..=(n_check as i64) {
sum += phase_grating_order_power(m, f, sin_half_sq, delta_phi);
}
assert!(
(sum - 1.0).abs() < 0.01,
"Energy not conserved: Σ|c_m|² = {sum:.6}"
);
}
#[test]
fn shallow_grating_zeroth_order_dominates() {
let f = 0.5_f64;
let delta_phi = 0.001_f64;
let sin_half_sq = (delta_phi * 0.5).sin().powi(2);
let c0 = phase_grating_order_power(0, f, sin_half_sq, delta_phi);
assert!(
c0 > 0.999,
"Shallow grating: |c_0|² should be ≈1, got {c0:.6}"
);
}
#[test]
fn oblique_path_increases_absorption() {
let alpha = 1.0e4; let thickness = 200.0e-6; let a_normal = absorptance_double_pass_oblique(alpha, thickness, 1.0);
let a_oblique = absorptance_double_pass_oblique(alpha, thickness, 0.9);
assert!(
a_oblique >= a_normal,
"Oblique A={a_oblique:.6} should be >= normal A={a_normal:.6}"
);
}
}