1use std::path::{Path, PathBuf};
9
10use rand::SeedableRng;
11use rand::distr::{Distribution, StandardUniform};
12use rand_chacha::ChaCha8Rng;
13use serde::{Deserialize, Serialize};
14
15use crate::Result;
16use crate::indexed::IndexedNetwork;
17use crate::io::meta::{CaseMetadata, MatrixMetadata, write_meta_json};
18use crate::io::mtx::{write_mtx, write_vector_mtx};
19use crate::matrix::{
20 BuildOptions, MatrixStats, build_adjacency, build_bdoubleprime, build_bprime, build_lacpf,
21 build_ybus, negate_into, sddm_check,
22};
23use crate::network::Network;
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
26#[non_exhaustive]
27pub enum MatrixKind {
28 BPrime,
30 BDoublePrime,
32 YbusG,
34 YbusB,
36 Lacpf,
38 Adjacency,
40}
41
42impl MatrixKind {
43 pub const ALL: &'static [MatrixKind] = &[
44 Self::BPrime,
45 Self::BDoublePrime,
46 Self::YbusG,
47 Self::YbusB,
48 Self::Lacpf,
49 Self::Adjacency,
50 ];
51
52 pub fn slug(self) -> &'static str {
53 match self {
54 Self::BPrime => "bprime",
55 Self::BDoublePrime => "bdoubleprime",
56 Self::YbusG => "ybus_real",
57 Self::YbusB => "ybus_imag",
58 Self::Lacpf => "lacpf",
59 Self::Adjacency => "adjacency",
60 }
61 }
62
63 pub fn label(self) -> &'static str {
64 match self {
65 Self::BPrime => "B' (FDPF, shuntless)",
66 Self::BDoublePrime => "B'' (FDPF, with shunts)",
67 Self::YbusG => "Re(Y_bus)",
68 Self::YbusB => "-Im(Y_bus)",
69 Self::Lacpf => "LACPF block (2n×2n)",
70 Self::Adjacency => "adjacency (0/1)",
71 }
72 }
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
77pub enum RhsKind {
78 #[default]
79 None,
80 Random,
82 Injection,
84}
85
86#[derive(Debug, Clone)]
87pub struct Pipeline {
88 pub matrices: Vec<MatrixKind>,
89 pub options: BuildOptions,
90 pub rhs: RhsKind,
91 pub rng_seed: u64,
92 pub source_file: Option<PathBuf>,
93}
94
95impl Default for Pipeline {
96 fn default() -> Self {
97 Self {
98 matrices: vec![MatrixKind::BPrime],
99 options: BuildOptions::default(),
100 rhs: RhsKind::None,
101 rng_seed: 0x00C0_FFEE,
102 source_file: None,
103 }
104 }
105}
106
107#[derive(Debug, Clone)]
108pub struct PipelineOutputs {
109 pub case_name: String,
110 pub files: Vec<PathBuf>,
111 pub metadata: CaseMetadata,
112}
113
114impl Pipeline {
115 pub fn run(&self, net: &Network, out_dir: impl AsRef<Path>) -> Result<PipelineOutputs> {
116 let out_dir = out_dir.as_ref();
117 std::fs::create_dir_all(out_dir)?;
118
119 let view = IndexedNetwork::new(net);
120
121 let mut files = Vec::new();
122 let mut matrices_meta = Vec::new();
123
124 for &kind in &self.matrices {
125 let matrix_path = out_dir.join(format!("{}_{}.mtx", view.name(), kind.slug()));
126 let matrix = self.build(&view, kind)?;
127 write_mtx(&matrix, &matrix_path)?;
128 let stats = MatrixStats::from_csr(&matrix);
129 let sddm = sddm_check(&matrix);
130 matrices_meta.push(MatrixMetadata {
131 kind: kind.slug().to_string(),
132 file: matrix_path
133 .file_name()
134 .and_then(|s| s.to_str())
135 .unwrap_or("")
136 .to_string(),
137 stats,
138 sddm,
139 });
140 files.push(matrix_path);
141
142 if let Some(rhs) = self.build_rhs(&view, kind) {
144 let rhs_path = out_dir.join(format!("{}_{}_rhs.mtx", view.name(), kind.slug()));
145 write_vector_mtx(&rhs, &rhs_path)?;
146 files.push(rhs_path);
147 }
148 }
149
150 let shunt_path = out_dir.join(format!("{}_shunt.mtx", view.name()));
152 let base = view.per_unit_base();
153 let shunt: Vec<f64> = view.bs().iter().map(|&b| b / base).collect();
154 write_vector_mtx(&shunt, &shunt_path)?;
155 files.push(shunt_path);
156
157 let metadata = CaseMetadata {
158 case_name: view.name().to_string(),
159 source_file: self
160 .source_file
161 .as_ref()
162 .and_then(|p| p.to_str())
163 .map(str::to_string),
164 source_sha256: self
165 .source_file
166 .as_ref()
167 .and_then(|p| std::fs::read(p).ok())
168 .map(|b| sha256_hex(&b)),
169 base_mva: view.base_mva(),
170 n_buses: view.n(),
171 n_branches: view.branches().len(),
172 build_options: self.options.clone(),
173 matrices: matrices_meta,
174 powerio_version: env!("CARGO_PKG_VERSION").to_string(),
175 };
176 let meta_path = out_dir.join(format!("{}_meta.json", view.name()));
177 write_meta_json(&metadata, &meta_path)?;
178 files.push(meta_path);
179
180 Ok(PipelineOutputs {
181 case_name: view.name().to_string(),
182 files,
183 metadata,
184 })
185 }
186
187 fn build(&self, case: &IndexedNetwork, kind: MatrixKind) -> Result<sprs::CsMat<f64>> {
188 build_kind(case, kind, &self.options)
189 }
190
191 fn build_rhs(&self, case: &IndexedNetwork, kind: MatrixKind) -> Option<Vec<f64>> {
192 if matches!(self.rhs, RhsKind::None)
194 || matches!(kind, MatrixKind::Lacpf | MatrixKind::Adjacency)
195 {
196 return None;
197 }
198 let n = case.n();
199 let v = match self.rhs {
200 RhsKind::Random => {
201 let mut rng = ChaCha8Rng::seed_from_u64(self.rng_seed.wrapping_add(kind as u64));
202 let dist = StandardUniform;
203 let mut v: Vec<f64> = (0..n)
204 .map(|_| {
205 let u: f64 = dist.sample(&mut rng);
206 u - 0.5
207 })
208 .collect();
209 let mean = v.iter().sum::<f64>() / n as f64;
210 for x in &mut v {
211 *x -= mean; }
213 v
214 }
215 RhsKind::Injection => {
216 let base = case.per_unit_base();
217 match kind {
218 MatrixKind::BPrime | MatrixKind::YbusG | MatrixKind::YbusB => {
219 case.pd().iter().map(|&p| -p / base).collect()
220 }
221 MatrixKind::BDoublePrime => case.qd().iter().map(|&q| -q / base).collect(),
222 MatrixKind::Lacpf | MatrixKind::Adjacency => unreachable!(),
223 }
224 }
225 RhsKind::None => unreachable!(),
226 };
227 Some(v)
228 }
229}
230
231pub fn build_kind(
235 view: &IndexedNetwork,
236 kind: MatrixKind,
237 opts: &BuildOptions,
238) -> Result<sprs::CsMat<f64>> {
239 match kind {
240 MatrixKind::BPrime => build_bprime(view, opts),
241 MatrixKind::BDoublePrime => build_bdoubleprime(view, opts),
242 MatrixKind::YbusG => build_ybus(view, opts).map(|p| p.g),
243 MatrixKind::YbusB => build_ybus(view, opts).map(|p| negate_into(p.b)),
244 MatrixKind::Lacpf => build_lacpf(view, opts),
245 MatrixKind::Adjacency => build_adjacency(view),
246 }
247}
248
249fn sha256_hex(bytes: &[u8]) -> String {
250 use sha2::{Digest, Sha256};
251 use std::fmt::Write as _;
252 let mut hasher = Sha256::new();
253 hasher.update(bytes);
254 let digest = hasher.finalize();
255 let mut out = String::with_capacity(digest.len() * 2);
256 for byte in digest {
257 let _ = write!(out, "{byte:02x}");
258 }
259 out
260}