Skip to main content

powerio/
dc.rs

1//! Conventions shared by DC network models and matrix builders.
2
3use serde::{Deserialize, Serialize};
4
5/// The magnitude below which a reactance, an impedance, or a tap ratio stops
6/// being a number a builder can divide by.
7///
8/// It is `f64::MIN_POSITIVE.sqrt()`: the square of anything smaller underflows
9/// to zero, and the reciprocal is above 1e153, which annihilates every real
10/// branch sharing its diagonal. Each builder compares a magnitude against it —
11/// `|x|`, `hypot(r, x)`, the tap — never `r² + x²`, which is a square. Per unit
12/// reactances run from 1e-6 to 10, so it rejects poison and nothing else.
13pub const MIN_DIVISIBLE_MAGNITUDE: f64 = 1.491_668_146_240_041_3e-154;
14
15/// Rule for the DC branch susceptance `b`.
16///
17/// `b` is positive for an inductive branch, the DC model convention MATPOWER
18/// `makeBdc` uses. It is the edge weight of the bus susceptance matrix and the
19/// coefficient in `f = b (theta_from - theta_to)`. The AC series susceptance
20/// `Im(1/(r + jx))` is its negation.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
22#[non_exhaustive]
23pub enum DcConvention {
24    /// `b = 1/x`, ignoring resistance, transformer taps, and phase shifts.
25    ///
26    /// The textbook DC linearization, which a paper reproducing a published
27    /// result needs exactly as written.
28    ReactanceOnly,
29    /// `b = 1/(x tau)` with phase shift injections, matching MATPOWER
30    /// `makeBdc`.
31    Matpower,
32    /// `b = x/(r² + x²)` with phase shift injections.
33    ///
34    /// Reads the whole series impedance, so it describes a branch with a real
35    /// r/x ratio. A transformer tap does not
36    /// scale it, and it reduces to `1/x` when the resistance is zero.
37    /// PowerModels' DC formulation uses the same quantity.
38    #[default]
39    SeriesImpedance,
40}
41
42impl DcConvention {
43    /// The branch susceptance from resistance, reactance, and effective tap.
44    /// Only [`Self::Matpower`] reads the tap, and only
45    /// [`Self::SeriesImpedance`] reads the resistance.
46    #[must_use]
47    pub fn branch_susceptance(self, resistance: f64, reactance: f64, effective_tap: f64) -> f64 {
48        match self {
49            Self::ReactanceOnly => 1.0 / reactance,
50            Self::Matpower => 1.0 / (reactance * effective_tap),
51            Self::SeriesImpedance => reactance / (resistance * resistance + reactance * reactance),
52        }
53    }
54
55    /// Whether phase shifts contribute to the nodal injection vector.
56    #[must_use]
57    pub fn includes_phase_shifts(self) -> bool {
58        match self {
59            Self::ReactanceOnly => false,
60            Self::Matpower | Self::SeriesImpedance => true,
61        }
62    }
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68
69    /// A resistanceless branch reads the same under both live conventions, so
70    /// the new default only moves a case that carries resistance.
71    #[test]
72    fn series_impedance_reduces_to_one_over_x() {
73        let b = DcConvention::SeriesImpedance.branch_susceptance(0.0, 0.25, 1.0);
74        assert!((b - 4.0).abs() < 1e-12);
75    }
76
77    /// Resistance lowers the susceptance, by more as `r` grows against `x`.
78    #[test]
79    fn resistance_lowers_the_susceptance() {
80        let lossless = DcConvention::SeriesImpedance.branch_susceptance(0.0, 0.1, 1.0);
81        let lossy = DcConvention::SeriesImpedance.branch_susceptance(0.1, 0.1, 1.0);
82        assert!(lossy < lossless);
83        assert!((lossy - 5.0).abs() < 1e-12);
84    }
85
86    #[test]
87    fn matpower_scales_by_the_tap() {
88        let b = DcConvention::Matpower.branch_susceptance(0.01, 0.2, 2.0);
89        assert!((b - 2.5).abs() < 1e-12);
90    }
91}