use num_complex::Complex64;
use serde::{Deserialize, Serialize};
use crate::harmonics::analysis::HarmonicSpectrum;
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub enum FilterType {
SingleTuned,
HighPass,
CType,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PassiveFilter {
pub filter_type: FilterType,
pub harmonic_order: f64,
pub q_mvar: f64,
pub fundamental_hz: f64,
pub bus_kv: f64,
pub q_factor: f64,
capacitance_f: f64, inductance_h: f64, resistance_ohm: f64, }
impl PassiveFilter {
pub fn single_tuned(
harmonic_order: f64,
q_mvar: f64,
fundamental_hz: f64,
bus_kv: f64,
q_factor: f64,
) -> Self {
let v_ll = bus_kv * 1000.0; let omega_1 = 2.0 * std::f64::consts::PI * fundamental_hz;
let omega_n = omega_1 * harmonic_order;
let q_var = q_mvar * 1e6;
let xc = v_ll * v_ll / q_var;
let capacitance_f = 1.0 / (omega_1 * xc);
let inductance_h = 1.0 / (omega_n * omega_n * capacitance_f);
let resistance_ohm = omega_n * inductance_h / q_factor;
Self {
filter_type: FilterType::SingleTuned,
harmonic_order,
q_mvar,
fundamental_hz,
bus_kv,
q_factor,
capacitance_f,
inductance_h,
resistance_ohm,
}
}
pub fn high_pass(
harmonic_order: f64,
q_mvar: f64,
fundamental_hz: f64,
bus_kv: f64,
q_factor: f64,
) -> Self {
let mut filter =
Self::single_tuned(harmonic_order, q_mvar, fundamental_hz, bus_kv, q_factor);
filter.filter_type = FilterType::HighPass;
filter.resistance_ohm = q_factor
* 2.0
* std::f64::consts::PI
* fundamental_hz
* harmonic_order
* filter.inductance_h;
filter
}
pub fn impedance(&self, freq_hz: f64) -> Complex64 {
let omega = 2.0 * std::f64::consts::PI * freq_hz;
let z_c = Complex64::new(0.0, -1.0 / (omega * self.capacitance_f));
let z_l = Complex64::new(0.0, omega * self.inductance_h);
let z_r = Complex64::new(self.resistance_ohm, 0.0);
match self.filter_type {
FilterType::SingleTuned | FilterType::CType => {
z_r + z_l + z_c
}
FilterType::HighPass => {
let z_rl = (z_r * z_l) / (z_r + z_l);
z_rl + z_c
}
}
}
pub fn mitigation_factor(&self, harmonic_order: u32, z_system_ohm: f64) -> f64 {
let freq = self.fundamental_hz * harmonic_order as f64;
let z_f = self.impedance(freq).norm();
if z_f + z_system_ohm < 1e-9 {
return 1.0;
}
z_f / (z_f + z_system_ohm)
}
pub fn apply_to_spectrum(
&self,
spectrum: &HarmonicSpectrum,
z_source_ohm: f64,
) -> HarmonicSpectrum {
let mut mitigated = spectrum.clone();
let mut thd_sum_sq = 0.0_f64;
for h in &mut mitigated.harmonics {
let mf = self.mitigation_factor(h.order, z_source_ohm);
h.magnitude *= mf;
h.ihd_pct = h.magnitude / mitigated.fundamental * 100.0;
thd_sum_sq += h.magnitude * h.magnitude;
}
mitigated.thd_pct = thd_sum_sq.sqrt() / mitigated.fundamental * 100.0;
mitigated
}
pub fn reactive_power_mvar(&self) -> f64 {
let omega = 2.0 * std::f64::consts::PI * self.fundamental_hz;
let xc = 1.0 / (omega * self.capacitance_f);
let v_ll = self.bus_kv * 1000.0;
v_ll * v_ll / xc / 1e6
}
pub fn capacitance_uf(&self) -> f64 {
self.capacitance_f * 1e6
}
pub fn inductance_mh(&self) -> f64 {
self.inductance_h * 1e3
}
}
pub fn design_filter_bank(
harmonic_orders: &[u32],
q_total_mvar: f64,
fundamental_hz: f64,
bus_kv: f64,
q_factor: f64,
) -> Vec<PassiveFilter> {
let q_per_filter = q_total_mvar / harmonic_orders.len() as f64;
harmonic_orders
.iter()
.map(|&h| {
PassiveFilter::single_tuned(
h as f64 - 0.15, q_per_filter,
fundamental_hz,
bus_kv,
q_factor,
)
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_filter() -> PassiveFilter {
PassiveFilter::single_tuned(5.0, 10.0, 60.0, 13.8, 50.0)
}
#[test]
fn test_filter_minimum_impedance_at_tuning_freq() {
let f = sample_filter();
let f_tune = f.fundamental_hz * f.harmonic_order;
let z_tune = f.impedance(f_tune).norm();
let z_fund = f.impedance(f.fundamental_hz).norm();
assert!(
z_tune < z_fund / 5.0,
"|Z_tune|={:.3} Ω should be << |Z_fund|={:.3} Ω",
z_tune,
z_fund
);
}
#[test]
fn test_filter_reactive_power_close_to_rated() {
let f = sample_filter();
let q_actual = f.reactive_power_mvar();
assert!(
(q_actual - f.q_mvar).abs() < f.q_mvar * 0.1,
"Q_actual={:.3} MVAr vs Q_spec={:.3} MVAr",
q_actual,
f.q_mvar
);
}
#[test]
fn test_filter_capacitance_positive() {
let f = sample_filter();
assert!(
f.capacitance_uf() > 0.0,
"C={:.4} μF should be positive",
f.capacitance_uf()
);
assert!(
f.inductance_mh() > 0.0,
"L={:.4} mH should be positive",
f.inductance_mh()
);
}
#[test]
fn test_high_pass_filter_bounded_at_high_harmonics() {
let hp = PassiveFilter::high_pass(3.0, 5.0, 60.0, 13.8, 10.0);
let z11 = hp.impedance(11.0 * 60.0).norm();
let z25 = hp.impedance(25.0 * 60.0).norm();
assert!(
z11 <= hp.resistance_ohm + 1.0,
"|Z_11|={:.3} should be ≤ R={:.3}",
z11,
hp.resistance_ohm
);
assert!(
z25 <= hp.resistance_ohm + 1.0,
"|Z_25|={:.3} should be ≤ R={:.3}",
z25,
hp.resistance_ohm
);
assert!(z25.is_finite(), "|Z_25| should be finite");
}
#[test]
fn test_mitigation_factor_reduces_at_tuning() {
let f = sample_filter();
let mf = f.mitigation_factor(5, 1.0); assert!(mf < 1.0, "Mitigation factor should be < 1.0: {:.4}", mf);
assert!(mf >= 0.0, "Mitigation factor should be ≥ 0.0");
}
#[test]
fn test_filter_bank_design() {
let bank = design_filter_bank(&[5, 7, 11], 30.0, 60.0, 13.8, 50.0);
assert_eq!(bank.len(), 3);
for f in &bank {
assert!(f.q_mvar > 0.0);
}
}
}