use serde::{Deserialize, Serialize};
pub const MIN_DIVISIBLE_MAGNITUDE: f64 = 1.491_668_146_240_041_3e-154;
pub(crate) fn series_admittance_parts(r: f64, x: f64) -> (f64, f64) {
let denom = r * r + x * x;
if denom.is_finite() {
return (r / denom, -x / denom);
}
let scale = r.abs().max(x.abs());
let (r, x) = (r / scale, x / scale);
let denom = (r * r + x * x) * scale;
(r / denom, -x / denom)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub enum DcConvention {
ReactanceOnly,
Matpower,
#[default]
SeriesImpedance,
}
impl DcConvention {
#[allow(non_upper_case_globals)]
#[deprecated(
since = "0.9.0",
note = "renamed to DcConvention::ReactanceOnly in 0.9.0; the alias goes away at 1.0.0"
)]
pub const PaperPure: DcConvention = DcConvention::ReactanceOnly;
#[must_use]
pub fn branch_susceptance(self, resistance: f64, reactance: f64, effective_tap: f64) -> f64 {
let reciprocal = |denominator: f64| {
if denominator.is_finite() {
1.0 / denominator
} else {
f64::NAN
}
};
match self {
Self::ReactanceOnly => reciprocal(reactance),
Self::Matpower => reciprocal(reactance * effective_tap),
Self::SeriesImpedance => -series_admittance_parts(resistance, reactance).1,
}
}
#[must_use]
pub fn includes_phase_shifts(self) -> bool {
match self {
Self::ReactanceOnly => false,
Self::Matpower | Self::SeriesImpedance => true,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn series_impedance_reduces_to_one_over_x() {
let b = DcConvention::SeriesImpedance.branch_susceptance(0.0, 0.25, 1.0);
assert!((b - 4.0).abs() < 1e-12);
}
#[test]
fn resistance_lowers_the_susceptance() {
let lossless = DcConvention::SeriesImpedance.branch_susceptance(0.0, 0.1, 1.0);
let lossy = DcConvention::SeriesImpedance.branch_susceptance(0.1, 0.1, 1.0);
assert!(lossy < lossless);
assert!((lossy - 5.0).abs() < 1e-12);
}
#[test]
fn matpower_scales_by_the_tap() {
let b = DcConvention::Matpower.branch_susceptance(0.01, 0.2, 2.0);
assert!((b - 2.5).abs() < 1e-12);
}
#[test]
fn a_non_finite_denominator_is_not_a_susceptance() {
for x in [f64::INFINITY, f64::NEG_INFINITY, f64::NAN] {
for conv in [
DcConvention::ReactanceOnly,
DcConvention::Matpower,
DcConvention::SeriesImpedance,
] {
let b = conv.branch_susceptance(0.01, x, 1.0);
assert!(!b.is_finite(), "{conv:?} read x = {x} as b = {b}");
}
}
for (x, tap) in [
(0.1, f64::INFINITY),
(0.1, f64::NAN),
(1e300, 1e300),
(1e300, -1e300),
] {
let b = DcConvention::Matpower.branch_susceptance(0.0, x, tap);
assert!(!b.is_finite(), "x = {x}, tap = {tap} read as b = {b}");
}
}
#[test]
fn an_impedance_whose_square_overflows_still_has_a_susceptance() {
let (r, x) = (1e160, 1e160);
assert!(r * r + x * x == f64::INFINITY, "the direct form overflows");
let b = DcConvention::SeriesImpedance.branch_susceptance(r, x, 1.0);
assert!(
(b / 5e-161 - 1.0).abs() < 1e-12,
"the branch is not dropped, got {b}"
);
let (g, susceptance) = series_admittance_parts(r, x);
assert!((g / 5e-161 - 1.0).abs() < 1e-12, "got {g}");
assert!(
(susceptance + b).abs() < 1e-175,
"the DC rule is its negation"
);
}
#[test]
fn the_ordinary_range_is_bit_identical_to_the_direct_quotient() {
for (r, x) in [
(0.01, 0.1),
(0.03, 0.04),
(0.0, 0.25),
(1e-6, 1e-5),
(7.0, 3.0),
] {
let denom = r * r + x * x;
assert_eq!(series_admittance_parts(r, x), (r / denom, -x / denom));
}
}
}