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 BranchSusceptanceFormula {
ReactanceOnly,
TapAdjustedReactance,
#[default]
SeriesSusceptance,
}
impl BranchSusceptanceFormula {
#[must_use]
pub fn calc_branch_susceptance(
self,
resistance: f64,
reactance: f64,
effective_tap: f64,
) -> f64 {
let negated_reciprocal = |denominator: f64| {
if denominator.is_finite() {
-1.0 / denominator
} else {
f64::NAN
}
};
match self {
Self::ReactanceOnly => negated_reciprocal(reactance),
Self::TapAdjustedReactance => negated_reciprocal(reactance * effective_tap),
Self::SeriesSusceptance => series_admittance_parts(resistance, reactance).1,
}
}
#[must_use]
pub fn calc_solver_edge_weight(
self,
resistance: f64,
reactance: f64,
effective_tap: f64,
) -> f64 {
-self.calc_branch_susceptance(resistance, reactance, effective_tap)
}
#[must_use]
pub fn reads_tap(self) -> bool {
matches!(self, Self::TapAdjustedReactance)
}
#[must_use]
pub fn includes_phase_shifts(self) -> bool {
match self {
Self::ReactanceOnly => false,
Self::TapAdjustedReactance | Self::SeriesSusceptance => true,
}
}
#[must_use]
pub fn formula_name(self) -> &'static str {
match self {
Self::SeriesSusceptance => "series_susceptance",
Self::TapAdjustedReactance => "tap_adjusted_reactance",
Self::ReactanceOnly => "reactance_only",
}
}
#[must_use]
pub fn from_formula_name(name: &str) -> Option<Self> {
match name {
"series_susceptance" => Some(Self::SeriesSusceptance),
"tap_adjusted_reactance" => Some(Self::TapAdjustedReactance),
"reactance_only" => Some(Self::ReactanceOnly),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn formula_names_round_trip() {
for convention in [
BranchSusceptanceFormula::SeriesSusceptance,
BranchSusceptanceFormula::TapAdjustedReactance,
BranchSusceptanceFormula::ReactanceOnly,
] {
assert_eq!(
BranchSusceptanceFormula::from_formula_name(convention.formula_name()),
Some(convention)
);
}
assert_eq!(BranchSusceptanceFormula::from_formula_name("mystery"), None);
assert_eq!(BranchSusceptanceFormula::from_formula_name("series"), None);
assert_eq!(
BranchSusceptanceFormula::from_formula_name("matpower"),
None
);
}
#[test]
fn series_susceptance_reduces_to_negated_one_over_x() {
let b = BranchSusceptanceFormula::SeriesSusceptance.calc_branch_susceptance(0.0, 0.25, 1.0);
assert!((b + 4.0).abs() < 1e-12);
let weight =
BranchSusceptanceFormula::SeriesSusceptance.calc_solver_edge_weight(0.0, 0.25, 1.0);
assert!((weight - 4.0).abs() < 1e-12);
}
#[test]
fn resistance_lowers_the_susceptance_magnitude() {
let lossless =
BranchSusceptanceFormula::SeriesSusceptance.calc_branch_susceptance(0.0, 0.1, 1.0);
let lossy =
BranchSusceptanceFormula::SeriesSusceptance.calc_branch_susceptance(0.1, 0.1, 1.0);
assert!(lossy.abs() < lossless.abs());
assert!((lossy + 5.0).abs() < 1e-12);
}
#[test]
fn matpower_scales_by_the_tap() {
let b =
BranchSusceptanceFormula::TapAdjustedReactance.calc_branch_susceptance(0.01, 0.2, 2.0);
assert!((b + 2.5).abs() < 1e-12);
}
#[test]
fn an_unread_tap_never_rejects_a_branch() {
for conv in [
BranchSusceptanceFormula::ReactanceOnly,
BranchSusceptanceFormula::SeriesSusceptance,
] {
assert!(!conv.reads_tap());
let b = conv.calc_branch_susceptance(0.01, 0.1, 1e-200);
assert!(b.is_finite(), "{conv:?} read the tap it never divides by");
}
assert!(BranchSusceptanceFormula::TapAdjustedReactance.reads_tap());
}
#[test]
fn a_non_finite_denominator_is_not_a_susceptance() {
for x in [f64::INFINITY, f64::NEG_INFINITY, f64::NAN] {
for conv in [
BranchSusceptanceFormula::ReactanceOnly,
BranchSusceptanceFormula::TapAdjustedReactance,
BranchSusceptanceFormula::SeriesSusceptance,
] {
let b = conv.calc_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 =
BranchSusceptanceFormula::TapAdjustedReactance.calc_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 = BranchSusceptanceFormula::SeriesSusceptance.calc_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 public rule is the series susceptance itself"
);
}
#[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));
}
}
}