use num_complex::Complex;
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VectorGroup {
Dyn11,
Yny0,
Dyn5,
YNyn0,
Dzn0,
Yyd0,
}
impl VectorGroup {
pub fn phase_shift_deg(&self) -> f64 {
match self {
VectorGroup::Dyn11 => 30.0,
VectorGroup::Yny0 => 0.0,
VectorGroup::Dyn5 => 150.0,
VectorGroup::YNyn0 => 0.0,
VectorGroup::Dzn0 => 0.0,
VectorGroup::Yyd0 => 0.0,
}
}
}
impl fmt::Display for VectorGroup {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
VectorGroup::Dyn11 => "Dyn11",
VectorGroup::Yny0 => "Yny0",
VectorGroup::Dyn5 => "Dyn5",
VectorGroup::YNyn0 => "YNyn0",
VectorGroup::Dzn0 => "Dzn0",
VectorGroup::Yyd0 => "Yyd0",
};
write!(f, "{s}")
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CoolingType {
Onan,
Onaf,
Ofaf,
Odaf,
}
impl fmt::Display for CoolingType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
CoolingType::Onan => "ONAN",
CoolingType::Onaf => "ONAF",
CoolingType::Ofaf => "OFAF",
CoolingType::Odaf => "ODAF",
};
write!(f, "{s}")
}
}
#[derive(Debug, Clone)]
pub struct RatingFactor {
pub cooling: CoolingType,
pub factor: f64,
}
#[derive(Debug, Clone)]
pub struct TransformerModel {
pub id: usize,
pub name: String,
pub rated_mva: f64,
pub v_hv_kv: f64,
pub v_lv_kv: f64,
pub r_pu: f64,
pub x_pu: f64,
pub g_fe_pu: f64,
pub b_mag_pu: f64,
pub tap_ratio: f64,
pub phase_shift_deg: f64,
pub vector_group: VectorGroup,
pub cooling: CoolingType,
pub rating_factors: Vec<RatingFactor>,
}
impl TransformerModel {
#[allow(clippy::too_many_arguments)]
pub fn new(
id: usize,
name: impl Into<String>,
rated_mva: f64,
v_hv_kv: f64,
v_lv_kv: f64,
r_pu: f64,
x_pu: f64,
) -> Self {
Self {
id,
name: name.into(),
rated_mva,
v_hv_kv,
v_lv_kv,
r_pu,
x_pu,
g_fe_pu: 0.0,
b_mag_pu: 0.0,
tap_ratio: 1.0,
phase_shift_deg: 0.0,
vector_group: VectorGroup::Yny0,
cooling: CoolingType::Onan,
rating_factors: vec![RatingFactor {
cooling: CoolingType::Onan,
factor: 1.0,
}],
}
}
pub fn rated_current_ka(&self) -> f64 {
self.rated_mva / (3.0_f64.sqrt() * self.v_hv_kv)
}
pub fn rated_current_amps_hv(&self) -> f64 {
self.rated_current_ka() * 1000.0
}
pub fn rated_current_amps_lv(&self) -> f64 {
self.rated_mva / (3.0_f64.sqrt() * self.v_lv_kv) * 1000.0
}
pub fn max_mva(&self) -> f64 {
let max_factor = self
.rating_factors
.iter()
.map(|rf| rf.factor)
.fold(1.0_f64, f64::max);
self.rated_mva * max_factor
}
pub fn y_matrix_elements(&self) -> TransformerYElements {
let y_s = self.series_admittance();
let y_m = Complex::new(self.g_fe_pu, -self.b_mag_pu);
let phi = self.phase_shift_deg.to_radians();
let a = Complex::new(self.tap_ratio * phi.cos(), self.tap_ratio * phi.sin());
let a_sq = self.tap_ratio * self.tap_ratio;
let y_ii = (y_s + y_m) / a_sq;
let y_jj = y_s;
let y_ij = -y_s / a.conj();
let y_ji = -y_s / a;
TransformerYElements {
y_ii,
y_jj,
y_ij,
y_ji,
}
}
fn series_admittance(&self) -> Complex<f64> {
let z = Complex::new(self.r_pu, self.x_pu);
if z.norm_sqr() < f64::EPSILON {
Complex::new(1e10, 0.0)
} else {
z.inv()
}
}
}
#[derive(Debug, Clone)]
pub struct TransformerYElements {
pub y_ii: Complex<f64>,
pub y_jj: Complex<f64>,
pub y_ij: Complex<f64>,
pub y_ji: Complex<f64>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum OltcAction {
NoChange,
TapUp(f64),
TapDown(f64),
AtLimit,
}
#[derive(Debug, Clone)]
pub struct OltcController {
pub tap_min: f64,
pub tap_max: f64,
pub tap_step: f64,
pub v_setpoint_pu: f64,
pub deadband_pu: f64,
pub delay_s: f64,
pub current_tap: f64,
pub pending_time: f64,
pub total_tap_operations: usize,
}
impl OltcController {
pub fn new(tap_min: f64, tap_max: f64, tap_step: f64, v_setpoint_pu: f64) -> Self {
Self {
tap_min,
tap_max,
tap_step,
v_setpoint_pu,
deadband_pu: 0.01,
delay_s: 30.0,
current_tap: 1.0,
pending_time: 0.0,
total_tap_operations: 0,
}
}
pub fn update(&mut self, v_measured_pu: f64, dt_s: f64) -> OltcAction {
let error = v_measured_pu - self.v_setpoint_pu;
if error.abs() <= self.deadband_pu {
self.pending_time = 0.0;
return OltcAction::NoChange;
}
self.pending_time += dt_s;
if self.pending_time < self.delay_s {
return OltcAction::NoChange;
}
self.pending_time = 0.0;
if error < 0.0 {
let new_tap = self.current_tap + self.tap_step;
if new_tap > self.tap_max + f64::EPSILON {
OltcAction::AtLimit
} else {
self.current_tap = new_tap.min(self.tap_max);
self.total_tap_operations += 1;
OltcAction::TapUp(self.tap_step)
}
} else {
let new_tap = self.current_tap - self.tap_step;
if new_tap < self.tap_min - f64::EPSILON {
OltcAction::AtLimit
} else {
self.current_tap = new_tap.max(self.tap_min);
self.total_tap_operations += 1;
OltcAction::TapDown(self.tap_step)
}
}
}
pub fn steps_up_available(&self) -> usize {
if self.tap_step <= 0.0 {
return 0;
}
((self.tap_max - self.current_tap) / self.tap_step).round() as usize
}
pub fn steps_down_available(&self) -> usize {
if self.tap_step <= 0.0 {
return 0;
}
((self.current_tap - self.tap_min) / self.tap_step).round() as usize
}
}
#[derive(Debug, Clone)]
pub struct TransformerThermalModel {
pub theta_a: f64,
pub theta_or: f64,
pub theta_hr: f64,
pub tau_o: f64,
pub tau_w: f64,
pub n: f64,
pub m: f64,
pub theta_o: f64,
pub delta_theta_h: f64,
}
impl TransformerThermalModel {
pub fn new_onan(theta_ambient: f64) -> Self {
Self {
theta_a: theta_ambient,
theta_or: 55.0,
theta_hr: 23.0,
tau_o: 180.0,
tau_w: 10.0,
n: 0.8,
m: 0.8,
theta_o: theta_ambient, delta_theta_h: 0.0, }
}
pub fn new_ofaf(theta_ambient: f64) -> Self {
Self {
theta_a: theta_ambient,
theta_or: 45.0,
theta_hr: 20.0,
tau_o: 90.0,
tau_w: 7.0,
n: 1.0,
m: 1.0,
theta_o: theta_ambient,
delta_theta_h: 0.0,
}
}
pub fn step(&mut self, load_factor_k: f64, dt_min: f64) {
let k = load_factor_k.max(0.0);
let d_theta_o_ss = self.theta_or * k.powf(2.0 * self.n);
let d_theta_h_ss = self.theta_hr * k.powf(2.0 * self.m);
let d_theta_o_dot = (d_theta_o_ss + self.theta_a - self.theta_o) / self.tau_o;
let d_theta_h_dot = (d_theta_h_ss - self.delta_theta_h) / self.tau_w;
self.theta_o += d_theta_o_dot * dt_min;
self.delta_theta_h += d_theta_h_dot * dt_min;
self.delta_theta_h = self.delta_theta_h.max(0.0);
}
pub fn hotspot_temperature(&self) -> f64 {
self.theta_o + self.delta_theta_h
}
pub fn top_oil_temperature(&self) -> f64 {
self.theta_o
}
pub fn aging_acceleration_factor(&self) -> f64 {
let theta_h = self.hotspot_temperature();
((15000.0 / 371.0) - (15000.0 / (theta_h + 273.0))).exp()
}
pub fn loss_of_life_pct_per_hour(&self) -> f64 {
const NORMAL_INSULATION_LIFE_H: f64 = 8.76e5; self.aging_acceleration_factor() / NORMAL_INSULATION_LIFE_H * 100.0
}
pub fn time_to_limit(&self, max_temp_c: f64) -> f64 {
let current_hs = self.hotspot_temperature();
if current_hs >= max_temp_c {
return 0.0;
}
let dt_probe = 0.1_f64; let mut probe = self.clone();
probe.step(1.0, dt_probe);
let rate = (probe.hotspot_temperature() - current_hs) / dt_probe; if rate <= 0.0 {
f64::INFINITY
} else {
(max_temp_c - current_hs) / rate
}
}
pub fn rated_hotspot_temperature(&self) -> f64 {
self.theta_a + self.theta_or + self.theta_hr
}
}
#[derive(Debug, Clone)]
pub struct DgaModel {
pub h2_ppm: f64,
pub ch4_ppm: f64,
pub c2h6_ppm: f64,
pub c2h4_ppm: f64,
pub c2h2_ppm: f64,
pub co_ppm: f64,
pub co2_ppm: f64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DgaFaultType {
Normal,
PaperThermal,
OilThermal300to700C,
OilThermalAbove700C,
PartialDischarge,
Arcing,
Unknown,
}
impl fmt::Display for DgaFaultType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
DgaFaultType::Normal => "Normal",
DgaFaultType::PaperThermal => "Thermal (paper <300°C)",
DgaFaultType::OilThermal300to700C => "Thermal (oil 300-700°C)",
DgaFaultType::OilThermalAbove700C => "Thermal (oil >700°C)",
DgaFaultType::PartialDischarge => "Partial Discharge",
DgaFaultType::Arcing => "Arcing",
DgaFaultType::Unknown => "Unknown",
};
write!(f, "{s}")
}
}
impl DgaModel {
pub fn new_baseline() -> Self {
Self {
h2_ppm: 50.0,
ch4_ppm: 2.0,
c2h6_ppm: 1.0,
c2h4_ppm: 0.5,
c2h2_ppm: 0.0,
co_ppm: 100.0,
co2_ppm: 500.0,
}
}
pub fn rogers_ratio_code(&self) -> DgaFaultType {
let r1 = if self.h2_ppm > 0.0 {
self.ch4_ppm / self.h2_ppm
} else {
f64::INFINITY
};
let r2 = if self.c2h6_ppm > 0.0 {
self.c2h4_ppm / self.c2h6_ppm
} else {
f64::INFINITY
};
let r3 = if self.c2h4_ppm > 0.0 {
self.c2h2_ppm / self.c2h4_ppm
} else {
f64::INFINITY
};
let c1 = encode_r1(r1);
let c2 = encode_r2(r2);
let c3 = encode_r3(r3);
match (c1, c2, c3) {
(0, 0, 0) => DgaFaultType::Normal,
(0, 0, 1) | (0, 0, 2) => DgaFaultType::PartialDischarge,
(1, 0, 0) => DgaFaultType::PaperThermal,
(2, 0, 0) => DgaFaultType::OilThermal300to700C,
(2, 2, 0) => DgaFaultType::OilThermalAbove700C,
(0, 1, 0) | (1, 1, 0) => DgaFaultType::OilThermal300to700C,
(0, 2, 0) | (2, 1, 0) => DgaFaultType::OilThermalAbove700C,
(0, 1, 1) | (0, 2, 2) => DgaFaultType::Arcing,
(0, 1, 2) | (1, 1, 2) | (2, 1, 2) => DgaFaultType::Arcing,
_ => DgaFaultType::Unknown,
}
}
pub fn total_combustible_gas(&self) -> f64 {
self.h2_ppm + self.ch4_ppm + self.c2h6_ppm + self.c2h4_ppm + self.c2h2_ppm + self.co_ppm
}
pub fn health_index(&self) -> f64 {
const TCG_REFERENCE: f64 = 2000.0; const C2H2_LIMIT: f64 = 35.0;
let tcg_score = 1.0 - (self.total_combustible_gas() / TCG_REFERENCE).min(1.0);
let arc_score = 1.0 - (self.c2h2_ppm / C2H2_LIMIT).min(1.0);
let raw = 0.6 * tcg_score + 0.4 * arc_score;
(raw * 100.0).clamp(0.0, 100.0)
}
}
fn encode_r1(r: f64) -> u8 {
if r < 0.1 {
0
} else if r < 1.0 {
1
} else {
2
}
}
fn encode_r2(r: f64) -> u8 {
if r < 1.0 {
0
} else if r < 3.0 {
1
} else {
2
}
}
fn encode_r3(r: f64) -> u8 {
if r < 0.1 {
0
} else if r < 3.0 {
1
} else {
2
}
}
#[derive(Debug, Clone)]
pub struct ThreeWindingTransformer {
pub id: usize,
pub name: String,
pub rated_mva_hv: f64,
pub rated_mva_mv: f64,
pub rated_mva_lv: f64,
pub v_hv_kv: f64,
pub v_mv_kv: f64,
pub v_lv_kv: f64,
pub r_hv_pu: f64,
pub x_hv_pu: f64,
pub r_mv_pu: f64,
pub x_mv_pu: f64,
pub r_lv_pu: f64,
pub x_lv_pu: f64,
pub tap_hv: f64,
pub tap_mv: f64,
pub tap_lv: f64,
}
impl ThreeWindingTransformer {
#[allow(clippy::too_many_arguments)]
pub fn new(
id: usize,
name: impl Into<String>,
rated_mva_hv: f64,
rated_mva_mv: f64,
rated_mva_lv: f64,
v_hv_kv: f64,
v_mv_kv: f64,
v_lv_kv: f64,
r_hv_pu: f64,
x_hv_pu: f64,
r_mv_pu: f64,
x_mv_pu: f64,
r_lv_pu: f64,
x_lv_pu: f64,
) -> Self {
Self {
id,
name: name.into(),
rated_mva_hv,
rated_mva_mv,
rated_mva_lv,
v_hv_kv,
v_mv_kv,
v_lv_kv,
r_hv_pu,
x_hv_pu,
r_mv_pu,
x_mv_pu,
r_lv_pu,
x_lv_pu,
tap_hv: 1.0,
tap_mv: 1.0,
tap_lv: 1.0,
}
}
pub fn to_star_equivalent(&self) -> [TransformerModel; 3] {
let mut hv = TransformerModel::new(
self.id,
format!("{}_HV", self.name),
self.rated_mva_hv,
self.v_hv_kv,
self.v_hv_kv, self.r_hv_pu,
self.x_hv_pu,
);
hv.tap_ratio = self.tap_hv;
let mva_ratio_mv = self.rated_mva_hv / self.rated_mva_mv.max(f64::EPSILON);
let mut mv = TransformerModel::new(
self.id,
format!("{}_MV", self.name),
self.rated_mva_mv,
self.v_hv_kv, self.v_mv_kv,
self.r_mv_pu / mva_ratio_mv,
self.x_mv_pu / mva_ratio_mv,
);
mv.tap_ratio = self.tap_mv;
let mva_ratio_lv = self.rated_mva_hv / self.rated_mva_lv.max(f64::EPSILON);
let mut lv = TransformerModel::new(
self.id,
format!("{}_LV", self.name),
self.rated_mva_lv,
self.v_hv_kv, self.v_lv_kv,
self.r_lv_pu / mva_ratio_lv,
self.x_lv_pu / mva_ratio_lv,
);
lv.tap_ratio = self.tap_lv;
[hv, mv, lv]
}
pub fn rated_current_hv_ka(&self) -> f64 {
self.rated_mva_hv / (3.0_f64.sqrt() * self.v_hv_kv)
}
pub fn rated_current_mv_ka(&self) -> f64 {
self.rated_mva_mv / (3.0_f64.sqrt() * self.v_mv_kv)
}
pub fn rated_current_lv_ka(&self) -> f64 {
self.rated_mva_lv / (3.0_f64.sqrt() * self.v_lv_kv)
}
}
#[cfg(test)]
mod tests {
use super::*;
const TOL: f64 = 1e-9;
#[test]
fn test_y_matrix_ideal_no_tap() {
let mut t = TransformerModel::new(0, "T1", 100.0, 110.0, 11.0, 0.005, 0.1);
t.tap_ratio = 1.0;
t.phase_shift_deg = 0.0;
let y = t.y_matrix_elements();
let y_s = Complex::new(0.005, 0.1).inv();
assert!((y.y_ii.re - y_s.re).abs() < TOL);
assert!((y.y_ii.im - y_s.im).abs() < TOL);
assert!((y.y_jj.re - y_s.re).abs() < TOL);
assert!((y.y_ij.re + y_s.re).abs() < TOL);
assert!((y.y_ij.im + y_s.im).abs() < TOL);
}
#[test]
fn test_y_matrix_off_nominal_tap() {
let mut t = TransformerModel::new(1, "T2", 100.0, 110.0, 11.0, 0.01, 0.12);
t.tap_ratio = 1.05;
t.phase_shift_deg = 0.0;
let y = t.y_matrix_elements();
let y_s = Complex::new(0.01_f64, 0.12_f64).inv();
let expected_y_ij = -y_s / 1.05_f64;
assert!((y.y_ij.re - expected_y_ij.re).abs() < TOL);
assert!((y.y_ij.im - expected_y_ij.im).abs() < TOL);
}
#[test]
fn test_y_matrix_yii_with_shunt_and_tap() {
let mut t = TransformerModel::new(2, "T3", 100.0, 110.0, 11.0, 0.01, 0.1);
t.tap_ratio = 1.1;
t.g_fe_pu = 0.002;
t.b_mag_pu = 0.04;
t.phase_shift_deg = 0.0;
let y = t.y_matrix_elements();
let y_s = Complex::new(0.01_f64, 0.1_f64).inv();
let y_m = Complex::new(0.002, -0.04);
let expected = (y_s + y_m) / (1.1 * 1.1);
assert!((y.y_ii.re - expected.re).abs() < 1e-8);
assert!((y.y_ii.im - expected.im).abs() < 1e-8);
}
#[test]
fn test_rated_current_hv() {
let t = TransformerModel::new(3, "T4", 100.0, 110.0, 11.0, 0.0, 0.1);
let expected_ka = 100.0 / (3.0_f64.sqrt() * 110.0);
assert!((t.rated_current_ka() - expected_ka).abs() < TOL);
assert!((t.rated_current_amps_hv() - expected_ka * 1000.0).abs() < TOL);
}
#[test]
fn test_rated_current_lv() {
let t = TransformerModel::new(4, "T5", 100.0, 110.0, 11.0, 0.0, 0.1);
let expected_a = 100.0 / (3.0_f64.sqrt() * 11.0) * 1000.0;
assert!((t.rated_current_amps_lv() - expected_a).abs() < 1e-6);
}
#[test]
fn test_oltc_no_action_within_deadband() {
let mut oltc = OltcController::new(0.85, 1.15, 0.00625, 1.0);
let action = oltc.update(1.005, 60.0); assert_eq!(action, OltcAction::NoChange);
assert_eq!(oltc.total_tap_operations, 0);
}
#[test]
fn test_oltc_tap_up_after_delay() {
let mut oltc = OltcController::new(0.85, 1.15, 0.00625, 1.0);
oltc.delay_s = 30.0;
let a1 = oltc.update(0.97, 20.0);
assert_eq!(a1, OltcAction::NoChange);
let a2 = oltc.update(0.97, 15.0); assert!(matches!(a2, OltcAction::TapUp(_)));
assert_eq!(oltc.total_tap_operations, 1);
assert!((oltc.current_tap - 1.00625).abs() < 1e-9);
}
#[test]
fn test_oltc_tap_down_high_voltage() {
let mut oltc = OltcController::new(0.85, 1.15, 0.00625, 1.0);
oltc.delay_s = 5.0;
let action = oltc.update(1.05, 10.0); assert!(matches!(action, OltcAction::TapDown(_)));
assert_eq!(oltc.total_tap_operations, 1);
}
#[test]
fn test_oltc_at_limit_max() {
let mut oltc = OltcController::new(0.85, 1.15, 0.00625, 1.0);
oltc.current_tap = 1.15; oltc.delay_s = 1.0;
let action = oltc.update(0.90, 5.0); assert_eq!(action, OltcAction::AtLimit);
assert_eq!(oltc.total_tap_operations, 0);
}
#[test]
fn test_oltc_at_limit_min() {
let mut oltc = OltcController::new(0.85, 1.15, 0.00625, 1.0);
oltc.current_tap = 0.85; oltc.delay_s = 1.0;
let action = oltc.update(1.10, 5.0); assert_eq!(action, OltcAction::AtLimit);
}
#[test]
fn test_thermal_step_increases_top_oil_under_overload() {
let mut model = TransformerThermalModel::new_onan(25.0);
let t0 = model.top_oil_temperature();
model.step(1.2, 5.0); assert!(model.top_oil_temperature() > t0);
}
#[test]
fn test_thermal_aging_factor_above_reference() {
let mut model = TransformerThermalModel::new_onan(25.0);
model.theta_o = 80.0;
model.delta_theta_h = 30.0; assert!(model.aging_acceleration_factor() > 1.0);
}
#[test]
fn test_thermal_steady_state_hotspot() {
let theta_a = 20.0;
let mut model = TransformerThermalModel::new_onan(theta_a);
for _ in 0..5000 {
model.step(1.0, 1.0); }
let expected = theta_a + model.theta_or + model.theta_hr; let actual = model.hotspot_temperature();
assert!(
(actual - expected).abs() < 0.5,
"hotspot {actual} vs expected {expected}"
);
}
#[test]
fn test_thermal_aging_factor_unity_at_reference() {
let mut model = TransformerThermalModel::new_onan(20.0);
model.theta_o = 75.0;
model.delta_theta_h = 23.0;
let faa = model.aging_acceleration_factor();
assert!((faa - 1.0).abs() < 1e-6, "FAA at 98°C = {faa}");
}
#[test]
fn test_dga_normal_gases() {
let dga = DgaModel::new_baseline();
assert_eq!(dga.rogers_ratio_code(), DgaFaultType::Normal);
}
#[test]
fn test_dga_high_acetylene_arcing() {
let mut dga = DgaModel::new_baseline();
dga.c2h2_ppm = 100.0; dga.c2h4_ppm = 0.5;
dga.c2h6_ppm = 0.5; dga.h2_ppm = 50.0; dga.ch4_ppm = 2.0;
assert_eq!(dga.rogers_ratio_code(), DgaFaultType::Arcing);
}
#[test]
fn test_dga_health_index_near_perfect() {
let dga = DgaModel {
h2_ppm: 0.0,
ch4_ppm: 0.0,
c2h6_ppm: 0.0,
c2h4_ppm: 0.0,
c2h2_ppm: 0.0,
co_ppm: 0.0,
co2_ppm: 0.0,
};
assert!((dga.health_index() - 100.0).abs() < TOL);
}
#[test]
fn test_dga_health_index_degraded() {
let dga = DgaModel {
h2_ppm: 1000.0,
ch4_ppm: 500.0,
c2h6_ppm: 100.0,
c2h4_ppm: 100.0,
c2h2_ppm: 200.0, co_ppm: 500.0,
co2_ppm: 2000.0,
};
assert!(dga.health_index() < 5.0);
}
#[test]
fn test_three_winding_star_equivalent_count() {
let tw = ThreeWindingTransformer::new(
0, "3W", 100.0, 50.0, 30.0, 220.0, 110.0, 10.0, 0.005, 0.08, 0.005, 0.08, 0.005, 0.08,
);
let branches = tw.to_star_equivalent();
assert_eq!(branches.len(), 3);
}
#[test]
fn test_three_winding_rated_current_hv() {
let tw = ThreeWindingTransformer::new(
1, "3W2", 300.0, 150.0, 100.0, 400.0, 220.0, 33.0, 0.0, 0.1, 0.0, 0.1, 0.0, 0.1,
);
let expected = 300.0 / (3.0_f64.sqrt() * 400.0);
assert!((tw.rated_current_hv_ka() - expected).abs() < TOL);
}
#[test]
fn test_three_winding_star_branch_voltages() {
let tw = ThreeWindingTransformer::new(
2, "3W3", 100.0, 60.0, 40.0, 110.0, 33.0, 11.0, 0.01, 0.1, 0.01, 0.1, 0.01, 0.1,
);
let [hv_br, mv_br, lv_br] = tw.to_star_equivalent();
assert!((hv_br.v_hv_kv - 110.0).abs() < TOL);
assert!((mv_br.v_lv_kv - 33.0).abs() < TOL);
assert!((lv_br.v_lv_kv - 11.0).abs() < TOL);
}
#[test]
fn test_vector_group_phase_shift() {
assert!((VectorGroup::Dyn11.phase_shift_deg() - 30.0).abs() < TOL);
assert!((VectorGroup::Yny0.phase_shift_deg() - 0.0).abs() < TOL);
}
#[test]
fn test_y_matrix_phase_shift_asymmetry() {
let mut t = TransformerModel::new(5, "PST", 100.0, 110.0, 110.0, 0.0, 0.1);
t.tap_ratio = 1.0;
t.phase_shift_deg = 30.0;
let y = t.y_matrix_elements();
assert!((y.y_ij - y.y_ji).norm() > 1e-6);
}
}