Skip to main content

powerio_matrix/
opf_pipeline.rs

1//! Writes the static DC OPF bundle for a case: one directory of named
2//! Matrix Market files plus a JSON manifest.
3//!
4//! Everything here is a pure function of the case: the incidence `A`, the
5//! DC Laplacian `L` and its reference-grounded form, the flow map `B Aᵀ`, the
6//! generator cost and limit data, the generator→bus map, and nodal load.
7
8use std::path::{Path, PathBuf};
9
10use serde::Serialize;
11use sprs::CsMat;
12
13use crate::Result;
14use crate::indexed::IndexedNetwork;
15use crate::io::mtx::{write_mtx, write_vector_mtx};
16use crate::matrix::incidence::{DcConvention, build_flow_map, build_incidence};
17use crate::matrix::laplacian::{build_weighted_laplacian, ground_at_each, reference_indicator};
18use crate::matrix::opf::{Units, build_opf_instance};
19use crate::matrix::{BuildOptions, ZeroImpedanceRule, ZeroImpedanceSkips};
20use crate::network::Network;
21use crate::{GenCostPatch, MissingGenCostPolicy};
22
23const DCOPF_SCHEMA: &str = "powerio.dcopf";
24const DCOPF_SCHEMA_VERSION: &str = "0.1.0";
25
26#[derive(Debug, Clone)]
27pub struct DcOpfOptions {
28    pub convention: DcConvention,
29    pub units: Units,
30    pub missing_gen_cost: MissingGenCostPolicy,
31    pub gen_cost_patches: Vec<GenCostPatch>,
32}
33
34impl Default for DcOpfOptions {
35    fn default() -> Self {
36        Self {
37            convention: DcConvention::default(),
38            units: Units::default(),
39            missing_gen_cost: MissingGenCostPolicy::Require,
40            gen_cost_patches: Vec::new(),
41        }
42    }
43}
44
45#[derive(Debug, Clone)]
46pub struct DcOpfOutputs {
47    pub dir: PathBuf,
48    pub files: Vec<PathBuf>,
49}
50
51#[derive(Serialize)]
52struct DcOpfMeta {
53    schema: &'static str,
54    schema_version: &'static str,
55    case_name: String,
56    base_mva: f64,
57    dimensions: DcOpfDimensions,
58    index_base: IndexBaseMeta,
59    dc_convention: DcConvention,
60    build_options: BuildOptions,
61    zero_impedance: ZeroImpedanceMeta,
62    grounding: GroundingMeta,
63    operators: Vec<OperatorMeta>,
64    /// Backward compatible aliases retained for current readers.
65    n: usize,
66    /// Backward compatible alias for the number of incidence columns.
67    m: usize,
68    /// Backward compatible alias for in-service generators.
69    n_gen: usize,
70    /// Dense indices of every grounded reference (slack) bus. Several entries
71    /// mean one reference per island, or several reference buses fixed in one
72    /// island. The solver grounds the Laplacian at all of them, matching
73    /// `L_grounded` and `e_r`.
74    reference_buses: Vec<usize>,
75    /// Backward compatible alias for `dc_convention`.
76    convention: DcConvention,
77    units: Units,
78    cost_policy: MissingGenCostPolicy,
79    synthesized_gen_costs: usize,
80    patched_gen_costs: usize,
81    files: Vec<String>,
82    powerio_version: String,
83}
84
85#[derive(Serialize)]
86#[allow(clippy::struct_field_names)]
87struct DcOpfDimensions {
88    n_buses: usize,
89    n_source_branches: usize,
90    n_branch_columns: usize,
91    n_generators: usize,
92    n_reference_buses: usize,
93    n_grounded_buses: usize,
94}
95
96#[derive(Serialize)]
97struct IndexBaseMeta {
98    dense: usize,
99    matrix_market: usize,
100}
101
102#[derive(Serialize)]
103struct ZeroImpedanceMeta {
104    skip: bool,
105    rule: ZeroImpedanceRule,
106    skipped: ZeroImpedanceSkips,
107}
108
109#[derive(Serialize)]
110struct GroundingMeta {
111    reference_buses: Vec<usize>,
112    removed_rows_and_columns: Vec<usize>,
113    grounded_operator: &'static str,
114    reference_selector: &'static str,
115}
116
117#[derive(Serialize)]
118struct OperatorMeta {
119    name: &'static str,
120    file: &'static str,
121    kind: &'static str,
122    rows: usize,
123    cols: usize,
124    index_space: &'static str,
125    units: &'static str,
126}
127
128/// Build and write the DC OPF bundle into `out_dir/<case>_dcopf/`.
129pub fn write_dcopf_bundle(
130    net: &Network,
131    out_dir: impl AsRef<Path>,
132    opts: &DcOpfOptions,
133) -> Result<DcOpfOutputs> {
134    let mut policy_net = net.clone();
135    let cost_report =
136        policy_net.apply_gen_cost_policy(&opts.gen_cost_patches, opts.missing_gen_cost)?;
137    let view = IndexedNetwork::new(&policy_net);
138
139    let dir = out_dir.as_ref().join(format!("{}_dcopf", view.name()));
140    std::fs::create_dir_all(&dir)?;
141
142    view.check_reference_coverage()?;
143    let refs = view.reference_bus_indices();
144    let build_options = BuildOptions::default();
145    let inc = build_incidence(&view, opts.convention, &build_options)?;
146    let l = build_weighted_laplacian(&inc.a, &inc.b);
147    let l_grounded = ground_at_each(&l, &refs);
148    let flow = build_flow_map(&inc.a, &inc.b);
149    let opf = build_opf_instance(&view, &inc, opts.units)?;
150    let e_r = reference_indicator(view.n(), &refs);
151
152    let mut files = Vec::new();
153
154    // Network operators.
155    put_mat(&dir, "A.mtx", &inc.a, &mut files)?;
156    put_mat(&dir, "L.mtx", &l, &mut files)?;
157    put_mat(&dir, "L_grounded.mtx", &l_grounded, &mut files)?;
158    put_mat(&dir, "BAt.mtx", &flow, &mut files)?;
159    put_mat(&dir, "Cg.mtx", &opf.c_g, &mut files)?;
160
161    // Network / OPF vectors (bus or branch indexed).
162    put_vec(&dir, "b.mtx", &inc.b, &mut files)?;
163    put_vec(&dir, "p_shift.mtx", &inc.p_shift, &mut files)?;
164    // e_r is 1 at every reference bus, not a single-slack one-hot: read it
165    // alongside `reference_buses` in the manifest (one entry ⇒ the old one-hot).
166    put_vec(&dir, "e_r.mtx", &e_r, &mut files)?;
167    put_vec(&dir, "q.mtx", &opf.bus.q, &mut files)?;
168    put_vec(&dir, "c.mtx", &opf.bus.c, &mut files)?;
169    put_vec(&dir, "pmax.mtx", &opf.bus.pmax, &mut files)?;
170    put_vec(&dir, "pmin.mtx", &opf.bus.pmin, &mut files)?;
171    put_vec(&dir, "fmax.mtx", &opf.f_max, &mut files)?;
172    put_vec(&dir, "pd.mtx", &opf.bus.p_d, &mut files)?;
173
174    // Generator-space provenance.
175    put_vec(&dir, "q_gen.mtx", &opf.gen_costs.q, &mut files)?;
176    put_vec(&dir, "c_gen.mtx", &opf.gen_costs.c, &mut files)?;
177    put_vec(&dir, "pmax_gen.mtx", &opf.gen_costs.pmax, &mut files)?;
178    put_vec(&dir, "pmin_gen.mtx", &opf.gen_costs.pmin, &mut files)?;
179
180    let meta = DcOpfMeta {
181        schema: DCOPF_SCHEMA,
182        schema_version: DCOPF_SCHEMA_VERSION,
183        case_name: view.name().to_string(),
184        base_mva: view.base_mva(),
185        dimensions: DcOpfDimensions {
186            n_buses: view.n(),
187            n_source_branches: view.branches().len(),
188            n_branch_columns: inc.m(),
189            n_generators: opf.n_gen(),
190            n_reference_buses: refs.len(),
191            n_grounded_buses: view.n() - refs.len(),
192        },
193        index_base: IndexBaseMeta {
194            dense: 0,
195            matrix_market: 1,
196        },
197        dc_convention: opts.convention,
198        build_options: build_options.clone(),
199        zero_impedance: ZeroImpedanceMeta {
200            skip: build_options.skip_zero_impedance,
201            rule: ZeroImpedanceRule::Reactance,
202            skipped: inc.skipped_zero_impedance.clone(),
203        },
204        grounding: GroundingMeta {
205            reference_buses: refs.clone(),
206            removed_rows_and_columns: refs.clone(),
207            grounded_operator: "L_grounded",
208            reference_selector: "e_r",
209        },
210        operators: operator_meta(view.n(), inc.m(), refs.len(), opf.n_gen()),
211        n: view.n(),
212        m: inc.m(),
213        n_gen: opf.n_gen(),
214        reference_buses: refs.clone(),
215        convention: opts.convention,
216        units: opts.units,
217        cost_policy: opts.missing_gen_cost,
218        synthesized_gen_costs: cost_report.synthesized,
219        patched_gen_costs: cost_report.patched,
220        files: files
221            .iter()
222            .filter_map(|p| p.file_name().and_then(|s| s.to_str()).map(str::to_string))
223            .collect(),
224        powerio_version: env!("CARGO_PKG_VERSION").to_string(),
225    };
226    let meta_path = dir.join("dcopf_meta.json");
227    let json = serde_json::to_string_pretty(&meta).map_err(|e| crate::Error::Mtx(e.to_string()))?;
228    std::fs::write(&meta_path, json)?;
229    files.push(meta_path);
230
231    Ok(DcOpfOutputs { dir, files })
232}
233
234#[allow(clippy::too_many_lines)]
235fn operator_meta(n: usize, m: usize, n_ref: usize, n_gen: usize) -> Vec<OperatorMeta> {
236    let n_grounded = n - n_ref;
237    vec![
238        op(
239            "signed_incidence",
240            "A.mtx",
241            "matrix",
242            n,
243            m,
244            "bus_by_branch",
245            "unitless",
246        ),
247        op(
248            "branch_susceptance",
249            "b.mtx",
250            "vector",
251            m,
252            1,
253            "branch",
254            "per_unit_susceptance",
255        ),
256        op(
257            "weighted_laplacian",
258            "L.mtx",
259            "matrix",
260            n,
261            n,
262            "bus_by_bus",
263            "per_unit_susceptance",
264        ),
265        op(
266            "grounded_laplacian",
267            "L_grounded.mtx",
268            "matrix",
269            n_grounded,
270            n_grounded,
271            "grounded_bus_by_grounded_bus",
272            "per_unit_susceptance",
273        ),
274        op(
275            "flow_map",
276            "BAt.mtx",
277            "matrix",
278            m,
279            n,
280            "branch_by_bus",
281            "per_unit_susceptance",
282        ),
283        op(
284            "generator_to_bus",
285            "Cg.mtx",
286            "matrix",
287            n,
288            n_gen,
289            "bus_by_generator",
290            "unitless",
291        ),
292        op(
293            "phase_shift_injection",
294            "p_shift.mtx",
295            "vector",
296            n,
297            1,
298            "bus",
299            "per_unit_power",
300        ),
301        op(
302            "reference_selector",
303            "e_r.mtx",
304            "vector",
305            n,
306            1,
307            "bus",
308            "indicator",
309        ),
310        op(
311            "bus_cost_quadratic",
312            "q.mtx",
313            "vector",
314            n,
315            1,
316            "bus",
317            "selected_cost_units",
318        ),
319        op(
320            "bus_cost_linear",
321            "c.mtx",
322            "vector",
323            n,
324            1,
325            "bus",
326            "selected_cost_units",
327        ),
328        op(
329            "bus_generation_upper",
330            "pmax.mtx",
331            "vector",
332            n,
333            1,
334            "bus",
335            "selected_power_units",
336        ),
337        op(
338            "bus_generation_lower",
339            "pmin.mtx",
340            "vector",
341            n,
342            1,
343            "bus",
344            "selected_power_units",
345        ),
346        op(
347            "branch_flow_limit",
348            "fmax.mtx",
349            "vector",
350            m,
351            1,
352            "branch",
353            "selected_power_units",
354        ),
355        op(
356            "bus_load",
357            "pd.mtx",
358            "vector",
359            n,
360            1,
361            "bus",
362            "selected_power_units",
363        ),
364        op(
365            "generator_cost_quadratic",
366            "q_gen.mtx",
367            "vector",
368            n_gen,
369            1,
370            "generator",
371            "selected_cost_units",
372        ),
373        op(
374            "generator_cost_linear",
375            "c_gen.mtx",
376            "vector",
377            n_gen,
378            1,
379            "generator",
380            "selected_cost_units",
381        ),
382        op(
383            "generator_upper",
384            "pmax_gen.mtx",
385            "vector",
386            n_gen,
387            1,
388            "generator",
389            "selected_power_units",
390        ),
391        op(
392            "generator_lower",
393            "pmin_gen.mtx",
394            "vector",
395            n_gen,
396            1,
397            "generator",
398            "selected_power_units",
399        ),
400    ]
401}
402
403fn op(
404    name: &'static str,
405    file: &'static str,
406    kind: &'static str,
407    rows: usize,
408    cols: usize,
409    index_space: &'static str,
410    units: &'static str,
411) -> OperatorMeta {
412    OperatorMeta {
413        name,
414        file,
415        kind,
416        rows,
417        cols,
418        index_space,
419        units,
420    }
421}
422
423fn put_mat(dir: &Path, name: &str, m: &CsMat<f64>, files: &mut Vec<PathBuf>) -> Result<()> {
424    let p = dir.join(name);
425    write_mtx(m, &p)?;
426    files.push(p);
427    Ok(())
428}
429
430fn put_vec(dir: &Path, name: &str, v: &[f64], files: &mut Vec<PathBuf>) -> Result<()> {
431    let p = dir.join(name);
432    write_vector_mtx(v, &p)?;
433    files.push(p);
434    Ok(())
435}