Skip to main content

powerio_matrix/dcopf/
mod.rs

1//! DC OPF assembly: the numerical preparation arrays, sparse matrices, and
2//! bundle writer derived from a [`DcOpfInstance`].
3
4mod bundle;
5pub(crate) mod limits;
6pub(crate) mod nodal;
7mod prep;
8#[cfg(test)]
9mod tests;
10
11use crate::matrix::calc_solver_branch_flow_matrix;
12use crate::matrix::triplet::CooBuilder;
13use crate::{
14    IndexedNetwork, SparseMatrix, calc_diagonal, calc_reference_indicator, calc_weighted_laplacian,
15    ground_at_each,
16};
17
18use crate::Result;
19use powerio_prob::DcOpfInstance;
20use prep::{DcOpfOptions, apply_instance_semantics, preparation_from_view};
21
22pub use bundle::{DcOpfBundleMetadata, DcOpfBundleOptions, DcOpfOutputs, emit_dcopf_bundle};
23pub use prep::{
24    DcBranchParameters, DcGeneratorParameters, DcOpfPreparation, NodalGeneratorParameters, Units,
25};
26
27/// Assembly choices that select the numerical content derived from an
28/// instance without changing the instance itself.
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30#[non_exhaustive]
31pub struct DcOpfAssemblyOptions {
32    /// Power and cost scaling of the derived arrays.
33    pub units: Units,
34    /// Skip non-self-loop branches with zero reactance. Off by default:
35    /// zero impedance branches are preserved in networks and instances, so
36    /// assembly refuses them until the caller resolves them explicitly
37    /// ([`powerio_prob::merge_zero_impedance_buses`]) or opts into skipping.
38    pub skip_zero_impedance: bool,
39    /// Give a branch with no thermal rating the bound
40    /// [`Branch::synthesize_rate_a`](powerio_tx::Branch::synthesize_rate_a)
41    /// states. If false, an absent rating reads as unlimited.
42    pub synthesize_unrated_limits: bool,
43    /// Apply PowerModels' ±60 degree correction to unconstrained or unusable
44    /// branch angle difference intervals in the prepared arrays.
45    pub correct_angle_difference_bounds: bool,
46}
47
48impl Default for DcOpfAssemblyOptions {
49    fn default() -> Self {
50        Self {
51            units: Units::default(),
52            skip_zero_impedance: false,
53            synthesize_unrated_limits: false,
54            correct_angle_difference_bounds: true,
55        }
56    }
57}
58
59impl DcOpfAssemblyOptions {
60    #[must_use]
61    pub const fn with_units(mut self, units: Units) -> Self {
62        self.units = units;
63        self
64    }
65
66    #[must_use]
67    pub const fn with_skip_zero_impedance(mut self, skip: bool) -> Self {
68        self.skip_zero_impedance = skip;
69        self
70    }
71
72    #[must_use]
73    pub const fn with_synthesize_unrated_limits(mut self, synthesize: bool) -> Self {
74        self.synthesize_unrated_limits = synthesize;
75        self
76    }
77
78    #[must_use]
79    pub const fn with_correct_angle_difference_bounds(mut self, correct: bool) -> Self {
80        self.correct_angle_difference_bounds = correct;
81        self
82    }
83}
84
85/// Sparse matrices for a DC OPF instance.
86#[derive(Debug, Clone)]
87#[non_exhaustive]
88pub struct DcOpfMatrices {
89    /// Bus by branch incidence matrix, with `+1` at each from bus and `-1` at
90    /// each to bus.
91    pub bus_branch_incidence: SparseMatrix,
92    pub laplacian: SparseMatrix,
93    pub grounded_laplacian: SparseMatrix,
94    /// Branch by bus flow matrix over the positive solver susceptance
95    /// magnitudes.
96    pub branch_flow_matrix: SparseMatrix,
97    pub generator_bus: SparseMatrix,
98    /// Generator space quadratic cost diagonal.
99    pub generator_cost: SparseMatrix,
100    pub reference_selector: Vec<f64>,
101}
102
103/// Derive the sparse DC OPF matrices from the instance. The instance keeps
104/// the typed network; an external solver that needs the contiguous arrays
105/// behind these matrices calls [`build_dc_opf_preparation`] instead.
106///
107/// # Errors
108/// A network the selected branch susceptance formula cannot assemble: missing reference
109/// coverage, an unresolved zero impedance branch, or an unusable cost curve.
110pub fn calc_dc_opf_matrices(
111    instance: &DcOpfInstance,
112    options: &DcOpfAssemblyOptions,
113) -> Result<DcOpfMatrices> {
114    Ok(matrices_from_preparation(&build_dc_opf_preparation(
115        instance, options,
116    )?))
117}
118
119/// Derive the complete matrix free DC OPF arrays from the instance: demand,
120/// shunt, and phase shift withdrawals, generator costs and bounds with their
121/// source row mapping, branch susceptances as positive solver edge weights,
122/// thermal limits, angle bounds, and the reference bus set. This is the one
123/// numerical assembly the matrix builders, the bundle writer, and external
124/// solvers read, published so each consumer formulates over the same arrays
125/// instead of re-deriving them from the network. [`DcOpfPreparation`]
126/// documents each field's unit and sign.
127///
128/// # Errors
129/// As [`calc_dc_opf_matrices`].
130pub fn build_dc_opf_preparation(
131    instance: &DcOpfInstance,
132    options: &DcOpfAssemblyOptions,
133) -> Result<DcOpfPreparation> {
134    let view = IndexedNetwork::new(instance.network());
135    let objective = crate::opf::compile_objective(instance.objective())?;
136    let mut preparation = preparation_from_view(
137        &view,
138        DcOpfOptions {
139            formula: instance.branch_susceptance_formula(),
140            units: options.units,
141            skip_zero_impedance: options.skip_zero_impedance,
142            synthesize_unrated_limits: options.synthesize_unrated_limits,
143            correct_angle_difference_bounds: options.correct_angle_difference_bounds,
144            objective,
145        },
146    )?;
147    apply_instance_semantics(&mut preparation, instance.network(), instance.constraints())?;
148    Ok(preparation)
149}
150
151pub(crate) fn matrices_from_preparation(instance: &DcOpfPreparation) -> DcOpfMatrices {
152    let n = instance.n_buses;
153    let m = instance.n_branches();
154    let mut incidence = CooBuilder::with_capacity_rect(n, m, 2 * m);
155    for column in 0..m {
156        incidence.add(instance.branches.from_bus[column], column, 1.0);
157        incidence.add(instance.branches.to_bus[column], column, -1.0);
158    }
159    let incidence = incidence.finish_csr();
160    let laplacian = calc_weighted_laplacian(&incidence, &instance.branches.susceptance_magnitude);
161    let grounded_laplacian = ground_at_each(&laplacian, instance.reference_buses.as_ref());
162    let branch_flow_matrix =
163        calc_solver_branch_flow_matrix(&incidence, &instance.branches.susceptance_magnitude);
164
165    let n_gen = instance.n_generators();
166    let mut generator_bus = CooBuilder::with_capacity_rect(n, n_gen, n_gen);
167    for (column, &bus) in instance.generators.bus_of_gen.iter().enumerate() {
168        generator_bus.add(bus, column, 1.0);
169    }
170
171    DcOpfMatrices {
172        bus_branch_incidence: incidence,
173        laplacian,
174        grounded_laplacian,
175        branch_flow_matrix,
176        generator_bus: generator_bus.finish_csr(),
177        generator_cost: calc_diagonal(&instance.generators.q),
178        reference_selector: calc_reference_indicator(n, instance.reference_buses.as_ref()),
179    }
180}