use serde::{Deserialize, Serialize};
use std::f64::consts::PI;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum DcDcTopology {
BuckConverter,
BoostConverter,
BuckBoostConverter,
CukConverter,
SepicConverter,
DualActiveBridge {
transformer_ratio: f64,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DcDcConverter {
pub topology: DcDcTopology,
pub v_in: f64,
pub v_out_ref: f64,
pub duty_cycle: f64,
pub inductance_h: f64,
pub capacitance_f: f64,
pub resistance_ohm: f64,
pub switching_freq_hz: f64,
pub il: f64,
pub vc: f64,
}
impl DcDcConverter {
pub fn new(
topology: DcDcTopology,
v_in: f64,
v_out_ref: f64,
inductance_h: f64,
capacitance_f: f64,
resistance_ohm: f64,
switching_freq_hz: f64,
) -> Self {
let mut conv = Self {
topology,
v_in,
v_out_ref,
duty_cycle: 0.5,
inductance_h,
capacitance_f,
resistance_ohm,
switching_freq_hz,
il: 0.0,
vc: v_out_ref,
};
conv.duty_cycle = conv.compute_duty_cycle();
conv
}
pub fn voltage_ratio(&self) -> f64 {
let d = self.duty_cycle;
match &self.topology {
DcDcTopology::BuckConverter => d,
DcDcTopology::BoostConverter => {
let denom = (1.0 - d).max(1e-9);
1.0 / denom
}
DcDcTopology::BuckBoostConverter | DcDcTopology::CukConverter => {
let denom = (1.0 - d).max(1e-9);
-d / denom
}
DcDcTopology::SepicConverter => {
let denom = (1.0 - d).max(1e-9);
d / denom
}
DcDcTopology::DualActiveBridge { transformer_ratio } => transformer_ratio * d,
}
}
pub fn compute_duty_cycle(&self) -> f64 {
let v_in = self.v_in.max(1e-9);
let v_out = self.v_out_ref;
match &self.topology {
DcDcTopology::BuckConverter => (v_out / v_in).clamp(0.01, 0.99),
DcDcTopology::BoostConverter => {
let d = 1.0 - v_in / v_out.abs().max(1e-9);
d.clamp(0.01, 0.99)
}
DcDcTopology::BuckBoostConverter
| DcDcTopology::CukConverter
| DcDcTopology::SepicConverter => {
let vabs = v_out.abs();
(vabs / (v_in + vabs)).clamp(0.01, 0.99)
}
DcDcTopology::DualActiveBridge { .. } => {
0.5
}
}
}
pub fn current_ripple_a(&self) -> f64 {
let d = self.duty_cycle;
let l = self.inductance_h.max(1e-12);
let fsw = self.switching_freq_hz.max(1.0);
match &self.topology {
DcDcTopology::BuckConverter => {
let v_out = self.v_out_ref;
let delta_v = (self.v_in - v_out).abs();
delta_v * d / (l * fsw)
}
_ => self.v_in * d / (l * fsw),
}
}
pub fn voltage_ripple_v(&self) -> f64 {
let c = self.capacitance_f.max(1e-15);
let fsw = self.switching_freq_hz.max(1.0);
self.current_ripple_a() / (8.0 * c * fsw)
}
pub fn efficiency(&self, rdson_ohm: f64, c_oss_f: f64) -> f64 {
let vc = self.vc.max(0.0);
let r = self.resistance_ohm.max(1e-9);
let p_out = vc * vc / r;
let i_load = vc / r;
let p_cond = i_load * i_load * rdson_ohm;
let p_sw = 0.5 * c_oss_f * self.v_in * self.v_in * self.switching_freq_hz;
let p_in = p_out + p_cond + p_sw;
if p_in < 1e-12 {
return 1.0;
}
(p_out / p_in).clamp(0.0, 1.0)
}
fn derivatives(&self, il: f64, vc: f64) -> (f64, f64) {
let d = self.duty_cycle;
let l = self.inductance_h.max(1e-12);
let c = self.capacitance_f.max(1e-15);
let r = self.resistance_ohm.max(1e-9);
match &self.topology {
DcDcTopology::BuckConverter => {
let dil = (self.v_in * d - vc) / l;
let dvc = (il - vc / r) / c;
(dil, dvc)
}
DcDcTopology::BoostConverter | DcDcTopology::DualActiveBridge { .. } => {
let dil = (self.v_in - (1.0 - d) * vc) / l;
let dvc = ((1.0 - d) * il - vc / r) / c;
(dil, dvc)
}
DcDcTopology::BuckBoostConverter | DcDcTopology::CukConverter => {
let dil = (self.v_in * d - (1.0 - d) * vc) / l;
let dvc = ((1.0 - d) * il - vc / r) / c;
(dil, dvc)
}
DcDcTopology::SepicConverter => {
let dil = (self.v_in - (1.0 - d) * vc) / l;
let dvc = ((1.0 - d) * il - vc / r) / c;
(dil, dvc)
}
}
}
pub fn step_rk4(&mut self, dt: f64) {
let (il0, vc0) = (self.il, self.vc);
let (k1_il, k1_vc) = self.derivatives(il0, vc0);
let il1 = il0 + 0.5 * dt * k1_il;
let vc1 = vc0 + 0.5 * dt * k1_vc;
let (k2_il, k2_vc) = self.derivatives(il1, vc1);
let il2 = il0 + 0.5 * dt * k2_il;
let vc2 = vc0 + 0.5 * dt * k2_vc;
let (k3_il, k3_vc) = self.derivatives(il2, vc2);
let il3 = il0 + dt * k3_il;
let vc3 = vc0 + dt * k3_vc;
let (k4_il, k4_vc) = self.derivatives(il3, vc3);
self.il = il0 + (dt / 6.0) * (k1_il + 2.0 * k2_il + 2.0 * k3_il + k4_il);
self.vc = vc0 + (dt / 6.0) * (k1_vc + 2.0 * k2_vc + 2.0 * k3_vc + k4_vc);
}
pub fn simulate(&mut self, duration_s: f64, dt: f64) -> Vec<(f64, f64, f64)> {
let n_steps = if dt > 0.0 && duration_s > 0.0 {
(duration_s / dt).ceil() as usize + 1
} else {
1
};
let mut traj = Vec::with_capacity(n_steps);
let mut t = 0.0_f64;
traj.push((t, self.il, self.vc));
let effective_dt = if dt > 0.0 { dt } else { 1e-6 };
while t < duration_s - 0.5 * effective_dt {
self.step_rk4(effective_dt);
t += effective_dt;
traj.push((t, self.il, self.vc));
}
traj
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ThreePhaseVsc {
pub v_dc: f64,
pub v_ac_nom: f64,
pub rated_power_mva: f64,
pub l_filter_h: f64,
pub r_filter_ohm: f64,
pub c_filter_f: f64,
pub omega: f64,
pub kp_current: f64,
pub ki_current: f64,
pub kp_pll: f64,
pub ki_pll: f64,
pub id: f64,
pub iq: f64,
pub theta_pll: f64,
pub omega_pll: f64,
pub int_d: f64,
pub int_q: f64,
pub int_pll: f64,
}
impl ThreePhaseVsc {
pub fn new(
v_dc: f64,
v_ac_nom: f64,
rated_power_mva: f64,
l_filter_h: f64,
r_filter_ohm: f64,
omega: f64,
) -> Self {
Self {
v_dc,
v_ac_nom,
rated_power_mva,
l_filter_h,
r_filter_ohm,
c_filter_f: 0.0,
omega,
kp_current: 2.0,
ki_current: 50.0,
kp_pll: 50.0,
ki_pll: 1000.0,
id: 0.0,
iq: 0.0,
theta_pll: 0.0,
omega_pll: omega,
int_d: 0.0,
int_q: 0.0,
int_pll: 0.0,
}
}
pub fn park_transform(va: f64, vb: f64, vc: f64, theta: f64) -> (f64, f64) {
let cos0 = theta.cos();
let cos_n = (theta - 2.0 * PI / 3.0).cos();
let cos_p = (theta + 2.0 * PI / 3.0).cos();
let sin0 = theta.sin();
let sin_n = (theta - 2.0 * PI / 3.0).sin();
let sin_p = (theta + 2.0 * PI / 3.0).sin();
let vd = (2.0 / 3.0) * (va * cos0 + vb * cos_n + vc * cos_p);
let vq = -(2.0 / 3.0) * (va * sin0 + vb * sin_n + vc * sin_p);
(vd, vq)
}
pub fn inv_park_transform(vd: f64, vq: f64, theta: f64) -> (f64, f64, f64) {
let va = vd * theta.cos() - vq * theta.sin();
let vb = vd * (theta - 2.0 * PI / 3.0).cos() - vq * (theta - 2.0 * PI / 3.0).sin();
let vc_out = vd * (theta + 2.0 * PI / 3.0).cos() - vq * (theta + 2.0 * PI / 3.0).sin();
(va, vb, vc_out)
}
pub fn compute_power(&self, vd_grid: f64, vq_grid: f64) -> (f64, f64) {
let p = 1.5 * (vd_grid * self.id + vq_grid * self.iq);
let q = 1.5 * (vq_grid * self.id - vd_grid * self.iq);
(p, q)
}
pub fn pll_step(&mut self, vq_measured: f64, dt: f64) {
let error = vq_measured;
self.int_pll += error * dt;
self.omega_pll = self.omega + self.kp_pll * error + self.ki_pll * self.int_pll;
self.theta_pll += self.omega_pll * dt;
while self.theta_pll > PI {
self.theta_pll -= 2.0 * PI;
}
while self.theta_pll < -PI {
self.theta_pll += 2.0 * PI;
}
}
pub fn current_controller_step(
&mut self,
id_ref: f64,
iq_ref: f64,
vd_grid: f64,
vq_grid: f64,
dt: f64,
) -> (f64, f64) {
let err_d = id_ref - self.id;
let err_q = iq_ref - self.iq;
self.int_d += err_d * dt;
self.int_q += err_q * dt;
let vd_raw = self.kp_current * err_d
+ self.ki_current * self.int_d
+ self.omega * self.l_filter_h * self.iq
+ vd_grid;
let vq_raw = self.kp_current * err_q + self.ki_current * self.int_q
- self.omega * self.l_filter_h * self.id
+ vq_grid;
let v_limit = self.v_dc / 2.0;
let vd_ref = vd_raw.clamp(-v_limit, v_limit);
let vq_ref = vq_raw.clamp(-v_limit, v_limit);
if vd_raw.abs() > v_limit {
let back_calc = (vd_ref - vd_raw) / self.ki_current.max(1e-9);
self.int_d += back_calc;
}
if vq_raw.abs() > v_limit {
let back_calc = (vq_ref - vq_raw) / self.ki_current.max(1e-9);
self.int_q += back_calc;
}
(vd_ref, vq_ref)
}
pub fn power_to_current_refs(p_ref_mw: f64, q_ref_mvar: f64, vd_grid_pu: f64) -> (f64, f64) {
let vd = (1.5 * vd_grid_pu).max(1e-9);
let id_ref = p_ref_mw / vd;
let iq_ref = -q_ref_mvar / vd;
(id_ref, iq_ref)
}
pub fn step_rk4(
&mut self,
p_ref_mw: f64,
q_ref_mvar: f64,
vd_grid: f64,
vq_grid: f64,
dt: f64,
) {
let (id_ref, iq_ref) = Self::power_to_current_refs(p_ref_mw, q_ref_mvar, vd_grid);
let (vd_mod, vq_mod) = self.current_controller_step(id_ref, iq_ref, vd_grid, vq_grid, dt);
let l = self.l_filter_h.max(1e-12);
let r = self.r_filter_ohm;
let omega = self.omega;
let deriv = |id: f64, iq: f64| -> (f64, f64) {
let did = (vd_mod - vd_grid - r * id + omega * l * iq) / l;
let diq = (vq_mod - vq_grid - r * iq - omega * l * id) / l;
(did, diq)
};
let (id0, iq0) = (self.id, self.iq);
let (k1d, k1q) = deriv(id0, iq0);
let (k2d, k2q) = deriv(id0 + 0.5 * dt * k1d, iq0 + 0.5 * dt * k1q);
let (k3d, k3q) = deriv(id0 + 0.5 * dt * k2d, iq0 + 0.5 * dt * k2q);
let (k4d, k4q) = deriv(id0 + dt * k3d, iq0 + dt * k3q);
self.id = id0 + (dt / 6.0) * (k1d + 2.0 * k2d + 2.0 * k3d + k4d);
self.iq = iq0 + (dt / 6.0) * (k1q + 2.0 * k2q + 2.0 * k3q + k4q);
}
pub fn dc_bus_ripple(&self, c_dc_f: f64, f_sw_hz: f64) -> f64 {
let c = c_dc_f.max(1e-15);
let f = f_sw_hz.max(1.0);
let v = self.v_dc.max(1e-9);
self.rated_power_mva * 1.0e6 / (c * v * 6.0 * f)
}
pub fn modulation_index(&self, vd_ref: f64, vq_ref: f64) -> f64 {
let v_half = (self.v_dc / 2.0).max(1e-9);
(vd_ref * vd_ref + vq_ref * vq_ref).sqrt() / v_half
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NpcConverter {
pub levels: usize,
pub v_dc: f64,
pub rated_power_mva: f64,
pub switching_freq_hz: f64,
pub carrier_phase_shift_deg: f64,
}
impl NpcConverter {
pub fn new(levels: usize, v_dc: f64, rated_power_mva: f64, switching_freq_hz: f64) -> Self {
let levels = levels.max(2);
let mut conv = Self {
levels,
v_dc,
rated_power_mva,
switching_freq_hz,
carrier_phase_shift_deg: 0.0,
};
conv.carrier_phase_shift_deg = conv.optimal_phase_shift();
conv
}
pub fn voltage_levels(&self) -> Vec<f64> {
let n = self.levels;
if n < 2 {
return vec![0.0];
}
let v_half = self.v_dc / 2.0;
(0..n)
.map(|k| -v_half + (k as f64) * self.v_dc / (n as f64 - 1.0))
.collect()
}
pub fn thd_estimate(&self, m_index: f64) -> f64 {
let m = m_index.clamp(0.0, 1.0);
let thd_two_level = (1.0 - m) * 40.0; let divisor = ((self.levels - 1).max(1) as f64).sqrt();
thd_two_level / divisor
}
pub fn device_count(&self) -> usize {
let n = self.levels;
let per_phase = 3 * (n - 1);
per_phase * 3
}
pub fn effective_switching_freq(&self) -> f64 {
(self.levels - 1).max(1) as f64 * self.switching_freq_hz
}
pub fn optimal_phase_shift(&self) -> f64 {
180.0 / (self.levels - 1).max(1) as f64
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MatrixConverter {
pub n_inputs: usize,
pub n_outputs: usize,
pub switching_freq_hz: f64,
pub input_freq_hz: f64,
pub output_freq_hz: f64,
pub voltage_transfer_ratio: f64,
}
impl MatrixConverter {
pub fn new(input_freq_hz: f64, output_freq_hz: f64, switching_freq_hz: f64) -> Self {
Self {
n_inputs: 3,
n_outputs: 3,
switching_freq_hz,
input_freq_hz,
output_freq_hz,
voltage_transfer_ratio: Self::max_voltage_ratio(),
}
}
pub fn max_voltage_ratio() -> f64 {
3_f64.sqrt() / 2.0
}
pub fn input_power_factor(&self) -> f64 {
1.0
}
pub fn frequency_ratio(&self) -> f64 {
self.output_freq_hz / self.input_freq_hz.max(1e-9)
}
pub fn valid_switch_states(&self) -> usize {
self.n_inputs.pow(self.n_outputs as u32)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::f64::consts::PI;
fn make_buck(v_in: f64, v_out: f64) -> DcDcConverter {
DcDcConverter::new(
DcDcTopology::BuckConverter,
v_in,
v_out,
1e-3, 100e-6, 10.0, 20e3, )
}
fn make_boost(v_in: f64, v_out: f64) -> DcDcConverter {
DcDcConverter::new(
DcDcTopology::BoostConverter,
v_in,
v_out,
1e-3,
100e-6,
10.0,
20e3,
)
}
#[test]
fn test_buck_voltage_ratio() {
let mut conv = make_buck(24.0, 12.0);
conv.duty_cycle = 0.5;
let ratio = conv.voltage_ratio();
assert!(
(ratio - 0.5).abs() < 1e-9,
"Buck ratio at D=0.5 should be 0.5, got {ratio}"
);
}
#[test]
fn test_boost_voltage_ratio() {
let mut conv = make_boost(12.0, 24.0);
conv.duty_cycle = 0.5;
let ratio = conv.voltage_ratio();
assert!(
(ratio - 2.0).abs() < 1e-9,
"Boost ratio at D=0.5 should be 2.0, got {ratio}"
);
}
#[test]
fn test_buck_boost_voltage_ratio() {
let mut conv = DcDcConverter::new(
DcDcTopology::BuckBoostConverter,
12.0,
12.0,
1e-3,
100e-6,
10.0,
20e3,
);
conv.duty_cycle = 0.5;
let ratio = conv.voltage_ratio();
assert!(
(ratio - (-1.0)).abs() < 1e-9,
"BuckBoost ratio at D=0.5 should be -1.0, got {ratio}"
);
}
#[test]
fn test_buck_duty_cycle_computation() {
let conv = make_buck(24.0, 12.0);
let d = conv.duty_cycle;
assert!(
(d - 0.5).abs() < 1e-9,
"Buck duty cycle for 12/24 V should be 0.5, got {d}"
);
}
#[test]
fn test_boost_duty_cycle_computation() {
let conv = make_boost(12.0, 24.0);
let d = conv.duty_cycle;
assert!(
(d - 0.5).abs() < 1e-9,
"Boost duty cycle for 12→24 V should be 0.5, got {d}"
);
}
#[test]
fn test_buck_current_ripple() {
let conv = make_buck(24.0, 12.0);
let ripple = conv.current_ripple_a();
assert!(
ripple > 0.0 && ripple.is_finite(),
"Buck current ripple should be positive and finite, got {ripple}"
);
}
#[test]
fn test_boost_current_ripple() {
let conv = make_boost(12.0, 24.0);
let ripple = conv.current_ripple_a();
assert!(
ripple > 0.0 && ripple.is_finite(),
"Boost current ripple should be positive and finite, got {ripple}"
);
}
#[test]
fn test_buck_voltage_ripple() {
let conv = make_buck(24.0, 12.0);
let ripple = conv.voltage_ripple_v();
assert!(
ripple > 0.0 && ripple.is_finite(),
"Buck voltage ripple should be positive and finite, got {ripple}"
);
}
#[test]
fn test_efficiency_estimate() {
let conv = make_buck(24.0, 12.0);
let eta = conv.efficiency(0.01, 100e-12);
assert!(
eta > 0.0 && eta <= 1.0,
"Efficiency should be in (0, 1], got {eta}"
);
}
#[test]
fn test_dc_dc_rk4_step_converges() {
let mut conv = make_buck(24.0, 12.0);
conv.step_rk4(1e-6);
assert!(
conv.il.is_finite() && !conv.il.is_nan(),
"il should be finite after RK4 step"
);
assert!(
conv.vc.is_finite() && !conv.vc.is_nan(),
"vc should be finite after RK4 step"
);
}
#[test]
fn test_dc_dc_simulation_reaches_steady_state() {
let mut conv = make_buck(24.0, 12.0);
let traj = conv.simulate(5e-3, 1e-6);
assert!(!traj.is_empty(), "trajectory should not be empty");
let last_vc = traj.last().expect("trajectory has entries").2;
let v_ref = 12.0;
let error_pct = (last_vc - v_ref).abs() / v_ref;
assert!(
error_pct < 0.20,
"vc={last_vc:.3} V should be within 20% of v_out_ref={v_ref} V"
);
}
#[test]
fn test_park_transform_abc_to_dq() {
let theta = 0.0_f64;
let va = 1.0_f64;
let vb = (theta - 2.0 * PI / 3.0).cos();
let vc = (theta + 2.0 * PI / 3.0).cos();
let (vd, vq) = ThreePhaseVsc::park_transform(va, vb, vc, theta);
assert!((vd - 1.0).abs() < 1e-9, "vd should be ≈1, got {vd}");
assert!(vq.abs() < 1e-9, "vq should be ≈0, got {vq}");
}
#[test]
fn test_inv_park_transform_dq_to_abc() {
let (va, _vb, _vc) = ThreePhaseVsc::inv_park_transform(1.0, 0.0, 0.0);
assert!(
(va - 1.0).abs() < 1e-9,
"va from inv_park should be ≈1.0, got {va}"
);
}
#[test]
fn test_park_inv_park_roundtrip() {
let theta = PI / 4.0;
let amplitude = 1.5_f64;
let va_orig = amplitude * theta.cos();
let vb_orig = amplitude * (theta - 2.0 * PI / 3.0).cos();
let vc_orig = amplitude * (theta + 2.0 * PI / 3.0).cos();
let (vd, vq) = ThreePhaseVsc::park_transform(va_orig, vb_orig, vc_orig, theta);
let (va_r, vb_r, vc_r) = ThreePhaseVsc::inv_park_transform(vd, vq, theta);
assert!(
(va_r - va_orig).abs() < 1e-9,
"va roundtrip failed: {va_r} vs {va_orig}"
);
assert!(
(vb_r - vb_orig).abs() < 1e-9,
"vb roundtrip failed: {vb_r} vs {vb_orig}"
);
assert!(
(vc_r - vc_orig).abs() < 1e-9,
"vc roundtrip failed: {vc_r} vs {vc_orig}"
);
}
#[test]
fn test_vsc_power_computation() {
let vsc = ThreePhaseVsc::new(400.0, 230.0, 0.1, 1e-3, 0.1, 2.0 * PI * 50.0);
let mut vsc = vsc;
vsc.id = 1.0;
vsc.iq = 0.0;
let (p, q) = vsc.compute_power(1.0, 0.0);
assert!((p - 1.5).abs() < 1e-9, "P should be 1.5, got {p}");
assert!(q.abs() < 1e-9, "Q should be 0, got {q}");
}
#[test]
fn test_vsc_pll_step() {
let mut vsc = ThreePhaseVsc::new(400.0, 230.0, 0.1, 1e-3, 0.1, 2.0 * PI * 50.0);
let omega_before = vsc.omega_pll;
vsc.pll_step(0.1, 1e-4);
assert!(
(vsc.omega_pll - omega_before).abs() > 1e-9,
"omega_pll should change after PLL step with non-zero vq_measured"
);
}
#[test]
fn test_vsc_current_controller_step() {
let mut vsc = ThreePhaseVsc::new(400.0, 230.0, 0.1, 1e-3, 0.1, 2.0 * PI * 50.0);
let (vd_ref, vq_ref) = vsc.current_controller_step(1.0, 0.0, 200.0, 0.0, 1e-4);
assert!(vd_ref.is_finite(), "vd_ref should be finite");
assert!(vq_ref.is_finite(), "vq_ref should be finite");
}
#[test]
fn test_power_to_current_refs() {
let (id_ref, iq_ref) = ThreePhaseVsc::power_to_current_refs(1.5, 0.0, 1.0);
assert!(
(id_ref - 1.0).abs() < 1e-9,
"id_ref should be ≈1.0, got {id_ref}"
);
assert!(iq_ref.abs() < 1e-9, "iq_ref should be 0.0, got {iq_ref}");
}
#[test]
fn test_vsc_rk4_step() {
let mut vsc = ThreePhaseVsc::new(400.0, 230.0, 1.0, 1e-3, 0.1, 2.0 * PI * 50.0);
vsc.step_rk4(1.0, 0.0, 163.0, 0.0, 1e-4);
assert!(
vsc.id.is_finite() && !vsc.id.is_nan(),
"id should be finite after RK4 step"
);
assert!(
vsc.iq.is_finite() && !vsc.iq.is_nan(),
"iq should be finite after RK4 step"
);
}
#[test]
fn test_npc_voltage_levels() {
let npc = NpcConverter::new(3, 600.0, 1.0, 5e3);
let levels = npc.voltage_levels();
assert_eq!(levels.len(), 3, "3-level NPC should have exactly 3 levels");
assert!(
(levels[0] - (-300.0)).abs() < 1e-6,
"first level should be -V_dc/2=-300, got {}",
levels[0]
);
assert!(
(levels[2] - 300.0).abs() < 1e-6,
"last level should be +V_dc/2=+300, got {}",
levels[2]
);
}
#[test]
fn test_npc_thd_estimate() {
let npc3 = NpcConverter::new(3, 600.0, 1.0, 5e3);
let npc5 = NpcConverter::new(5, 600.0, 1.0, 5e3);
let thd3 = npc3.thd_estimate(0.8);
let thd5 = npc5.thd_estimate(0.8);
assert!(
thd5 < thd3,
"5-level NPC THD ({thd5:.2}%) should be less than 3-level ({thd3:.2}%)"
);
assert!(thd3 >= 0.0 && thd5 >= 0.0, "THD must be non-negative");
}
#[test]
fn test_matrix_converter_max_ratio() {
let ratio = MatrixConverter::max_voltage_ratio();
let expected = 3_f64.sqrt() / 2.0; assert!(
(ratio - expected).abs() < 1e-3,
"max voltage ratio should be ≈0.866, got {ratio}"
);
assert!(
(ratio - 0.866).abs() < 1e-3,
"max voltage ratio should be ≈0.866 numerically, got {ratio}"
);
}
#[test]
fn test_matrix_converter_valid_switch_states() {
let mc = MatrixConverter::new(50.0, 60.0, 10e3);
assert_eq!(
mc.valid_switch_states(),
27,
"3×3 matrix converter has 27 valid switch states"
);
}
#[test]
fn test_matrix_converter_frequency_ratio() {
let mc = MatrixConverter::new(50.0, 75.0, 10e3);
assert!(
(mc.frequency_ratio() - 1.5).abs() < 1e-9,
"freq ratio should be 1.5"
);
}
#[test]
fn test_npc_device_count() {
let npc3 = NpcConverter::new(3, 600.0, 1.0, 5e3);
let count = npc3.device_count();
assert!(count > 0, "device count should be positive, got {count}");
}
#[test]
fn test_npc_effective_switching_freq() {
let npc = NpcConverter::new(3, 600.0, 1.0, 5e3);
let eff = npc.effective_switching_freq();
assert!(
(eff - 10e3).abs() < 1e-6,
"3-level effective fsw should be 2*5kHz=10kHz, got {eff}"
);
}
}