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}