use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, PartialEq)]
pub enum ConverterError {
InvalidParameter(String),
SimulationFailed(String),
}
impl fmt::Display for ConverterError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidParameter(s) => write!(f, "invalid parameter: {s}"),
Self::SimulationFailed(s) => write!(f, "simulation failed: {s}"),
}
}
}
impl std::error::Error for ConverterError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ConverterTopology {
TwoLevelVsc,
ThreeLevelNpc,
ModularMultilevel,
HBridge,
BuckBoostDc,
}
impl ConverterTopology {
pub fn harmonic_factor(self) -> f64 {
match self {
Self::TwoLevelVsc => 1.0,
Self::ThreeLevelNpc => 0.5,
Self::ModularMultilevel => 0.1,
Self::HBridge => 0.8,
Self::BuckBoostDc => 1.2,
}
}
pub fn conduction_loss_coeff(self) -> f64 {
match self {
Self::TwoLevelVsc => 0.015,
Self::ThreeLevelNpc => 0.018,
Self::ModularMultilevel => 0.012,
Self::HBridge => 0.016,
Self::BuckBoostDc => 0.020,
}
}
pub fn switching_loss_coeff(self) -> f64 {
match self {
Self::TwoLevelVsc => 0.010,
Self::ThreeLevelNpc => 0.008,
Self::ModularMultilevel => 0.005,
Self::HBridge => 0.009,
Self::BuckBoostDc => 0.012,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConverterModel {
pub topology: ConverterTopology,
pub rated_power_mva: f64,
pub dc_voltage_kv: f64,
pub ac_voltage_kv: f64,
pub switching_freq_khz: f64,
pub inductance_pu: f64,
pub capacitance_pu: f64,
}
impl ConverterModel {
pub fn validate(&self) -> Result<(), ConverterError> {
if self.rated_power_mva <= 0.0 {
return Err(ConverterError::InvalidParameter(
"rated_power_mva must be positive".to_string(),
));
}
if self.dc_voltage_kv <= 0.0 {
return Err(ConverterError::InvalidParameter(
"dc_voltage_kv must be positive".to_string(),
));
}
if self.ac_voltage_kv <= 0.0 {
return Err(ConverterError::InvalidParameter(
"ac_voltage_kv must be positive".to_string(),
));
}
if self.switching_freq_khz <= 0.0 {
return Err(ConverterError::InvalidParameter(
"switching_freq_khz must be positive".to_string(),
));
}
if self.inductance_pu <= 0.0 {
return Err(ConverterError::InvalidParameter(
"inductance_pu must be positive".to_string(),
));
}
if self.capacitance_pu <= 0.0 {
return Err(ConverterError::InvalidParameter(
"capacitance_pu must be positive".to_string(),
));
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct ConverterState {
pub d_alpha: f64,
pub d_beta: f64,
pub i_alpha_pu: f64,
pub i_beta_pu: f64,
pub v_dc_pu: f64,
pub time_s: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConverterResult {
pub states: Vec<ConverterState>,
pub ac_power_mw: f64,
pub ac_reactive_mvar: f64,
pub dc_power_mw: f64,
pub efficiency_pct: f64,
pub thd_ac_pct: f64,
pub ripple_dc_pct: f64,
pub fundamental_component: (f64, f64),
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct ConverterController {
pub kp_current: f64,
pub ki_current: f64,
pub kp_voltage: f64,
pub ki_voltage: f64,
pub current_limit_pu: f64,
}
impl ConverterController {
pub fn default_vsc() -> Self {
Self {
kp_current: 2.0,
ki_current: 50.0,
kp_voltage: 0.5,
ki_voltage: 10.0,
current_limit_pu: 1.2,
}
}
}
pub struct ConverterSimulator {
model: ConverterModel,
controller: ConverterController,
}
impl ConverterSimulator {
pub fn new(model: ConverterModel, controller: ConverterController) -> Self {
Self { model, controller }
}
pub fn simulate_step_response(
&self,
p_setpoint_pu: f64,
q_setpoint_pu: f64,
v_grid_pu: f64,
theta_grid_rad: f64,
duration_s: f64,
dt_s: f64,
) -> Result<ConverterResult, ConverterError> {
self.model.validate()?;
if dt_s <= 0.0 || duration_s <= 0.0 {
return Err(ConverterError::InvalidParameter(
"dt_s and duration_s must be positive".to_string(),
));
}
if dt_s > duration_s {
return Err(ConverterError::InvalidParameter(
"dt_s must not exceed duration_s".to_string(),
));
}
let omega_base = 2.0 * std::f64::consts::PI * 50.0;
let l_pu = self.model.inductance_pu;
let c_pu = self.model.capacitance_pu;
let v_d = v_grid_pu; let v_q = 0.0_f64; let v_g_mag = v_grid_pu.max(1e-9);
let id_ref = p_setpoint_pu / v_g_mag;
let iq_ref = -q_setpoint_pu / v_g_mag;
let mut i_d = 0.0_f64; let mut i_q = 0.0_f64; let mut v_dc = 1.0_f64; let mut integral_d = 0.0_f64; let mut integral_q = 0.0_f64;
let dt_pu = dt_s * omega_base;
let r_pu = 0.005_f64;
let n_steps = (duration_s / dt_s).ceil() as usize + 1;
let mut states = Vec::with_capacity(n_steps);
let mut i_alpha_waveform = Vec::with_capacity(n_steps);
let kp = self.controller.kp_current;
let ki = self.controller.ki_current;
let ilim = self.controller.current_limit_pu;
for step in 0..n_steps {
let t = step as f64 * dt_s;
let err_d = id_ref - i_d;
let err_q = iq_ref - i_q;
integral_d += err_d * dt_pu;
integral_q += err_q * dt_pu;
let v_d_ref = v_d - l_pu * i_q + kp * err_d + ki * integral_d;
let v_q_ref = v_q + l_pu * i_d + kp * err_q + ki * integral_q;
let m_max = 1.0_f64;
let m_d = (v_d_ref / v_dc.max(1e-9)).clamp(-m_max, m_max);
let m_q = (v_q_ref / v_dc.max(1e-9)).clamp(-m_max, m_max);
let di_d = (m_d * v_dc - v_d - r_pu * i_d - l_pu * i_q) / l_pu * dt_pu;
let di_q = (m_q * v_dc - v_q - r_pu * i_q + l_pu * i_d) / l_pu * dt_pu;
i_d += di_d;
i_q += di_q;
let i_mag = (i_d * i_d + i_q * i_q).sqrt();
if i_mag > ilim {
let scale = ilim / i_mag;
i_d *= scale;
i_q *= scale;
integral_d *= scale;
integral_q *= scale;
}
let i_dc_out = m_d * i_d + m_q * i_q;
let dv_dc = -(i_dc_out - 1.0) / c_pu * dt_pu * 0.01;
v_dc = (v_dc + dv_dc).clamp(0.5, 2.0);
let theta_t = theta_grid_rad + omega_base * t;
let i_alpha = i_d * theta_t.cos() - i_q * theta_t.sin();
let i_beta = i_d * theta_t.sin() + i_q * theta_t.cos();
let d_alpha = m_d * theta_t.cos() - m_q * theta_t.sin();
let d_beta = m_d * theta_t.sin() + m_q * theta_t.cos();
i_alpha_waveform.push(i_alpha);
states.push(ConverterState {
d_alpha,
d_beta,
i_alpha_pu: i_alpha,
i_beta_pu: i_beta,
v_dc_pu: v_dc,
time_s: t,
});
}
let ss_start = (n_steps * 4 / 5).min(n_steps.saturating_sub(1));
let (i_d_ss, i_q_ss, v_dc_ss) = if ss_start < n_steps {
let count = (n_steps - ss_start) as f64;
let id_avg = states[ss_start..]
.iter()
.map(|s| {
let th = theta_grid_rad + omega_base * s.time_s;
s.i_alpha_pu * th.cos() + s.i_beta_pu * th.sin()
})
.sum::<f64>()
/ count;
let iq_avg = states[ss_start..]
.iter()
.map(|s| {
let th = theta_grid_rad + omega_base * s.time_s;
-s.i_alpha_pu * th.sin() + s.i_beta_pu * th.cos()
})
.sum::<f64>()
/ count;
let vdc_avg = states[ss_start..].iter().map(|s| s.v_dc_pu).sum::<f64>() / count;
(id_avg, iq_avg, vdc_avg)
} else {
(i_d, i_q, v_dc)
};
let ac_power_pu = v_g_mag * i_d_ss;
let ac_reactive_pu = -v_g_mag * i_q_ss;
let ac_power_mw = ac_power_pu * self.model.rated_power_mva;
let ac_reactive_mvar = ac_reactive_pu * self.model.rated_power_mva;
let p_abs = ac_power_pu.abs();
let efficiency_pct = self.compute_efficiency(p_abs);
let dc_power_mw = if efficiency_pct > 0.0 {
ac_power_mw / (efficiency_pct / 100.0)
} else {
0.0
};
let thd_ac_pct = self.compute_thd(&i_alpha_waveform, dt_s);
let v_dc_vals: Vec<f64> = states.iter().map(|s| s.v_dc_pu).collect();
let v_dc_max = v_dc_vals.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
let v_dc_min = v_dc_vals.iter().cloned().fold(f64::INFINITY, f64::min);
let ripple_dc_pct = if v_dc_ss > 1e-9 {
(v_dc_max - v_dc_min) / v_dc_ss * 100.0
} else {
0.0
};
let i_fund_mag = (i_d_ss * i_d_ss + i_q_ss * i_q_ss).sqrt();
let i_fund_angle_deg = i_q_ss.atan2(i_d_ss).to_degrees();
Ok(ConverterResult {
states,
ac_power_mw,
ac_reactive_mvar,
dc_power_mw,
efficiency_pct,
thd_ac_pct,
ripple_dc_pct,
fundamental_component: (i_fund_mag, i_fund_angle_deg),
})
}
fn compute_efficiency(&self, p_pu: f64) -> f64 {
let i_pu = p_pu.abs().sqrt().clamp(0.0, 2.0);
let f_sw = self.model.switching_freq_khz;
let v_dc = 1.0_f64;
let k_cond = self.model.topology.conduction_loss_coeff();
let k_sw = self.model.topology.switching_loss_coeff();
let p_conduction = k_cond * i_pu * i_pu;
let p_switching = k_sw * f_sw * v_dc * i_pu;
let p_loss = p_conduction + p_switching;
let p_out = p_pu.abs();
if p_out < 1e-9 {
return 100.0 * (1.0 - p_loss.min(0.999));
}
let eta = p_out / (p_out + p_loss);
(eta * 100.0).clamp(50.0, 100.0)
}
fn compute_thd(&self, waveform: &[f64], dt_s: f64) -> f64 {
let n_total = waveform.len();
if n_total < 4 {
return 0.0;
}
let f0 = 50.0_f64; let fs = 1.0 / dt_s;
let omega0 = 2.0 * std::f64::consts::PI * f0;
let samples_per_cycle = (fs / f0).round() as usize;
let ss_len = (2 * samples_per_cycle).min(n_total);
let ss_start = n_total.saturating_sub(ss_len);
let ss_waveform = &waveform[ss_start..];
let n = ss_waveform.len();
let mut harmonics_sq = 0.0_f64;
let mut h1_sq = 0.0_f64;
for order in 1u32..=20 {
let omega_h = omega0 * order as f64;
let mut re = 0.0_f64;
let mut im = 0.0_f64;
for (k, &x) in ss_waveform.iter().enumerate() {
let t = k as f64 / fs;
re += x * (omega_h * t).cos();
im -= x * (omega_h * t).sin();
}
re /= n as f64;
im /= n as f64;
let h_sq = 2.0 * (re * re + im * im);
if order == 1 {
h1_sq = h_sq;
} else {
harmonics_sq += h_sq;
}
}
if h1_sq < 1e-12 {
return 0.0;
}
let factor = self.model.topology.harmonic_factor();
(harmonics_sq / h1_sq).sqrt() * 100.0 * factor
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::f64::consts::PI;
fn make_vsc() -> ConverterSimulator {
let model = ConverterModel {
topology: ConverterTopology::TwoLevelVsc,
rated_power_mva: 100.0,
dc_voltage_kv: 200.0,
ac_voltage_kv: 100.0,
switching_freq_khz: 1.05, inductance_pu: 0.15,
capacitance_pu: 0.05,
};
let ctrl = ConverterController {
kp_current: 2.0,
ki_current: 50.0,
kp_voltage: 0.5,
ki_voltage: 10.0,
current_limit_pu: 1.2,
};
ConverterSimulator::new(model, ctrl)
}
#[test]
fn test_step_to_rated_p_reaches_setpoint() {
let sim = make_vsc();
let result = sim
.simulate_step_response(1.0, 0.0, 1.0, 0.0, 0.4, 1e-4)
.expect("simulation must succeed");
assert!(
result.ac_power_mw > 50.0,
"AC power should exceed 50 MW after step, got {:.2} MW",
result.ac_power_mw
);
}
#[test]
fn test_reactive_power_step() {
let sim = make_vsc();
let result = sim
.simulate_step_response(0.0, 0.5, 1.0, 0.0, 0.4, 1e-4)
.expect("simulation must succeed");
assert!(
result.ac_reactive_mvar.abs() > 1.0,
"reactive power should respond to Q* step, got {:.2} MVAR",
result.ac_reactive_mvar
);
}
#[test]
fn test_current_limiting() {
let sim = make_vsc();
let result = sim
.simulate_step_response(2.0, 0.0, 1.0, 0.0, 0.2, 1e-4)
.expect("simulation must succeed");
let limit = sim.controller.current_limit_pu + 1e-6; for s in &result.states {
let i_mag = (s.i_alpha_pu * s.i_alpha_pu + s.i_beta_pu * s.i_beta_pu).sqrt();
assert!(
i_mag <= limit,
"current magnitude {:.4} exceeds limit {:.4} at t={:.4}",
i_mag,
limit,
s.time_s
);
}
}
#[test]
fn test_efficiency_higher_at_rated() {
let sim = make_vsc();
let eff_rated = sim.compute_efficiency(1.0);
let eff_partial = sim.compute_efficiency(0.2);
assert!(
eff_rated > eff_partial,
"efficiency at rated ({:.2}%) should exceed partial ({:.2}%)",
eff_rated,
eff_partial
);
}
#[test]
fn test_thd_reasonable_for_two_level_vsc() {
let sim = make_vsc();
let result = sim
.simulate_step_response(1.0, 0.0, 1.0, 0.0, 0.4, 1e-4)
.expect("simulation must succeed");
assert!(
result.thd_ac_pct < 10.0,
"THD should be < 10% for 2-level VSC, got {:.2}%",
result.thd_ac_pct
);
}
#[test]
fn test_invalid_parameters_rejected() {
let model = ConverterModel {
topology: ConverterTopology::TwoLevelVsc,
rated_power_mva: -1.0, dc_voltage_kv: 200.0,
ac_voltage_kv: 100.0,
switching_freq_khz: 1.0,
inductance_pu: 0.15,
capacitance_pu: 0.05,
};
let ctrl = ConverterController::default_vsc();
let sim = ConverterSimulator::new(model, ctrl);
let result = sim.simulate_step_response(1.0, 0.0, 1.0, 0.0, 0.1, 1e-3);
assert!(result.is_err(), "negative rated power should return error");
}
#[test]
fn test_pure_sine_thd_near_zero() {
let sim = make_vsc();
let dt = 1e-4_f64;
let omega = 2.0 * PI * 50.0;
let n = 400; let sine: Vec<f64> = (0..n).map(|k| (omega * k as f64 * dt).sin()).collect();
let thd = sim.compute_thd(&sine, dt);
assert!(
thd < 5.0,
"pure sine THD should be near zero, got {:.4}%",
thd
);
}
#[test]
fn test_mmc_lower_harmonic_factor() {
assert!(
ConverterTopology::ModularMultilevel.harmonic_factor()
< ConverterTopology::TwoLevelVsc.harmonic_factor()
);
}
}