1mod adjacency;
11mod bdoubleprime;
12mod bprime;
13mod incidence;
14mod lacpf;
15pub mod laplacian;
16pub mod multiconductor;
17pub mod sensitivity;
18pub mod triplet;
19mod ybus;
20
21#[cfg(test)]
22mod tests;
23
24pub use adjacency::calc_adjacency_matrix;
25pub use bdoubleprime::calc_bdoubleprime_matrix;
26pub use bprime::calc_bprime_matrix;
27pub(crate) use incidence::{IncidenceParts, build_incidence, calc_solver_branch_flow_matrix};
28pub use incidence::{calc_diagonal, calc_susceptance_diagonal};
29pub use lacpf::calc_lacpf_matrix;
30pub use laplacian::{
31 GroundedIndexMap, calc_reference_indicator, calc_unit_vector, calc_weighted_laplacian,
32 ground_at, ground_at_each,
33};
34pub use powerio_tx::BranchSusceptanceFormula;
35pub use sensitivity::{
36 SensitivityMatrices, SensitivityMatrixMetadata, SensitivityMetadata, SensitivityOptions,
37 SensitivitySolver, SensitivitySolverPath, calc_lodf, calc_ptdf, calc_ptdf_lodf,
38 calc_ptdf_lodf_with_options,
39};
40pub use ybus::{YbusParts, calc_admittance_matrix};
41#[cfg(feature = "gridfm")]
44pub(crate) use ybus::{YbusFlags, branch_admittance, branch_flows};
45
46use sprs::CsMat;
47
48pub(crate) use powerio_tx::dc::MIN_DIVISIBLE_MAGNITUDE;
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
57#[non_exhaustive]
58pub enum Scheme {
59 #[default]
60 Bx,
61 Xb,
62}
63
64#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
65pub struct BuildOptions {
66 pub scheme: Scheme,
67 pub include_taps: bool,
70 pub include_shifts: bool,
73 pub skip_zero_impedance: bool,
75}
76
77impl Default for BuildOptions {
78 fn default() -> Self {
79 Self {
80 scheme: Scheme::Bx,
81 include_taps: true,
82 include_shifts: true,
83 skip_zero_impedance: false,
89 }
90 }
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
96#[non_exhaustive]
97pub enum ZeroImpedanceRule {
98 Series,
101 Reactance,
104}
105
106#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
109#[non_exhaustive]
110pub struct ZeroImpedanceSkips {
111 pub count: usize,
112 pub branch_indices: Vec<usize>,
113}
114
115impl ZeroImpedanceSkips {
116 pub fn new(branch_indices: Vec<usize>) -> Self {
117 Self {
118 count: branch_indices.len(),
119 branch_indices,
120 }
121 }
122
123 pub fn is_empty(&self) -> bool {
124 self.count == 0
125 }
126}
127
128pub fn calc_zero_impedance_skips(
131 case: &crate::indexed::IndexedNetwork,
132 rule: ZeroImpedanceRule,
133) -> ZeroImpedanceSkips {
134 let branch_indices = case
135 .in_service_branches()
136 .filter_map(|(row, br)| {
137 let zero = match rule {
138 ZeroImpedanceRule::Series => br.r * br.r + br.x * br.x == 0.0,
139 ZeroImpedanceRule::Reactance => br.x == 0.0,
140 };
141 zero.then_some(row)
142 })
143 .collect();
144 ZeroImpedanceSkips::new(branch_indices)
145}
146
147#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
149#[non_exhaustive]
150pub struct MatrixStats {
151 pub n: usize,
152 pub nnz: usize,
153 pub min_diag: f64,
154 pub max_diag: f64,
155 pub min_dd_margin: f64,
158 pub m_matrix_sign: bool,
160 pub frobenius_norm: f64,
161 #[serde(default)]
163 pub skipped_zero_impedance: usize,
164 #[serde(default)]
166 pub skipped_zero_impedance_branches: Vec<usize>,
167}
168
169impl MatrixStats {
170 pub fn from_csr(a: &CsMat<f64>) -> Self {
171 let n = a.rows();
172 let mut min_diag = f64::INFINITY;
173 let mut max_diag = f64::NEG_INFINITY;
174 let mut min_dd = f64::INFINITY;
175 let mut m_sign = true;
176 let mut fro_sq = 0.0_f64;
177
178 for (row_idx, row) in a.outer_iterator().enumerate() {
179 let mut diag = 0.0_f64;
180 let mut off_abs = 0.0_f64;
181 for (col, &v) in row.iter() {
182 fro_sq += v * v;
183 if col == row_idx {
184 diag = v;
185 } else {
186 off_abs += v.abs();
187 if v > 0.0 {
188 m_sign = false;
189 }
190 }
191 }
192 min_diag = min_diag.min(diag);
193 max_diag = max_diag.max(diag);
194 min_dd = min_dd.min(diag - off_abs);
195 }
196
197 Self {
198 n,
199 nnz: a.nnz(),
200 min_diag,
201 max_diag,
202 min_dd_margin: min_dd,
203 m_matrix_sign: m_sign,
204 frobenius_norm: fro_sq.sqrt(),
205 skipped_zero_impedance: 0,
206 skipped_zero_impedance_branches: Vec::new(),
207 }
208 }
209
210 #[must_use]
211 pub fn with_zero_impedance_skips(mut self, skips: ZeroImpedanceSkips) -> Self {
212 self.skipped_zero_impedance = skips.count;
213 self.skipped_zero_impedance_branches = skips.branch_indices;
214 self
215 }
216}
217
218pub(crate) fn negate_into(mut a: CsMat<f64>) -> CsMat<f64> {
222 a.data_mut().iter_mut().for_each(|v| *v = -*v);
223 a
224}
225
226pub fn check_sddm(a: &CsMat<f64>) -> bool {
229 if !is_symmetric(a) {
230 return false;
231 }
232 let stats = MatrixStats::from_csr(a);
233 stats.m_matrix_sign && stats.min_dd_margin >= -1e-12 && stats.min_diag > 0.0
234}
235
236fn is_symmetric(a: &CsMat<f64>) -> bool {
237 if a.rows() != a.cols() {
238 return false;
239 }
240 for (&v, (i, j)) in a {
241 let other = a.get(j, i).copied().unwrap_or(0.0);
242 let scale = v.abs().max(other.abs()).max(1.0);
243 if (v - other).abs() > 1e-12 * scale {
244 return false;
245 }
246 }
247 true
248}