use crate::core::scalar::ControlScalar;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MpptDirection {
Increase,
Decrease,
}
#[derive(Debug, Clone, Copy)]
pub struct PerturbeAndObserve<S: ControlScalar> {
pub step: S,
pub v_min: S,
pub v_max: S,
v_ref: S,
p_prev: S,
v_prev: S,
eps: S,
direction: MpptDirection,
}
impl<S: ControlScalar> PerturbeAndObserve<S> {
pub fn new(v_init: S, step: S, v_min: S, v_max: S) -> Self {
Self {
step,
v_min,
v_max,
v_ref: v_init,
p_prev: S::ZERO,
v_prev: v_init,
eps: S::from_f64(1e-6),
direction: MpptDirection::Increase,
}
}
pub fn update(&mut self, v: S, i: S) -> S {
let p = v * i;
let dp = p - self.p_prev;
let dv = v - self.v_prev;
let perturbation = if dp.abs() <= self.eps {
match self.direction {
MpptDirection::Increase => self.step,
MpptDirection::Decrease => -self.step,
}
} else {
let same_sign = (dp > S::ZERO) == (dv > S::ZERO);
if same_sign {
self.direction = MpptDirection::Increase;
self.step
} else {
self.direction = MpptDirection::Decrease;
-self.step
}
};
self.p_prev = p;
self.v_prev = v;
let v_new = (self.v_ref + perturbation).clamp_val(self.v_min, self.v_max);
self.v_ref = v_new;
perturbation
}
pub fn v_ref(&self) -> S {
self.v_ref
}
pub fn power(&self) -> S {
self.p_prev
}
pub fn direction(&self) -> MpptDirection {
self.direction
}
pub fn reset(&mut self, v_init: S) {
self.v_ref = v_init;
self.v_prev = v_init;
self.p_prev = S::ZERO;
self.direction = MpptDirection::Increase;
}
}
#[derive(Debug, Clone, Copy)]
pub struct IncrementalConductance<S: ControlScalar> {
pub step: S,
pub v_min: S,
pub v_max: S,
v_ref: S,
v_prev: S,
i_prev: S,
eps: S,
}
impl<S: ControlScalar> IncrementalConductance<S> {
pub fn new(v_init: S, step: S, v_min: S, v_max: S) -> Self {
Self {
step,
v_min,
v_max,
v_ref: v_init,
v_prev: v_init,
i_prev: S::ZERO,
eps: S::from_f64(1e-6),
}
}
pub fn update(&mut self, v: S, i: S) -> S {
let dv = v - self.v_prev;
let di = i - self.i_prev;
let delta = if dv.abs() < self.eps {
if di.abs() < self.eps {
S::ZERO
} else if di > S::ZERO {
self.step
} else {
-self.step
}
} else {
let conductance_sum = if v.abs() > self.eps {
i / v + di / dv
} else {
di / dv
};
if conductance_sum.abs() < self.eps {
S::ZERO
} else if conductance_sum > S::ZERO {
self.step
} else {
-self.step
}
};
self.v_prev = v;
self.i_prev = i;
self.v_ref = (self.v_ref + delta).clamp_val(self.v_min, self.v_max);
delta
}
pub fn v_ref(&self) -> S {
self.v_ref
}
pub fn reset(&mut self, v_init: S) {
self.v_ref = v_init;
self.v_prev = v_init;
self.i_prev = S::ZERO;
}
}
#[derive(Debug, Clone, Copy)]
pub struct FractionalOcv<S: ControlScalar> {
pub k_oc: S,
v_oc: S,
v_mpp: S,
pub v_min: S,
pub v_max: S,
}
impl<S: ControlScalar> FractionalOcv<S> {
pub fn new(k_oc: S, v_oc: S, v_min: S, v_max: S) -> Self {
let v_mpp = (k_oc * v_oc).clamp_val(v_min, v_max);
Self {
k_oc,
v_oc,
v_mpp,
v_min,
v_max,
}
}
pub fn set_voc(&mut self, v_oc: S) -> S {
self.v_oc = v_oc;
self.v_mpp = (self.k_oc * v_oc).clamp_val(self.v_min, self.v_max);
self.v_mpp
}
pub fn v_mpp(&self) -> S {
self.v_mpp
}
pub fn v_oc(&self) -> S {
self.v_oc
}
}
#[derive(Debug, Clone, Copy)]
pub struct PvCellModel<S: ControlScalar> {
pub iph: S,
pub i0: S,
pub n: S,
pub vt: S,
pub rs: S,
pub rsh: S,
max_iter: u32,
tol: S,
}
impl<S: ControlScalar> PvCellModel<S> {
pub fn new(iph: S, i0: S, n: S, vt: S, rs: S, rsh: S) -> Self {
Self {
iph,
i0,
n,
vt,
rs,
rsh,
max_iter: 50,
tol: S::from_f64(1e-9),
}
}
pub fn current_at(&self, v: S) -> S {
let n_vt = self.n * self.vt;
let mut i = self.iph - self.i0 * ((v / n_vt).exp() - S::ONE) - v / self.rsh;
i = i.clamp_val(S::ZERO, self.iph);
for _ in 0..self.max_iter {
let vj = v + i * self.rs; let exp_term = (vj / n_vt).exp();
let f = i - self.iph + self.i0 * (exp_term - S::ONE) + vj / self.rsh;
let df = S::ONE + (self.i0 * self.rs / n_vt) * exp_term + self.rs / self.rsh;
if df.abs() < S::from_f64(1e-30) {
break;
}
let delta = f / df;
i -= delta;
i = i.clamp_val(S::ZERO, self.iph);
if delta.abs() < self.tol {
break;
}
}
i
}
pub fn power_at(&self, v: S) -> S {
v * self.current_at(v)
}
pub fn find_mpp(&self, v_lo: S, v_hi: S) -> (S, S) {
let mut lo = v_lo;
let mut hi = v_hi;
let third = S::from_f64(1.0 / 3.0);
for _ in 0..100 {
if hi - lo < S::from_f64(1e-6) {
break;
}
let m1 = lo + (hi - lo) * third;
let m2 = hi - (hi - lo) * third;
if self.power_at(m1) < self.power_at(m2) {
lo = m1;
} else {
hi = m2;
}
}
let v_opt = (lo + hi) * S::HALF;
(v_opt, self.power_at(v_opt))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn pv_cell() -> PvCellModel<f64> {
PvCellModel::new(
9.0, 1e-9, 1.5, 0.02585, 0.02, 300.0, )
}
#[test]
fn pv_cell_short_circuit_current() {
let cell = pv_cell();
let i_sc = cell.current_at(0.0);
assert!((i_sc - 9.0).abs() < 0.1, "Isc={i_sc:.4} A (expected ~9 A)");
}
#[test]
fn pv_cell_open_circuit_voltage() {
let cell = pv_cell();
let v_oc_approx = cell.n * cell.vt * (cell.iph / cell.i0).ln();
let i_at_voc = cell.current_at(v_oc_approx);
assert!(
i_at_voc.abs() < 0.5,
"I at V_oc = {i_at_voc:.4} A (should be near 0)"
);
}
#[test]
fn pv_cell_mpp_is_maximum() {
let cell = pv_cell();
let (v_mpp, p_mpp) = cell.find_mpp(0.1, 0.75);
let p_lo = cell.power_at(0.3);
let p_hi = cell.power_at(0.72);
assert!(
p_mpp >= p_lo && p_mpp >= p_hi,
"MPP power {p_mpp:.4} should exceed p_lo={p_lo:.4} and p_hi={p_hi:.4}"
);
assert!(
v_mpp > 0.3 && v_mpp < 0.75,
"V_mpp={v_mpp:.4} should be within (0.3, 0.75)"
);
}
#[test]
fn po_converges_near_mpp() {
let cell = pv_cell();
let (v_mpp_true, _) = cell.find_mpp(0.1, 0.75);
let mut po = PerturbeAndObserve::new(0.3_f64, 0.002, 0.1, 0.75);
for _ in 0..500 {
let v = po.v_ref();
let i = cell.current_at(v);
po.update(v, i);
}
let v_final = po.v_ref();
assert!(
(v_final - v_mpp_true).abs() < 0.015,
"P&O: v_ref={v_final:.4} V, v_mpp={v_mpp_true:.4} V"
);
}
#[test]
fn po_direction_tracking() {
let mut po = PerturbeAndObserve::new(0.3_f64, 0.01, 0.1, 0.75);
let delta = po.update(0.3, 8.5); let _d2 = po.update(0.31, 8.6); assert!(delta != 0.0, "delta must be non-zero: {delta}");
}
#[test]
fn inc_at_mpp_returns_zero_perturbation() {
let cell = pv_cell();
let (v_mpp, _) = cell.find_mpp(0.1, 0.75);
let dv = 1e-6_f64;
let i_at_mpp = cell.current_at(v_mpp);
let i_plus = cell.current_at(v_mpp + dv);
let mut inc = IncrementalConductance::new(v_mpp - dv, 0.001, 0.1, 0.75);
let i_prev = cell.current_at(v_mpp - dv);
inc.update(v_mpp - dv, i_prev);
let delta = inc.update(v_mpp, i_at_mpp);
let _d3 = inc.update(v_mpp + dv, i_plus);
assert!(delta.is_finite(), "InC delta must be finite: {delta}");
}
#[test]
fn inc_converges_near_mpp() {
let cell = pv_cell();
let (v_mpp_true, _) = cell.find_mpp(0.1, 0.75);
let mut inc = IncrementalConductance::new(0.3_f64, 0.002, 0.1, 0.75);
for _ in 0..500 {
let v = inc.v_ref();
let i = cell.current_at(v);
inc.update(v, i);
}
let v_final = inc.v_ref();
assert!(
(v_final - v_mpp_true).abs() < 0.015,
"InC: v_ref={v_final:.4} V, v_mpp={v_mpp_true:.4} V"
);
}
#[test]
fn fractional_ocv_estimate() {
let cell = pv_cell();
let (v_mpp_true, _) = cell.find_mpp(0.1, 0.75);
let v_oc = 0.78_f64;
let k_oc = 0.76_f64;
let mut focv = FractionalOcv::new(k_oc, v_oc, 0.1, 0.75);
let v_mpp_est = focv.v_mpp();
assert!(
(v_mpp_est - v_mpp_true).abs() < 0.15,
"FractionalOcv estimate={v_mpp_est:.4}, true={v_mpp_true:.4}"
);
let v_new = focv.set_voc(0.80);
assert!((v_new - k_oc * 0.80).abs() < 1e-10, "set_voc mismatch");
}
#[test]
fn po_reset_works() {
let mut po = PerturbeAndObserve::new(0.3_f64, 0.01, 0.1, 0.75);
po.update(0.3, 8.0);
po.update(0.31, 7.9);
po.reset(0.3);
assert!((po.v_ref() - 0.3).abs() < 1e-10);
assert_eq!(po.power(), 0.0);
}
#[test]
fn inc_reset_works() {
let mut inc = IncrementalConductance::new(0.3_f64, 0.01, 0.1, 0.75);
inc.update(0.3, 8.0);
inc.reset(0.3);
assert!((inc.v_ref() - 0.3).abs() < 1e-10);
}
}