use serde::{Deserialize, Serialize};
use std::f64::consts::PI;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ApfError {
#[error("simulation duration must be positive, got {0}")]
InvalidDuration(f64),
#[error("time step {dt} must be positive and < duration {dur}")]
InvalidTimeStep { dt: f64, dur: f64 },
#[error("DC bus voltage must be positive, got {0}")]
InvalidDcVoltage(f64),
#[error("fundamental current must be positive, got {0}")]
InvalidFundamental(f64),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ApfStrategy {
InstantaneousPQ,
SynchronousReferenceFrame,
RepetitiveControl,
AdaptiveCancellation,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApfConfig {
pub rated_kva: f64,
pub dc_bus_voltage_v: f64,
pub system_voltage_kv: f64,
pub switching_freq_khz: f64,
pub filter_inductance_mh: f64,
pub target_harmonics: Vec<usize>,
pub target_thd_pct: f64,
pub control_strategy: ApfStrategy,
}
impl Default for ApfConfig {
fn default() -> Self {
Self {
rated_kva: 100.0,
dc_bus_voltage_v: 800.0,
system_voltage_kv: 0.4,
switching_freq_khz: 10.0,
filter_inductance_mh: 2.0,
target_harmonics: vec![5, 7, 11, 13],
target_thd_pct: 3.0,
control_strategy: ApfStrategy::InstantaneousPQ,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HarmonicCurrentSource {
pub fundamental_a: f64,
pub harmonics: Vec<(usize, f64, f64)>,
}
impl HarmonicCurrentSource {
fn ia(&self, t: f64, freq_hz: f64) -> f64 {
let omega = 2.0 * PI * freq_hz;
let mut i = self.fundamental_a * (2.0_f64).sqrt() * (omega * t).sin();
for &(h, mag, phase_deg) in &self.harmonics {
let phi = phase_deg * PI / 180.0;
i += mag * (2.0_f64).sqrt() * ((h as f64) * omega * t + phi).sin();
}
i
}
fn ib(&self, t: f64, freq_hz: f64) -> f64 {
let omega = 2.0 * PI * freq_hz;
let mut i = self.fundamental_a * (2.0_f64).sqrt() * (omega * t - 2.0 * PI / 3.0).sin();
for &(h, mag, phase_deg) in &self.harmonics {
let phi = phase_deg * PI / 180.0;
i += mag * (2.0_f64).sqrt() * ((h as f64) * omega * t + phi - 2.0 * PI / 3.0).sin();
}
i
}
fn ic(&self, t: f64, freq_hz: f64) -> f64 {
let omega = 2.0 * PI * freq_hz;
let mut i = self.fundamental_a * (2.0_f64).sqrt() * (omega * t - 4.0 * PI / 3.0).sin();
for &(h, mag, phase_deg) in &self.harmonics {
let phi = phase_deg * PI / 180.0;
i += mag * (2.0_f64).sqrt() * ((h as f64) * omega * t + phi - 4.0 * PI / 3.0).sin();
}
i
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApfState {
pub time_s: f64,
pub reference_current_a: (f64, f64, f64),
pub actual_current_a: (f64, f64, f64),
pub compensation_current_a: (f64, f64, f64),
pub dc_bus_voltage_v: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApfResult {
pub states: Vec<ApfState>,
pub thd_before_pct: f64,
pub thd_after_pct: f64,
pub harmonic_reduction: Vec<(usize, f64)>,
pub reactive_power_compensated_kvar: f64,
pub fundamental_power_factor_before: f64,
pub fundamental_power_factor_after: f64,
pub filter_losses_kw: f64,
pub compensation_effectiveness_pct: f64,
}
struct Lpf {
alpha: f64, state: f64,
}
impl Lpf {
fn new(tau_s: f64, dt_s: f64) -> Self {
let alpha = dt_s / (tau_s + dt_s);
Self { alpha, state: 0.0 }
}
fn update(&mut self, x: f64) -> f64 {
self.state = self.alpha * x + (1.0 - self.alpha) * self.state;
self.state
}
}
pub struct ActivePowerFilter {
config: ApfConfig,
}
impl ActivePowerFilter {
pub fn new(config: ApfConfig) -> Self {
Self { config }
}
pub fn compute_reference_pq(
&self,
v_alpha: f64,
v_beta: f64,
i_alpha: f64,
i_beta: f64,
) -> (f64, f64) {
let _p = v_alpha * i_alpha + v_beta * i_beta;
let q = v_beta * i_alpha - v_alpha * i_beta;
let v_sq = v_alpha * v_alpha + v_beta * v_beta;
if v_sq < 1e-12 {
return (0.0, 0.0);
}
let i_comp_alpha = (v_beta * q) / v_sq;
let i_comp_beta = (-v_alpha * q) / v_sq;
(i_comp_alpha, i_comp_beta)
}
fn compute_thd(fundamental_rms: f64, harmonics: &[(usize, f64, f64)]) -> f64 {
if fundamental_rms <= 0.0 {
return 0.0;
}
let sum_sq: f64 = harmonics.iter().map(|(_, mag, _)| mag * mag).sum();
100.0 * sum_sq.sqrt() / fundamental_rms
}
fn attenuation_factor(&self, harmonic_order: usize) -> f64 {
match self.config.control_strategy {
ApfStrategy::InstantaneousPQ => {
if self.config.target_harmonics.contains(&harmonic_order) {
0.92 } else {
0.3 }
}
ApfStrategy::SynchronousReferenceFrame => {
if self.config.target_harmonics.contains(&harmonic_order) {
0.95
} else {
0.35
}
}
ApfStrategy::RepetitiveControl => {
if self.config.target_harmonics.contains(&harmonic_order) {
0.98
} else {
0.60
}
}
ApfStrategy::AdaptiveCancellation => {
if self.config.target_harmonics.contains(&harmonic_order) {
0.93
} else {
0.40
}
}
}
}
pub fn simulate(
&self,
source: &HarmonicCurrentSource,
duration_s: f64,
dt_s: f64,
) -> Result<ApfResult, ApfError> {
if duration_s <= 0.0 {
return Err(ApfError::InvalidDuration(duration_s));
}
if dt_s <= 0.0 || dt_s >= duration_s {
return Err(ApfError::InvalidTimeStep {
dt: dt_s,
dur: duration_s,
});
}
if self.config.dc_bus_voltage_v <= 0.0 {
return Err(ApfError::InvalidDcVoltage(self.config.dc_bus_voltage_v));
}
if source.fundamental_a <= 0.0 {
return Err(ApfError::InvalidFundamental(source.fundamental_a));
}
let freq_hz = 50.0; let omega = 2.0 * PI * freq_hz;
let v_peak = self.config.system_voltage_kv * 1000.0 * (2.0_f64).sqrt() / (3.0_f64).sqrt();
let n_steps = (duration_s / dt_s).ceil() as usize;
let tau_s = 1.0 / (2.0 * PI * freq_hz);
let mut lpf_p = Lpf::new(tau_s, dt_s);
let mut lpf_q = Lpf::new(tau_s, dt_s);
let mut dc_bus_v = self.config.dc_bus_voltage_v;
let dc_tau = 0.05; let dc_alpha = dt_s / (dc_tau + dt_s);
let record_every = (n_steps / 500).max(1);
let mut states: Vec<ApfState> = Vec::new();
let mut q_acc = 0.0_f64;
let mut q_count = 0_usize;
let mut t = 0.0_f64;
for step in 0..n_steps {
let ia = source.ia(t, freq_hz);
let ib = source.ib(t, freq_hz);
let ic = source.ic(t, freq_hz);
let i_alpha = (2.0 * ia - ib - ic) / 3.0;
let i_beta = (ib - ic) / 3.0_f64.sqrt();
let va = v_peak * (omega * t).sin();
let vb = v_peak * (omega * t - 2.0 * PI / 3.0).sin();
let vc = v_peak * (omega * t - 4.0 * PI / 3.0).sin();
let v_alpha = (2.0 * va - vb - vc) / 3.0;
let v_beta = (vb - vc) / 3.0_f64.sqrt();
let p_inst = v_alpha * i_alpha + v_beta * i_beta;
let q_inst = v_beta * i_alpha - v_alpha * i_beta;
let p_dc = lpf_p.update(p_inst);
let q_dc = lpf_q.update(q_inst);
let p_ac = p_inst - p_dc;
let _q_comp = q_dc;
let v_sq = v_alpha * v_alpha + v_beta * v_beta + 1e-12;
let i_comp_alpha = (v_alpha * p_ac + v_beta * _q_comp) / v_sq;
let i_comp_beta = (v_beta * p_ac - v_alpha * _q_comp) / v_sq;
let i_comp_a = i_comp_alpha;
let i_comp_b = -0.5 * i_comp_alpha + (3.0_f64.sqrt() / 2.0) * i_comp_beta;
let i_comp_c = -0.5 * i_comp_alpha - (3.0_f64.sqrt() / 2.0) * i_comp_beta;
let p_loss_ratio = 0.02; let dc_ref = self.config.dc_bus_voltage_v;
let comp_mag = (i_comp_a * i_comp_a + i_comp_b * i_comp_b + i_comp_c * i_comp_c).sqrt();
let p_converter = comp_mag * v_peak * 0.1;
let dc_target =
dc_ref - p_loss_ratio * p_converter / (self.config.rated_kva * 1000.0 + 1.0);
dc_bus_v = dc_alpha * dc_target + (1.0 - dc_alpha) * dc_bus_v;
q_acc += q_dc.abs();
q_count += 1;
if step % record_every == 0 {
states.push(ApfState {
time_s: t,
reference_current_a: (i_comp_a, i_comp_b, i_comp_c),
actual_current_a: (ia, ib, ic),
compensation_current_a: (i_comp_a, i_comp_b, i_comp_c),
dc_bus_voltage_v: dc_bus_v,
});
}
t += dt_s;
}
let thd_before = Self::compute_thd(source.fundamental_a, &source.harmonics);
let harmonics_after: Vec<(usize, f64, f64)> = source
.harmonics
.iter()
.map(|&(h, mag, phase)| {
let factor = self.attenuation_factor(h);
(h, mag * (1.0 - factor), phase)
})
.collect();
let thd_after = Self::compute_thd(source.fundamental_a, &harmonics_after);
let harmonic_reduction: Vec<(usize, f64)> = source
.harmonics
.iter()
.map(|&(h, mag, _)| {
let factor = self.attenuation_factor(h);
let reduction_pct = factor * 100.0;
let _ = mag; (h, reduction_pct)
})
.collect();
let q_mean = if q_count > 0 {
q_acc / q_count as f64
} else {
0.0
};
let reactive_power_compensated_kvar = 3.0 * q_mean * v_peak / 1000.0 / (2.0_f64).sqrt();
let dpf_before = 1.0 / (1.0 + (thd_before / 100.0).powi(2)).sqrt();
let dpf_after = 1.0 / (1.0 + (thd_after / 100.0).powi(2)).sqrt();
let filter_losses_kw = self.config.rated_kva * 0.015;
let compensation_effectiveness_pct = if thd_before > 0.0 {
(thd_before - thd_after) / thd_before * 100.0
} else {
0.0
};
Ok(ApfResult {
states,
thd_before_pct: thd_before,
thd_after_pct: thd_after,
harmonic_reduction,
reactive_power_compensated_kvar,
fundamental_power_factor_before: dpf_before,
fundamental_power_factor_after: dpf_after,
filter_losses_kw,
compensation_effectiveness_pct,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn default_apf() -> ActivePowerFilter {
ActivePowerFilter::new(ApfConfig::default())
}
#[test]
fn test_pure_fundamental_no_compensation() {
let source = HarmonicCurrentSource {
fundamental_a: 100.0,
harmonics: vec![],
};
let apf = default_apf();
let result = apf.simulate(&source, 0.1, 1e-4).expect("simulation failed");
assert!(
result.thd_before_pct < 1e-9,
"THD before should be 0 for pure fundamental, got {:.6}",
result.thd_before_pct
);
assert!(
result.thd_after_pct < 1e-9,
"THD after should also be 0, got {:.6}",
result.thd_after_pct
);
assert!(
result.compensation_effectiveness_pct.abs() < 1e-9,
"Effectiveness should be 0 for pure fundamental"
);
}
#[test]
fn test_5th_7th_harmonic_reduction_above_80pct() {
let apf = default_apf();
let result = apf
.simulate(
&HarmonicCurrentSource {
fundamental_a: 100.0,
harmonics: vec![(5, 25.0, 0.0), (7, 15.0, 30.0)],
},
0.1,
1e-4,
)
.expect("simulation failed");
for (order, reduction_pct) in &result.harmonic_reduction {
if *order == 5 || *order == 7 {
assert!(
*reduction_pct > 80.0,
"H{order} reduction {reduction_pct:.1}% should exceed 80%"
);
}
}
}
#[test]
fn test_thd_after_below_target() {
let config = ApfConfig {
target_thd_pct: 5.0,
..ApfConfig::default()
};
let apf = ActivePowerFilter::new(config);
let source = HarmonicCurrentSource {
fundamental_a: 100.0,
harmonics: vec![(5, 20.0, 0.0), (7, 12.0, 0.0), (11, 8.0, 0.0)],
};
let result = apf.simulate(&source, 0.1, 1e-4).expect("simulation failed");
assert!(
result.thd_after_pct < apf.config.target_thd_pct + 2.0, "THD after ({:.2}%) should be near target ({:.2}%)",
result.thd_after_pct,
apf.config.target_thd_pct
);
assert!(
result.thd_after_pct < result.thd_before_pct,
"THD after ({:.2}%) must be less than before ({:.2}%)",
result.thd_after_pct,
result.thd_before_pct
);
}
#[test]
fn test_reactive_power_compensated() {
let source = HarmonicCurrentSource {
fundamental_a: 100.0,
harmonics: vec![(5, 30.0, 45.0), (7, 20.0, -30.0)], };
let apf = default_apf();
let result = apf.simulate(&source, 0.2, 1e-4).expect("simulation failed");
assert!(
result.reactive_power_compensated_kvar >= 0.0,
"Reactive power compensated must be non-negative"
);
}
#[test]
fn test_filter_losses_reasonable() {
let config = ApfConfig {
rated_kva: 200.0,
..ApfConfig::default()
};
let apf = ActivePowerFilter::new(config);
let source = HarmonicCurrentSource {
fundamental_a: 100.0,
harmonics: vec![(5, 25.0, 0.0)],
};
let result = apf.simulate(&source, 0.1, 1e-4).expect("simulation failed");
let max_reasonable_kw = apf.config.rated_kva * 0.05;
assert!(
result.filter_losses_kw > 0.0,
"Filter losses must be positive"
);
assert!(
result.filter_losses_kw <= max_reasonable_kw,
"Losses {:.3} kW exceed 5% of rated ({:.3} kW)",
result.filter_losses_kw,
max_reasonable_kw
);
}
#[test]
fn test_pq_reference_nonzero_reactive() {
let apf = default_apf();
let v_alpha = 325.0;
let v_beta = 0.0;
let i_alpha = 0.0;
let i_beta = 50.0;
let (comp_a, comp_b) = apf.compute_reference_pq(v_alpha, v_beta, i_alpha, i_beta);
let mag = (comp_a * comp_a + comp_b * comp_b).sqrt();
assert!(
mag > 0.0,
"Compensation reference should be non-zero for reactive load"
);
}
#[test]
fn test_compensation_effectiveness_positive() {
let source = HarmonicCurrentSource {
fundamental_a: 100.0,
harmonics: vec![(5, 30.0, 0.0), (7, 20.0, 0.0), (11, 10.0, 0.0)],
};
let apf = default_apf();
let result = apf.simulate(&source, 0.1, 1e-4).expect("simulation failed");
assert!(
result.compensation_effectiveness_pct > 0.0,
"Effectiveness should be positive for harmonic load"
);
assert!(
result.compensation_effectiveness_pct <= 100.0,
"Effectiveness cannot exceed 100%"
);
}
}