use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InverterParams {
pub p_ac_rated: f64,
pub p_dc_rated: f64,
pub v_dc_ref: f64,
pub p_self: f64,
pub c0: f64,
pub c1: f64,
pub c2: f64,
pub c3: f64,
}
impl InverterParams {
pub fn residential_5kw() -> Self {
Self {
p_ac_rated: 5000.0,
p_dc_rated: 5200.0,
v_dc_ref: 400.0,
p_self: 15.0,
c0: -2.5e-6,
c1: -1.5e-5,
c2: 1.2e-4,
c3: -8.0e-5,
}
}
pub fn utility_500kw() -> Self {
Self {
p_ac_rated: 500_000.0,
p_dc_rated: 510_000.0,
v_dc_ref: 600.0,
p_self: 200.0,
c0: -1.5e-7,
c1: -6.0e-6,
c2: 8.0e-5,
c3: -3.0e-5,
}
}
pub fn ac_power(&self, p_dc: f64, v_dc: f64) -> f64 {
if p_dc <= self.p_self {
return 0.0;
}
let dv = (v_dc - self.v_dc_ref) / self.v_dc_ref;
let a = self.p_dc_rated * (1.0 + self.c1 * dv);
let b = self.p_self * (1.0 + self.c2 * dv);
let c = self.c0 * (1.0 + self.c3 * dv);
let denom = a - b;
if denom.abs() < 1e-6 {
return 0.0;
}
let p_ac = (self.p_ac_rated / denom) * (p_dc - b) + c * (p_dc - b).powi(2);
p_ac.clamp(0.0, self.p_ac_rated)
}
pub fn efficiency(&self, p_dc: f64, v_dc: f64) -> f64 {
if p_dc < 1e-6 {
return 0.0;
}
self.ac_power(p_dc, v_dc) / p_dc
}
pub fn european_efficiency(&self) -> f64 {
let weights = [0.03, 0.06, 0.13, 0.10, 0.48, 0.20];
let fractions = [0.05, 0.10, 0.20, 0.30, 0.50, 1.00];
let v = self.v_dc_ref;
weights
.iter()
.zip(fractions.iter())
.map(|(&w, &f)| w * self.efficiency(f * self.p_dc_rated, v))
.sum()
}
pub fn cec_efficiency(&self) -> f64 {
let weights = [0.04, 0.05, 0.12, 0.21, 0.53, 0.05];
let fractions = [0.10, 0.20, 0.30, 0.50, 0.75, 1.00];
let v = self.v_dc_ref;
weights
.iter()
.zip(fractions.iter())
.map(|(&w, &f)| w * self.efficiency(f * self.p_dc_rated, v))
.sum()
}
}
pub fn simulate_inverter(params: &InverterParams, p_dc_series: &[f64], v_dc: f64) -> Vec<f64> {
p_dc_series
.iter()
.map(|&p| params.ac_power(p, v_dc))
.collect()
}
pub fn daily_yield_wh(params: &InverterParams, p_dc_series: &[f64], v_dc: f64, dt_h: f64) -> f64 {
p_dc_series
.iter()
.map(|&p| params.ac_power(p, v_dc) * dt_h)
.sum()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_peak_efficiency_near_rated() {
let inv = InverterParams::residential_5kw();
let eta = inv.efficiency(inv.p_dc_rated, inv.v_dc_ref);
assert!(
eta > 0.90 && eta <= 1.0,
"Peak efficiency should be > 90%: η={:.4}",
eta
);
}
#[test]
fn test_zero_input_zero_output() {
let inv = InverterParams::residential_5kw();
assert_eq!(inv.ac_power(0.0, inv.v_dc_ref), 0.0);
}
#[test]
fn test_below_self_consumption_no_output() {
let inv = InverterParams::residential_5kw();
assert_eq!(inv.ac_power(inv.p_self * 0.5, inv.v_dc_ref), 0.0);
}
#[test]
fn test_ac_output_does_not_exceed_rated() {
let inv = InverterParams::residential_5kw();
let p_ac = inv.ac_power(inv.p_dc_rated * 1.5, inv.v_dc_ref);
assert!(
p_ac <= inv.p_ac_rated + 1.0,
"AC output exceeds rated: {:.1} W",
p_ac
);
}
#[test]
fn test_european_efficiency_reasonable() {
let inv = InverterParams::residential_5kw();
let eta_eu = inv.european_efficiency();
assert!(
eta_eu > 0.80 && eta_eu < 1.0,
"European efficiency out of range: η_EU={:.4}",
eta_eu
);
}
#[test]
fn test_utility_inverter_efficiency_reasonable() {
let large = InverterParams::utility_500kw();
let eta = large.efficiency(large.p_dc_rated, large.v_dc_ref);
assert!(
eta > 0.85 && eta <= 1.0,
"Utility η={:.4} should be > 85%",
eta
);
}
#[test]
fn test_simulate_inverter_series() {
let inv = InverterParams::residential_5kw();
let p_dc = vec![0.0, 1000.0, 3000.0, 5000.0, 6000.0];
let p_ac = simulate_inverter(&inv, &p_dc, inv.v_dc_ref);
assert_eq!(p_ac.len(), 5);
assert_eq!(p_ac[0], 0.0);
assert!(p_ac[3] > p_ac[1]); assert!(p_ac[4] <= inv.p_ac_rated + 1.0); }
#[test]
fn test_voltage_deviation_effect() {
let inv = InverterParams::residential_5kw();
let p = inv.p_dc_rated * 0.5;
let p_ref = inv.ac_power(p, inv.v_dc_ref);
let p_high_v = inv.ac_power(p, inv.v_dc_ref * 1.1);
assert!(
(p_ref - p_high_v).abs() < p_ref * 0.05,
"Voltage effect should be small: {:.1} vs {:.1}",
p_ref,
p_high_v
);
}
}