Skip to main content

powerio_prob/
ac.rs

1use serde::{Deserialize, Serialize};
2
3use powerio::{BusId, Error, IndexedNetwork, Result};
4
5use crate::Units;
6
7/// Options for AC OPF instance assembly.
8///
9/// There is no convention enum: the branch pi model always carries taps,
10/// shifts, and charging.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12pub struct AcOpfOptions {
13    pub units: Units,
14    /// Skip non-self-loop branches with `r² + x² = 0`. If false, assembly
15    /// returns [`Error::ZeroImpedance`].
16    pub skip_zero_impedance: bool,
17}
18
19impl Default for AcOpfOptions {
20    fn default() -> Self {
21        Self {
22            units: Units::default(),
23            skip_zero_impedance: true,
24        }
25    }
26}
27
28/// Bus data in dense bus order.
29#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
30#[non_exhaustive]
31pub struct AcBusData {
32    /// Nodal active demand in the selected power unit.
33    pub p_d: Vec<f64>,
34    /// Nodal reactive demand in the selected power unit.
35    pub q_d: Vec<f64>,
36    /// Nodal shunt conductance in the selected admittance unit. Includes the
37    /// folded pi model stamp of any self-loop branch, matching `build_ybus`.
38    pub g_s: Vec<f64>,
39    /// Nodal shunt susceptance in the selected admittance unit. Includes the
40    /// folded pi model stamp of any self-loop branch, matching `build_ybus`.
41    pub b_s: Vec<f64>,
42    /// Voltage magnitude lower bound, per unit.
43    pub vm_min: Vec<f64>,
44    /// Voltage magnitude upper bound, per unit.
45    pub vm_max: Vec<f64>,
46    /// Case voltage magnitude, per unit: the raw initial guess, zero when the
47    /// source has none.
48    pub vm: Vec<f64>,
49}
50
51/// Branch data in active branch column order.
52#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
53#[non_exhaustive]
54pub struct AcBranchData {
55    pub from_bus: Vec<usize>,
56    pub to_bus: Vec<usize>,
57    /// Series conductance `r / (r² + x²)` in the selected admittance unit.
58    pub g: Vec<f64>,
59    /// Series susceptance `−x / (r² + x²)` in the selected admittance unit.
60    pub b: Vec<f64>,
61    /// Charging conductance at the from terminal.
62    pub g_fr: Vec<f64>,
63    /// Charging susceptance at the from terminal.
64    pub b_fr: Vec<f64>,
65    /// Charging conductance at the to terminal.
66    pub g_to: Vec<f64>,
67    /// Charging susceptance at the to terminal.
68    pub b_to: Vec<f64>,
69    /// Tap ratio magnitude; one for a line. Kept separate from `shift` so a
70    /// consumer stamps the complex tap itself.
71    pub tap: Vec<f64>,
72    /// Phase shift in radians.
73    pub shift: Vec<f64>,
74    /// Apparent power limit in the selected power unit. Zero means unlimited.
75    pub s_max: Vec<f64>,
76    /// Branch angle bounds in radians, as the source states them.
77    pub angle_min: Vec<f64>,
78    pub angle_max: Vec<f64>,
79    /// Branch column to source branch row.
80    pub source_rows: Vec<usize>,
81    /// Source branch rows omitted because `r² + x² = 0`.
82    pub skipped_zero_impedance: Vec<usize>,
83}
84
85/// Generator data in generator column order.
86#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
87#[non_exhaustive]
88pub struct AcGeneratorData {
89    /// Generator column to dense bus index.
90    pub bus_of_gen: Vec<usize>,
91    /// Generator column to source generator row.
92    pub source_rows: Vec<usize>,
93    /// Quadratic objective diagonal in `0.5 * q * p^2 + c * p + c0`.
94    pub q: Vec<f64>,
95    /// Linear objective coefficient.
96    pub c: Vec<f64>,
97    /// Constant objective term. Unscaled in both unit systems: it carries no
98    /// power dimension.
99    pub c0: Vec<f64>,
100    pub pmax: Vec<f64>,
101    pub pmin: Vec<f64>,
102    pub qmax: Vec<f64>,
103    pub qmin: Vec<f64>,
104    /// Scheduled active output in the selected power unit.
105    pub pg: Vec<f64>,
106    /// Scheduled reactive output in the selected power unit.
107    pub qg: Vec<f64>,
108    /// Voltage magnitude setpoint, per unit; zero when the source has none.
109    pub vg: Vec<f64>,
110}
111
112/// Matrix free AC OPF input data on the branch pi model.
113///
114/// A problem instance is complete numerical input for one problem family. It
115/// is separate from the source network, a matrix projection, a solver
116/// formulation, and a solution. Relaxations of AC OPF, the SOC forms
117/// included, consume this same instance; the relaxation is a formulation
118/// choice made downstream.
119///
120/// Units follow [`Units`]. Under [`Units::PerUnit`], powers are per unit on
121/// `base_mva` and admittances are per unit on the system base. Under
122/// [`Units::Native`], powers stay in MW/MVAr and every admittance vector is
123/// scaled by `base_mva`, so power computed from admittances and per unit
124/// voltages lands in MW/MVAr. Voltage magnitudes are per unit and angles are
125/// radians in both systems.
126#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
127#[non_exhaustive]
128pub struct AcOpfInstance {
129    pub name: String,
130    pub n_buses: usize,
131    pub n_source_generators: usize,
132    pub n_source_branches: usize,
133    pub base_mva: f64,
134    pub units: Units,
135    pub skip_zero_impedance: bool,
136    /// Dense bus index to external bus ID.
137    pub bus_ids: Vec<BusId>,
138    pub reference_buses: Vec<usize>,
139    pub buses: AcBusData,
140    pub generators: AcGeneratorData,
141    pub branches: AcBranchData,
142}
143
144impl AcOpfInstance {
145    #[must_use]
146    pub fn n_generators(&self) -> usize {
147        self.generators.q.len()
148    }
149
150    #[must_use]
151    pub fn n_branches(&self) -> usize {
152        self.branches.g.len()
153    }
154
155    /// Conventional voltage magnitude start: the case voltage, overwritten by
156    /// each generator's positive setpoint in generator column order (last
157    /// wins), with a non-positive case voltage falling back to 1.0.
158    ///
159    /// The result is not clamped to `[vm_min, vm_max]`; feasibility repair is
160    /// solver preparation and stays downstream.
161    #[must_use]
162    pub fn vm_setpoints(&self) -> Vec<f64> {
163        let mut vm: Vec<f64> = self
164            .buses
165            .vm
166            .iter()
167            .map(|&value| if value > 0.0 { value } else { 1.0 })
168            .collect();
169        for generator in 0..self.n_generators() {
170            let vg = self.generators.vg[generator];
171            if vg > 0.0 {
172                vm[self.generators.bus_of_gen[generator]] = vg;
173            }
174        }
175        vm
176    }
177}
178
179/// Build a matrix free AC OPF instance from an indexed network.
180#[allow(clippy::too_many_lines)]
181pub fn build_ac_opf_instance(
182    case: &IndexedNetwork,
183    options: &AcOpfOptions,
184) -> Result<AcOpfInstance> {
185    case.check_reference_coverage()?;
186    case.network().check_base_mva()?;
187
188    let n_buses = case.n();
189    let base = case.per_unit_base();
190    let (p_scale, y_scale) = options.units.power_scales(base);
191    let (q_scale, c_scale) = options.units.cost_scales(base);
192
193    let mut bus_of_gen = Vec::new();
194    let mut generator_rows = Vec::new();
195    let mut cost_q = Vec::new();
196    let mut cost_c = Vec::new();
197    let mut cost_c0 = Vec::new();
198    let mut pmax = Vec::new();
199    let mut pmin = Vec::new();
200    let mut qmax = Vec::new();
201    let mut qmin = Vec::new();
202    let mut pg = Vec::new();
203    let mut qg = Vec::new();
204    let mut vg = Vec::new();
205
206    for (source_row, generator) in case.in_service_gens() {
207        let bus = case.bus_index(generator.bus).ok_or(Error::UnknownBus {
208            bus_id: generator.bus,
209            element_index: source_row,
210        })?;
211        let cost = generator.cost.as_ref().ok_or(Error::MissingGenCost {
212            gen_index: source_row,
213        })?;
214        let (q_raw, c_raw, c0_raw) =
215            cost.quadratic_with_constant()
216                .ok_or(Error::UnsupportedCostModel {
217                    gen_index: source_row,
218                    model: cost.model,
219                    ncost: cost.ncost,
220                })?;
221        bus_of_gen.push(bus);
222        generator_rows.push(source_row);
223        cost_q.push(q_raw * q_scale);
224        cost_c.push(c_raw * c_scale);
225        cost_c0.push(c0_raw);
226        pmax.push(generator.pmax * p_scale);
227        pmin.push(generator.pmin * p_scale);
228        qmax.push(generator.qmax * p_scale);
229        qmin.push(generator.qmin * p_scale);
230        pg.push(generator.pg * p_scale);
231        qg.push(generator.qg * p_scale);
232        vg.push(generator.vg);
233    }
234    if cost_q.is_empty() {
235        return Err(Error::NoGenerators);
236    }
237
238    let mut g_s: Vec<f64> = case.gs().iter().map(|value| value * p_scale).collect();
239    let mut b_s: Vec<f64> = case.bs().iter().map(|value| value * p_scale).collect();
240
241    let mut from_bus = Vec::new();
242    let mut to_bus = Vec::new();
243    let mut g = Vec::new();
244    let mut b = Vec::new();
245    let mut g_fr = Vec::new();
246    let mut b_fr = Vec::new();
247    let mut g_to = Vec::new();
248    let mut b_to = Vec::new();
249    let mut tap = Vec::new();
250    let mut shift = Vec::new();
251    let mut s_max = Vec::new();
252    let mut angle_min = Vec::new();
253    let mut angle_max = Vec::new();
254    let mut branch_rows = Vec::new();
255    let mut skipped_zero_impedance = Vec::new();
256
257    for (source_row, branch) in case.in_service_branches() {
258        let from = case.bus_index(branch.from).ok_or(Error::UnknownBus {
259            bus_id: branch.from,
260            element_index: source_row,
261        })?;
262        let to = case.bus_index(branch.to).ok_or(Error::UnknownBus {
263            bus_id: branch.to,
264            element_index: source_row,
265        })?;
266        let Some((series_g, series_b)) = branch.series_admittance(source_row)? else {
267            if options.skip_zero_impedance {
268                skipped_zero_impedance.push(source_row);
269                continue;
270            }
271            return Err(Error::ZeroImpedance { row: source_row });
272        };
273        let charging = branch.terminal_charging();
274        if from == to {
275            // A self-loop is not a flow element; its whole pi model stamp
276            // lands on the bus diagonal, exactly as `build_ybus` folds it.
277            // With t = tap·e^{jθ}: Yff + Yft + Ytf + Ytt
278            //   = (y + y_fr)/tap² + (y + y_to) − y·2cos(θ)/tap.
279            let tap = branch.effective_tap();
280            let tap_squared = tap * tap;
281            let cross = 2.0 * case.angle_radians(branch.shift).cos() / tap;
282            g_s[from] += ((series_g + charging.g_fr) / tap_squared + (series_g + charging.g_to)
283                - series_g * cross)
284                * y_scale;
285            b_s[from] += ((series_b + charging.b_fr) / tap_squared + (series_b + charging.b_to)
286                - series_b * cross)
287                * y_scale;
288            continue;
289        }
290        from_bus.push(from);
291        to_bus.push(to);
292        g.push(series_g * y_scale);
293        b.push(series_b * y_scale);
294        g_fr.push(charging.g_fr * y_scale);
295        b_fr.push(charging.b_fr * y_scale);
296        g_to.push(charging.g_to * y_scale);
297        b_to.push(charging.b_to * y_scale);
298        tap.push(branch.effective_tap());
299        shift.push(case.angle_radians(branch.shift));
300        s_max.push(branch.rate_a * p_scale);
301        angle_min.push(case.angle_radians(branch.angmin));
302        angle_max.push(case.angle_radians(branch.angmax));
303        branch_rows.push(source_row);
304    }
305
306    // Dense bus order is the position order of `network().buses`; the view
307    // already holds the star-lowered network when 3-winding expansion ran.
308    let network = case.network();
309    let mut vm_min = Vec::with_capacity(n_buses);
310    let mut vm_max = Vec::with_capacity(n_buses);
311    let mut vm = Vec::with_capacity(n_buses);
312    for bus in &network.buses {
313        vm_min.push(bus.vmin);
314        vm_max.push(bus.vmax);
315        vm.push(bus.vm);
316    }
317
318    Ok(AcOpfInstance {
319        name: case.name().to_owned(),
320        n_buses,
321        n_source_generators: case.generators().len(),
322        n_source_branches: case.branches().len(),
323        base_mva: case.base_mva(),
324        units: options.units,
325        skip_zero_impedance: options.skip_zero_impedance,
326        bus_ids: (0..n_buses).map(|index| case.bus_id(index)).collect(),
327        reference_buses: case.reference_bus_indices(),
328        buses: AcBusData {
329            p_d: case.pd().iter().map(|value| value * p_scale).collect(),
330            q_d: case.qd().iter().map(|value| value * p_scale).collect(),
331            g_s,
332            b_s,
333            vm_min,
334            vm_max,
335            vm,
336        },
337        generators: AcGeneratorData {
338            bus_of_gen,
339            source_rows: generator_rows,
340            q: cost_q,
341            c: cost_c,
342            c0: cost_c0,
343            pmax,
344            pmin,
345            qmax,
346            qmin,
347            pg,
348            qg,
349            vg,
350        },
351        branches: AcBranchData {
352            from_bus,
353            to_bus,
354            g,
355            b,
356            g_fr,
357            b_fr,
358            g_to,
359            b_to,
360            tap,
361            shift,
362            s_max,
363            angle_min,
364            angle_max,
365            source_rows: branch_rows,
366            skipped_zero_impedance,
367        },
368    })
369}