Skip to main content

powerio_prob/matrix/
bundle.rs

1use std::path::{Path, PathBuf};
2
3use powerio::{GenCostPolicyReport, MissingGenCostPolicy};
4
5use crate::Result;
6use powerio_matrix::SparseMatrix;
7use powerio_matrix::io::{write_mtx, write_vector_mtx};
8use serde::Serialize;
9
10use crate::{DcOpfInstance, Units};
11
12use super::build_dc_opf_matrices;
13
14const DCOPF_SCHEMA: &str = "powerio.dcopf";
15
16/// Cost policy information recorded in a bundle manifest.
17#[derive(Debug, Clone)]
18pub struct DcOpfBundleMetadata {
19    pub cost_policy: MissingGenCostPolicy,
20    pub cost_report: GenCostPolicyReport,
21}
22
23impl Default for DcOpfBundleMetadata {
24    fn default() -> Self {
25        Self {
26            cost_policy: MissingGenCostPolicy::Require,
27            cost_report: GenCostPolicyReport::default(),
28        }
29    }
30}
31
32/// Options that affect bundle output without changing the instance.
33#[derive(Debug, Clone, Default)]
34pub struct DcOpfBundleOptions {
35    pub metadata: DcOpfBundleMetadata,
36}
37
38#[derive(Debug, Clone)]
39#[non_exhaustive]
40pub struct DcOpfOutputs {
41    pub dir: PathBuf,
42    pub files: Vec<PathBuf>,
43}
44
45#[derive(Serialize)]
46struct DcOpfMeta<'a> {
47    schema: &'static str,
48    case_name: &'a str,
49    base_mva: f64,
50    dimensions: DcOpfDimensions,
51    index_base: IndexBaseMeta,
52    dc_convention: powerio::DcConvention,
53    build_options: BuildOptionsMeta,
54    zero_impedance: ZeroImpedanceMeta<'a>,
55    grounding: GroundingMeta<'a>,
56    operators: Vec<OperatorMeta>,
57    n: usize,
58    m: usize,
59    n_gen: usize,
60    reference_buses: &'a [usize],
61    convention: powerio::DcConvention,
62    units: Units,
63    cost_policy: MissingGenCostPolicy,
64    synthesized_gen_costs: usize,
65    patched_gen_costs: usize,
66    files: Vec<String>,
67    powerio_version: &'static str,
68}
69
70#[derive(Serialize)]
71#[allow(clippy::struct_field_names)]
72struct DcOpfDimensions {
73    n_buses: usize,
74    n_source_branches: usize,
75    n_branch_columns: usize,
76    n_generators: usize,
77    n_reference_buses: usize,
78    n_grounded_buses: usize,
79}
80
81#[derive(Serialize)]
82struct IndexBaseMeta {
83    dense: usize,
84    matrix_market: usize,
85}
86
87#[derive(Serialize)]
88struct BuildOptionsMeta {
89    skip_zero_impedance: bool,
90}
91
92#[derive(Serialize)]
93struct ZeroImpedanceMeta<'a> {
94    skip: bool,
95    rule: &'static str,
96    skipped: ZeroImpedanceSkips<'a>,
97}
98
99#[derive(Serialize)]
100struct ZeroImpedanceSkips<'a> {
101    count: usize,
102    branch_indices: &'a [usize],
103}
104
105#[derive(Serialize)]
106struct GroundingMeta<'a> {
107    reference_buses: &'a [usize],
108    removed_rows_and_columns: &'a [usize],
109    grounded_operator: &'static str,
110    reference_selector: &'static str,
111}
112
113#[derive(Serialize)]
114struct OperatorMeta {
115    name: &'static str,
116    file: &'static str,
117    kind: &'static str,
118    rows: usize,
119    cols: usize,
120    index_space: &'static str,
121    units: &'static str,
122}
123
124/// Write matrix projections for an assembled DC OPF instance.
125///
126/// The writer reads all costs, bounds, mappings, units, and conventions from
127/// `instance`. It does not retain or read a source network.
128#[allow(clippy::too_many_lines)]
129pub fn write_dcopf_bundle(
130    instance: &DcOpfInstance,
131    out_dir: impl AsRef<Path>,
132    options: &DcOpfBundleOptions,
133) -> Result<DcOpfOutputs> {
134    let matrices = build_dc_opf_matrices(instance);
135    let nodal = instance.nodal_generator_data();
136    // The case name comes from source file content, so it must not steer the
137    // output path. `sanitize_stem` reduces it to one safe component and
138    // disambiguates names that would otherwise sanitize alike, so a batch
139    // export cannot be steered into overwriting an earlier bundle.
140    let dir = out_dir.as_ref().join(format!(
141        "{}_dcopf",
142        powerio_matrix::sanitize_stem(&instance.name)
143    ));
144    std::fs::create_dir_all(&dir)?;
145
146    let mut files = Vec::new();
147    put_mat(&dir, "A.mtx", &matrices.incidence, &mut files)?;
148    put_mat(&dir, "L.mtx", &matrices.laplacian, &mut files)?;
149    put_mat(
150        &dir,
151        "L_grounded.mtx",
152        &matrices.grounded_laplacian,
153        &mut files,
154    )?;
155    put_mat(&dir, "BAt.mtx", &matrices.flow_map, &mut files)?;
156    put_mat(&dir, "Cg.mtx", &matrices.generator_bus, &mut files)?;
157
158    put_vec(&dir, "b.mtx", &instance.branches.b, &mut files)?;
159    put_vec(&dir, "p_shift.mtx", &instance.p_shift, &mut files)?;
160    put_vec(&dir, "e_r.mtx", &matrices.reference_selector, &mut files)?;
161    put_vec(&dir, "q.mtx", &nodal.q, &mut files)?;
162    put_vec(&dir, "c.mtx", &nodal.c, &mut files)?;
163    put_vec(&dir, "c0.mtx", &nodal.c0, &mut files)?;
164    put_vec(&dir, "pmax.mtx", &nodal.pmax, &mut files)?;
165    put_vec(&dir, "pmin.mtx", &nodal.pmin, &mut files)?;
166    put_vec(&dir, "fmax.mtx", &instance.branches.f_max, &mut files)?;
167    put_vec(&dir, "pd.mtx", &instance.p_d, &mut files)?;
168    put_vec(&dir, "gs.mtx", &instance.g_s, &mut files)?;
169    put_vec(
170        &dir,
171        "angle_min.mtx",
172        &instance.branches.angle_min,
173        &mut files,
174    )?;
175    put_vec(
176        &dir,
177        "angle_max.mtx",
178        &instance.branches.angle_max,
179        &mut files,
180    )?;
181
182    put_vec(&dir, "q_gen.mtx", &instance.generators.q, &mut files)?;
183    put_vec(&dir, "c_gen.mtx", &instance.generators.c, &mut files)?;
184    put_vec(&dir, "c0_gen.mtx", &instance.generators.c0, &mut files)?;
185    put_vec(&dir, "pmax_gen.mtx", &instance.generators.pmax, &mut files)?;
186    put_vec(&dir, "pmin_gen.mtx", &instance.generators.pmin, &mut files)?;
187
188    let power_units = match instance.units {
189        Units::PerUnit => "per_unit_power",
190        Units::Native => "native_power",
191    };
192    let meta = DcOpfMeta {
193        schema: DCOPF_SCHEMA,
194        case_name: &instance.name,
195        base_mva: instance.base_mva,
196        dimensions: DcOpfDimensions {
197            n_buses: instance.n_buses,
198            n_source_branches: instance.n_source_branches,
199            n_branch_columns: instance.n_branches(),
200            n_generators: instance.n_generators(),
201            n_reference_buses: instance.reference_buses.len(),
202            n_grounded_buses: instance.n_buses - instance.reference_buses.len(),
203        },
204        index_base: IndexBaseMeta {
205            dense: 0,
206            matrix_market: 1,
207        },
208        dc_convention: instance.convention,
209        build_options: BuildOptionsMeta {
210            skip_zero_impedance: instance.skip_zero_impedance,
211        },
212        zero_impedance: ZeroImpedanceMeta {
213            skip: instance.skip_zero_impedance,
214            rule: "Reactance",
215            skipped: ZeroImpedanceSkips {
216                count: instance.branches.skipped_zero_impedance.len(),
217                branch_indices: &instance.branches.skipped_zero_impedance,
218            },
219        },
220        grounding: GroundingMeta {
221            reference_buses: instance.reference_buses.as_ref(),
222            removed_rows_and_columns: instance.reference_buses.as_ref(),
223            grounded_operator: "L_grounded",
224            reference_selector: "e_r",
225        },
226        operators: operator_meta(
227            instance.n_buses,
228            instance.n_branches(),
229            instance.reference_buses.len(),
230            instance.n_generators(),
231            power_units,
232        ),
233        n: instance.n_buses,
234        m: instance.n_branches(),
235        n_gen: instance.n_generators(),
236        reference_buses: instance.reference_buses.as_ref(),
237        convention: instance.convention,
238        units: instance.units,
239        cost_policy: options.metadata.cost_policy,
240        synthesized_gen_costs: options.metadata.cost_report.synthesized,
241        patched_gen_costs: options.metadata.cost_report.patched,
242        files: files
243            .iter()
244            .filter_map(|path| path.file_name()?.to_str().map(str::to_owned))
245            .collect(),
246        powerio_version: powerio::VERSION,
247    };
248    let meta_path = dir.join("dcopf_meta.json");
249    let json = serde_json::to_string_pretty(&meta)
250        .map_err(|error| powerio_matrix::Error::Mtx(error.to_string()))?;
251    std::fs::write(&meta_path, json)?;
252    files.push(meta_path);
253
254    Ok(DcOpfOutputs { dir, files })
255}
256
257#[allow(clippy::too_many_lines)]
258fn operator_meta(
259    n: usize,
260    m: usize,
261    n_ref: usize,
262    n_gen: usize,
263    power_units: &'static str,
264) -> Vec<OperatorMeta> {
265    let n_grounded = n - n_ref;
266    vec![
267        op(
268            "signed_incidence",
269            "A.mtx",
270            "matrix",
271            n,
272            m,
273            "bus_by_branch",
274            "unitless",
275        ),
276        op(
277            "branch_susceptance",
278            "b.mtx",
279            "vector",
280            m,
281            1,
282            "branch",
283            power_units,
284        ),
285        op(
286            "weighted_laplacian",
287            "L.mtx",
288            "matrix",
289            n,
290            n,
291            "bus_by_bus",
292            power_units,
293        ),
294        op(
295            "grounded_laplacian",
296            "L_grounded.mtx",
297            "matrix",
298            n_grounded,
299            n_grounded,
300            "grounded_bus_by_grounded_bus",
301            power_units,
302        ),
303        op(
304            "flow_map",
305            "BAt.mtx",
306            "matrix",
307            m,
308            n,
309            "branch_by_bus",
310            power_units,
311        ),
312        op(
313            "generator_to_bus",
314            "Cg.mtx",
315            "matrix",
316            n,
317            n_gen,
318            "bus_by_generator",
319            "unitless",
320        ),
321        op(
322            "phase_shift_injection",
323            "p_shift.mtx",
324            "vector",
325            n,
326            1,
327            "bus",
328            power_units,
329        ),
330        op(
331            "reference_selector",
332            "e_r.mtx",
333            "vector",
334            n,
335            1,
336            "bus",
337            "indicator",
338        ),
339        op(
340            "bus_cost_quadratic",
341            "q.mtx",
342            "vector",
343            n,
344            1,
345            "bus",
346            "selected_cost_units",
347        ),
348        op(
349            "bus_cost_linear",
350            "c.mtx",
351            "vector",
352            n,
353            1,
354            "bus",
355            "selected_cost_units",
356        ),
357        op(
358            "bus_generation_upper",
359            "pmax.mtx",
360            "vector",
361            n,
362            1,
363            "bus",
364            power_units,
365        ),
366        op(
367            "bus_generation_lower",
368            "pmin.mtx",
369            "vector",
370            n,
371            1,
372            "bus",
373            power_units,
374        ),
375        op(
376            "branch_flow_limit",
377            "fmax.mtx",
378            "vector",
379            m,
380            1,
381            "branch",
382            power_units,
383        ),
384        op("bus_load", "pd.mtx", "vector", n, 1, "bus", power_units),
385        op(
386            "branch_angle_minimum",
387            "angle_min.mtx",
388            "vector",
389            m,
390            1,
391            "branch",
392            "radian",
393        ),
394        op(
395            "branch_angle_maximum",
396            "angle_max.mtx",
397            "vector",
398            m,
399            1,
400            "branch",
401            "radian",
402        ),
403        op(
404            "generator_cost_quadratic",
405            "q_gen.mtx",
406            "vector",
407            n_gen,
408            1,
409            "generator",
410            "selected_cost_units",
411        ),
412        op(
413            "generator_cost_linear",
414            "c_gen.mtx",
415            "vector",
416            n_gen,
417            1,
418            "generator",
419            "selected_cost_units",
420        ),
421        op(
422            "generator_upper",
423            "pmax_gen.mtx",
424            "vector",
425            n_gen,
426            1,
427            "generator",
428            power_units,
429        ),
430        op(
431            "generator_lower",
432            "pmin_gen.mtx",
433            "vector",
434            n_gen,
435            1,
436            "generator",
437            power_units,
438        ),
439    ]
440}
441
442fn op(
443    name: &'static str,
444    file: &'static str,
445    kind: &'static str,
446    rows: usize,
447    cols: usize,
448    index_space: &'static str,
449    units: &'static str,
450) -> OperatorMeta {
451    OperatorMeta {
452        name,
453        file,
454        kind,
455        rows,
456        cols,
457        index_space,
458        units,
459    }
460}
461
462fn put_mat(dir: &Path, name: &str, matrix: &SparseMatrix, files: &mut Vec<PathBuf>) -> Result<()> {
463    let path = dir.join(name);
464    write_mtx(matrix, &path)?;
465    files.push(path);
466    Ok(())
467}
468
469fn put_vec(dir: &Path, name: &str, values: &[f64], files: &mut Vec<PathBuf>) -> Result<()> {
470    let path = dir.join(name);
471    write_vector_mtx(values, &path)?;
472    files.push(path);
473    Ok(())
474}