use serde::{Deserialize, Serialize};
const K_B: f64 = 1.380649e-23; const Q_E: f64 = 1.602176634e-19;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SingleDiodeParams {
pub i_ph_stc: f64,
pub i_0_stc: f64,
pub r_s: f64,
pub r_sh: f64,
pub n_diode: f64,
pub n_cells: u32,
pub alpha_isc: f64,
pub beta_voc: f64,
}
impl SingleDiodeParams {
pub fn crystalline_si_250w() -> Self {
Self {
i_ph_stc: 8.76,
i_0_stc: 2.5e-10,
r_s: 0.38,
r_sh: 300.0,
n_diode: 1.1,
n_cells: 60,
alpha_isc: 0.0053, beta_voc: -0.1090, }
}
pub fn thin_film_cdte_85w() -> Self {
Self {
i_ph_stc: 1.17,
i_0_stc: 1.0e-12,
r_s: 3.0,
r_sh: 1500.0,
n_diode: 1.5,
n_cells: 116,
alpha_isc: 0.00043,
beta_voc: -0.160,
}
}
pub fn v_t(&self, temp_k: f64) -> f64 {
self.n_diode * K_B * temp_k / Q_E * self.n_cells as f64
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct IVPoint {
pub voltage: f64,
pub current: f64,
pub power: f64,
}
pub fn diode_current(params: &SingleDiodeParams, v: f64, g: f64, temp_k: f64) -> f64 {
let g_ratio = (g / 1000.0).max(0.0);
let dt = temp_k - 298.15;
let i_ph = (params.i_ph_stc + params.alpha_isc * dt) * g_ratio;
let i_0 = params.i_0_stc
* (temp_k / 298.15).powi(3)
* ((Q_E / (params.n_diode * K_B)) * (1.0 / 298.15 - 1.0 / temp_k)).exp();
let v_t = params.v_t(temp_k);
if g <= 0.0 {
return 0.0;
}
let mut i = i_ph - i_0 * ((v / v_t).exp() - 1.0);
i = i.clamp(0.0, i_ph);
for _ in 0..50 {
let exp_arg = (v + i * params.r_s) / v_t;
let exp_val = exp_arg.min(700.0).exp();
let f = i - i_ph + i_0 * (exp_val - 1.0) + (v + i * params.r_s) / params.r_sh;
let df = 1.0 + i_0 * params.r_s / v_t * exp_val + params.r_s / params.r_sh;
let di = f / df;
i -= di;
i = i.clamp(0.0, i_ph * 1.1);
if di.abs() < 1e-9 {
break;
}
}
i.max(0.0)
}
pub fn find_mpp(params: &SingleDiodeParams, g: f64, temp_k: f64) -> IVPoint {
if g <= 0.0 {
return IVPoint {
voltage: 0.0,
current: 0.0,
power: 0.0,
};
}
let v_oc_stc = params.n_cells as f64 * params.n_diode * K_B * 298.15 / Q_E
* ((params.i_ph_stc / params.i_0_stc) + 1.0).ln();
let dt = temp_k - 298.15;
let v_oc_est = (v_oc_stc + params.beta_voc * dt).max(0.1);
let phi = (5.0_f64.sqrt() - 1.0) / 2.0;
let mut a = 0.0_f64;
let mut b = v_oc_est;
let mut c = b - phi * (b - a);
let mut d = a + phi * (b - a);
for _ in 0..50 {
let pc = diode_current(params, c, g, temp_k) * c;
let pd = diode_current(params, d, g, temp_k) * d;
if pc < pd {
a = c;
c = d;
d = a + phi * (b - a);
} else {
b = d;
d = c;
c = b - phi * (b - a);
}
if (b - a).abs() < 1e-6 {
break;
}
}
let v_mpp = (a + b) / 2.0;
let i_mpp = diode_current(params, v_mpp, g, temp_k);
let p_mpp = v_mpp * i_mpp;
IVPoint {
voltage: v_mpp,
current: i_mpp,
power: p_mpp,
}
}
pub fn iv_curve(params: &SingleDiodeParams, g: f64, temp_k: f64, n_points: usize) -> Vec<IVPoint> {
if g <= 0.0 || n_points == 0 {
return vec![];
}
let v_oc_stc = params.n_cells as f64 * params.n_diode * K_B * 298.15 / Q_E
* ((params.i_ph_stc / params.i_0_stc) + 1.0).ln();
let dt = temp_k - 298.15;
let v_oc = (v_oc_stc + params.beta_voc * dt).max(0.1);
if n_points == 1 {
let i = diode_current(params, 0.0, g, temp_k);
return vec![IVPoint {
voltage: 0.0,
current: i,
power: 0.0,
}];
}
(0..n_points)
.map(|k| {
let v = v_oc * k as f64 / (n_points - 1) as f64;
let i = diode_current(params, v, g, temp_k);
IVPoint {
voltage: v,
current: i,
power: v * i,
}
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mpp_at_stc() {
let params = SingleDiodeParams::crystalline_si_250w();
let mpp = find_mpp(¶ms, 1000.0, 298.15);
assert!(
mpp.power > 230.0 && mpp.power < 285.0,
"Pmpp={:.1} W",
mpp.power
);
assert!(
mpp.voltage > 20.0 && mpp.voltage < 35.0,
"Vmpp={:.2} V",
mpp.voltage
);
}
#[test]
fn test_mpp_lower_irradiance() {
let params = SingleDiodeParams::crystalline_si_250w();
let mpp_full = find_mpp(¶ms, 1000.0, 298.15);
let mpp_half = find_mpp(¶ms, 500.0, 298.15);
assert!(mpp_half.power < mpp_full.power);
assert!(mpp_half.power > mpp_full.power * 0.4);
}
#[test]
fn test_mpp_higher_temp_lower_power() {
let params = SingleDiodeParams::crystalline_si_250w();
let mpp_cool = find_mpp(¶ms, 1000.0, 298.15);
let mpp_hot = find_mpp(¶ms, 1000.0, 328.15); assert!(
mpp_hot.power < mpp_cool.power,
"hot={:.1} cool={:.1}",
mpp_hot.power,
mpp_cool.power
);
}
#[test]
fn test_zero_irradiance_gives_zero_power() {
let params = SingleDiodeParams::crystalline_si_250w();
let mpp = find_mpp(¶ms, 0.0, 298.15);
assert_eq!(mpp.power, 0.0);
}
#[test]
fn test_iv_curve_monotone_voltage() {
let params = SingleDiodeParams::crystalline_si_250w();
let curve = iv_curve(¶ms, 800.0, 298.15, 20);
for i in 1..curve.len() {
assert!(curve[i].voltage >= curve[i - 1].voltage);
}
}
#[test]
fn test_thermal_voltage_stc() {
let params = SingleDiodeParams::crystalline_si_250w();
let vt = params.v_t(298.15);
let expected = 1.1 * 1.380649e-23 * 298.15 / 1.602176634e-19 * 60.0;
assert!(
(vt - expected).abs() < 1e-6,
"v_t={:.6} expected={:.6}",
vt,
expected
);
}
#[test]
fn test_thermal_voltage_temperature_scaling() {
let params = SingleDiodeParams::crystalline_si_250w();
let t1 = 298.15;
let t2 = 348.15; let vt1 = params.v_t(t1);
let vt2 = params.v_t(t2);
let ratio = vt2 / vt1;
let expected_ratio = t2 / t1;
assert!(
(ratio - expected_ratio).abs() < 1e-9,
"ratio={:.9} expected={:.9}",
ratio,
expected_ratio
);
}
#[test]
fn test_thin_film_mpp_at_stc() {
let params = SingleDiodeParams::thin_film_cdte_85w();
let mpp = find_mpp(¶ms, 1000.0, 298.15);
assert!(
mpp.power > 60.0 && mpp.power < 130.0,
"CdTe Pmpp={:.2} W",
mpp.power
);
assert!(mpp.voltage > 0.0, "Vmpp must be positive");
assert!(mpp.current > 0.0, "Impp must be positive");
}
#[test]
fn test_diode_current_at_zero_voltage() {
let params = SingleDiodeParams::crystalline_si_250w();
let i_sc = diode_current(¶ms, 0.0, 1000.0, 298.15);
let expected = params.i_ph_stc;
let rel_err = (i_sc - expected).abs() / expected;
assert!(
rel_err < 0.05,
"I_sc={:.4} A expected≈{:.4} A (rel_err={:.4})",
i_sc,
expected,
rel_err
);
}
#[test]
fn test_diode_current_negative_irradiance_is_zero() {
let params = SingleDiodeParams::crystalline_si_250w();
let i = diode_current(¶ms, 10.0, -50.0, 298.15);
assert_eq!(i, 0.0, "negative irradiance must yield zero current");
}
#[test]
fn test_iv_curve_empty_for_zero_points() {
let params = SingleDiodeParams::crystalline_si_250w();
let curve = iv_curve(¶ms, 800.0, 298.15, 0);
assert!(curve.is_empty(), "expected empty curve for n_points=0");
}
#[test]
fn test_iv_curve_single_point() {
let params = SingleDiodeParams::crystalline_si_250w();
let curve = iv_curve(¶ms, 1000.0, 298.15, 1);
assert_eq!(curve.len(), 1, "expected exactly 1 point");
assert!(
curve[0].voltage.abs() < 1e-9,
"single-point voltage should be 0, got {}",
curve[0].voltage
);
}
#[test]
fn test_iv_curve_current_non_increasing() {
let params = SingleDiodeParams::crystalline_si_250w();
let curve = iv_curve(¶ms, 1000.0, 298.15, 50);
for i in 1..curve.len() {
assert!(
curve[i].current <= curve[i - 1].current + 1e-9,
"current increased at index {}: {:.6} > {:.6}",
i,
curve[i].current,
curve[i - 1].current
);
}
}
#[test]
fn test_iv_curve_power_field_consistency() {
let params = SingleDiodeParams::crystalline_si_250w();
let curve = iv_curve(¶ms, 900.0, 308.15, 30);
for (idx, pt) in curve.iter().enumerate() {
let computed = pt.voltage * pt.current;
assert!(
(pt.power - computed).abs() < 1e-12,
"power mismatch at index {}: stored={:.6} computed={:.6}",
idx,
pt.power,
computed
);
}
}
#[test]
fn test_find_mpp_exceeds_sampled_curve_peak() {
let params = SingleDiodeParams::crystalline_si_250w();
let g = 700.0;
let temp_k = 308.15;
let mpp = find_mpp(¶ms, g, temp_k);
let curve = iv_curve(¶ms, g, temp_k, 100);
let sampled_peak = curve.iter().map(|pt| pt.power).fold(0.0_f64, f64::max);
assert!(
mpp.power >= sampled_peak - 0.5,
"MPP={:.3} W < sampled peak={:.3} W",
mpp.power,
sampled_peak
);
}
#[test]
fn test_find_mpp_negative_irradiance_zero_power() {
let params = SingleDiodeParams::crystalline_si_250w();
let mpp = find_mpp(¶ms, -100.0, 298.15);
assert_eq!(
mpp.power, 0.0,
"negative irradiance must yield zero MPP power"
);
assert_eq!(mpp.voltage, 0.0);
assert_eq!(mpp.current, 0.0);
}
#[test]
fn test_iv_curve_empty_for_zero_irradiance() {
let params = SingleDiodeParams::thin_film_cdte_85w();
let curve = iv_curve(¶ms, 0.0, 298.15, 20);
assert!(curve.is_empty(), "expected empty curve for zero irradiance");
}
}