Skip to main content

powerio_matrix/
acopf.rs

1//! AC OPF assembly: the matrix free numerical arrays derived from an
2//! [`AcOpfInstance`], on the branch pi model. 0.9 exposed this surface as
3//! `powerio_prob::build_ac_opf_instance`; it lives here now beside the DC
4//! preparation so every solver formulates over the one shared assembly.
5
6use serde::{Deserialize, Serialize};
7
8use powerio_prob::{AcBusSpecification, AcOpfInstance, AcPfInstance, ReferenceBuses};
9use powerio_tx::{BalancedNetwork, BusId, IndexedNetwork};
10
11use crate::dcopf::{Units, limits, nodal};
12use crate::{AnalysisBranchSource, Error, PiecewiseLinearCost, PreparedObjective, Result};
13
14/// Assembly choices that select the numerical content derived from an AC
15/// instance without changing the instance itself. There is no convention
16/// field: the branch pi model always carries taps, shifts, and charging.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18#[non_exhaustive]
19pub struct AcOpfAssemblyOptions {
20    /// Power and cost scaling of the derived arrays.
21    pub units: Units,
22    /// Skip non-self-loop branches with `r² + x² = 0`. Off by default: zero
23    /// impedance branches are preserved in networks and instances, so
24    /// assembly refuses them until the caller resolves them explicitly
25    /// ([`powerio_prob::merge_zero_impedance_buses`]) or opts into skipping.
26    pub skip_zero_impedance: bool,
27    /// Give a branch with no thermal rating the bound
28    /// [`Branch::synthesize_rate_a`](powerio_tx::Branch::synthesize_rate_a)
29    /// states. If false, `rate_a <= 0` reaches `s_max` as zero, which reads
30    /// as unlimited.
31    pub synthesize_unrated_limits: bool,
32    /// Apply PowerModels' ±60 degree correction to unconstrained or unusable
33    /// branch angle difference intervals in the prepared arrays.
34    pub correct_angle_difference_bounds: bool,
35}
36
37impl Default for AcOpfAssemblyOptions {
38    fn default() -> Self {
39        Self {
40            units: Units::default(),
41            skip_zero_impedance: false,
42            synthesize_unrated_limits: false,
43            correct_angle_difference_bounds: true,
44        }
45    }
46}
47
48impl AcOpfAssemblyOptions {
49    #[must_use]
50    pub const fn with_units(mut self, units: Units) -> Self {
51        self.units = units;
52        self
53    }
54
55    #[must_use]
56    pub const fn with_skip_zero_impedance(mut self, skip: bool) -> Self {
57        self.skip_zero_impedance = skip;
58        self
59    }
60
61    #[must_use]
62    pub const fn with_synthesize_unrated_limits(mut self, synthesize: bool) -> Self {
63        self.synthesize_unrated_limits = synthesize;
64        self
65    }
66
67    #[must_use]
68    pub const fn with_correct_angle_difference_bounds(mut self, correct: bool) -> Self {
69        self.correct_angle_difference_bounds = correct;
70        self
71    }
72}
73
74/// Assembly choices for an AC power flow instance.
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76#[non_exhaustive]
77pub struct AcPfAssemblyOptions {
78    /// Power and admittance scaling of the prepared values.
79    pub units: Units,
80    /// Skip non-self-loop branches with `r² + x² = 0`.
81    pub skip_zero_impedance: bool,
82    /// Apply PowerModels' ±60 degree correction to unconstrained or unusable
83    /// branch angle difference intervals in the prepared arrays.
84    pub correct_angle_difference_bounds: bool,
85}
86
87impl Default for AcPfAssemblyOptions {
88    fn default() -> Self {
89        Self {
90            units: Units::default(),
91            skip_zero_impedance: false,
92            correct_angle_difference_bounds: true,
93        }
94    }
95}
96
97impl AcPfAssemblyOptions {
98    #[must_use]
99    pub const fn with_units(mut self, units: Units) -> Self {
100        self.units = units;
101        self
102    }
103
104    #[must_use]
105    pub const fn with_skip_zero_impedance(mut self, skip: bool) -> Self {
106        self.skip_zero_impedance = skip;
107        self
108    }
109
110    #[must_use]
111    pub const fn with_correct_angle_difference_bounds(mut self, correct: bool) -> Self {
112        self.correct_angle_difference_bounds = correct;
113        self
114    }
115}
116
117/// One AC power flow bus specification in preparation units.
118///
119/// This has the same cases as [`AcBusSpecification`], but active and
120/// reactive power use the preparation's selected [`Units`] and reference
121/// angles are radians. The builder converts the caller's exact case and
122/// values; it never derives a replacement from the network bus type.
123#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
124#[serde(rename_all = "snake_case", tag = "kind")]
125#[non_exhaustive]
126pub enum PreparedAcBusSpecification {
127    Pq { p: f64, q: f64 },
128    Pv { p: f64, vm: f64 },
129    Reference { vm: f64, va: f64 },
130    Isolated,
131}
132
133/// Bus values needed to start and evaluate an AC power flow.
134#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
135#[non_exhaustive]
136pub struct AcPfBusData {
137    /// Nodal reactive demand in the selected power unit. Synthetic transformer
138    /// star buses carry zero.
139    pub q_d: Vec<f64>,
140    /// Nodal shunt conductance in the selected admittance unit.
141    pub g_s: Vec<f64>,
142    /// Nodal shunt susceptance in the selected admittance unit.
143    pub b_s: Vec<f64>,
144    /// Initial voltage magnitude, per unit.
145    pub initial_vm: Vec<f64>,
146    /// Initial voltage angle, radians.
147    pub initial_va: Vec<f64>,
148}
149
150/// Generator data used by PV to PQ reactive limit handling.
151#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
152#[non_exhaustive]
153pub struct AcPfGeneratorData {
154    pub identities: Vec<String>,
155    /// Generator column to dense bus index.
156    pub bus_of_gen: Vec<usize>,
157    /// Generator column to row in the star-lowered analysis network.
158    pub analysis_rows: Vec<usize>,
159    /// Generator column to source generator row.
160    pub source_rows: Vec<Option<usize>>,
161    /// Initial reactive output in the selected power unit.
162    pub qg: Vec<f64>,
163    pub qmax: Vec<f64>,
164    pub qmin: Vec<f64>,
165}
166
167/// Matrix free AC power flow input on the branch pi model.
168#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
169#[non_exhaustive]
170pub struct AcPfPreparation {
171    pub name: String,
172    pub n_buses: usize,
173    pub n_source_generators: usize,
174    pub n_source_branches: usize,
175    pub base_mva: f64,
176    pub units: Units,
177    pub skip_zero_impedance: bool,
178    /// Whether PowerModels' angle difference correction was applied.
179    pub correct_angle_difference_bounds: bool,
180    /// Dense bus index to external bus ID.
181    pub bus_ids: Vec<BusId>,
182    /// Dense bus index to row in the star-lowered analysis network.
183    pub bus_analysis_rows: Vec<usize>,
184    /// Dense bus index to source bus row. A synthetic transformer star bus has
185    /// no source row; a source bus specified as isolated has no dense row.
186    pub bus_source_rows: Vec<Option<usize>>,
187    /// Bus specifications in dense bus order. A synthetic transformer star
188    /// bus is a zero injection PQ junction. Source rows specified as isolated
189    /// are absent from the numerical rows but remain on the `AcPfInstance`.
190    pub specifications: Vec<PreparedAcBusSpecification>,
191    pub reference_buses: ReferenceBuses,
192    pub buses: AcPfBusData,
193    pub generators: AcPfGeneratorData,
194    pub branches: AcBranchData,
195}
196
197impl AcPfPreparation {
198    #[must_use]
199    pub fn n_generators(&self) -> usize {
200        self.generators.qg.len()
201    }
202
203    #[must_use]
204    pub fn n_branches(&self) -> usize {
205        self.branches.g.len()
206    }
207}
208
209/// Bus data in dense bus order.
210#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
211#[non_exhaustive]
212pub struct AcBusData {
213    /// Nodal active demand in the selected power unit.
214    pub p_d: Vec<f64>,
215    /// Nodal reactive demand in the selected power unit.
216    pub q_d: Vec<f64>,
217    /// Nodal shunt conductance in the selected admittance unit. Includes the
218    /// folded pi model stamp of any self-loop branch, matching `calc_admittance_matrix`.
219    pub g_s: Vec<f64>,
220    /// Nodal shunt susceptance in the selected admittance unit. Includes the
221    /// folded pi model stamp of any self-loop branch, matching `calc_admittance_matrix`.
222    pub b_s: Vec<f64>,
223    /// Voltage magnitude lower bound, per unit.
224    pub vm_min: Vec<f64>,
225    /// Voltage magnitude upper bound, per unit.
226    pub vm_max: Vec<f64>,
227    /// Initial voltage magnitude, per unit. The case voltage is used when the
228    /// instance does not supply an override.
229    pub initial_vm: Vec<f64>,
230    /// Initial voltage angle, radians. The case angle is used when the
231    /// instance does not supply an override.
232    pub initial_va: Vec<f64>,
233    /// Whether the instance activates each bus's voltage magnitude bounds.
234    pub voltage_bound_active: Vec<bool>,
235}
236
237/// Branch data in active branch column order.
238#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
239#[non_exhaustive]
240pub struct AcBranchData {
241    /// Stable branch identity aligned with every following column.
242    pub identities: Vec<String>,
243    pub from_bus: Vec<usize>,
244    pub to_bus: Vec<usize>,
245    /// Series conductance `r / (r² + x²)` in the selected admittance unit.
246    pub g: Vec<f64>,
247    /// Series susceptance `−x / (r² + x²)` in the selected admittance unit.
248    pub b: Vec<f64>,
249    /// Charging conductance at the from terminal.
250    pub g_fr: Vec<f64>,
251    /// Charging susceptance at the from terminal.
252    pub b_fr: Vec<f64>,
253    /// Charging conductance at the to terminal.
254    pub g_to: Vec<f64>,
255    /// Charging susceptance at the to terminal.
256    pub b_to: Vec<f64>,
257    /// Tap ratio magnitude; one for a line. Kept separate from `shift` so a
258    /// consumer stamps the complex tap itself.
259    pub tap: Vec<f64>,
260    /// Phase shift in radians.
261    pub shift: Vec<f64>,
262    /// Apparent power limit in the selected power unit. Zero means unlimited.
263    pub s_max: Vec<f64>,
264    /// Branch angle bounds in radians, as the source states them.
265    pub angle_min: Vec<f64>,
266    pub angle_max: Vec<f64>,
267    /// Branch column to row in the star-lowered analysis network.
268    pub analysis_rows: Vec<usize>,
269    /// Source component for each analysis branch column. Lowered transformer
270    /// windings remain mapped to their typed transformer row and winding.
271    pub analysis_sources: Vec<AnalysisBranchSource>,
272    /// Analysis branch rows omitted because `r² + x² = 0`.
273    pub skipped_zero_impedance: Vec<usize>,
274    /// Whether the instance activates each apparent power limit.
275    pub thermal_limit_active: Vec<bool>,
276    /// Whether the instance activates each angle difference bound.
277    pub angle_bound_active: Vec<bool>,
278}
279
280/// Generator data in generator column order.
281#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
282#[non_exhaustive]
283pub struct AcGeneratorData {
284    /// Stable generator identity aligned with every following column.
285    pub identities: Vec<String>,
286    /// Generator column to dense bus index.
287    pub bus_of_gen: Vec<usize>,
288    /// Generator column to row in the analysis network.
289    pub analysis_rows: Vec<usize>,
290    /// Generator column to source generator row.
291    pub source_rows: Vec<Option<usize>>,
292    /// Quadratic objective diagonal in `0.5 * q * p^2 + c * p + c0`.
293    pub q: Vec<f64>,
294    /// Linear objective coefficient.
295    pub c: Vec<f64>,
296    /// Constant objective term. Unscaled in both unit systems: it carries no
297    /// power dimension.
298    pub c0: Vec<f64>,
299    /// Convex piecewise linear costs aligned with the generator columns. A
300    /// present curve is the complete objective term for that generator; its
301    /// `q`, `c`, and `c0` entries are zero.
302    pub piecewise_linear: Vec<Option<PiecewiseLinearCost>>,
303    pub pmax: Vec<f64>,
304    pub pmin: Vec<f64>,
305    pub qmax: Vec<f64>,
306    pub qmin: Vec<f64>,
307    /// Scheduled active output in the selected power unit.
308    pub pg: Vec<f64>,
309    /// Scheduled reactive output in the selected power unit.
310    pub qg: Vec<f64>,
311    /// Voltage magnitude setpoint, per unit; zero when the source has none.
312    pub vg: Vec<f64>,
313    /// Whether the instance activates this generator's capability bounds.
314    pub capability_active: Vec<bool>,
315}
316
317/// Storage data in active storage column order.
318///
319/// Power, energy, ratings, reactive limits, and fixed losses use the
320/// preparation's selected [`Units`]. Efficiencies, impedance, and service
321/// status are dimensionless. Out of service storage and storage attached to
322/// an isolated bus are omitted, matching the other prepared element tables.
323#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
324#[non_exhaustive]
325pub struct AcStorageData {
326    /// Stable storage identity aligned with every following column.
327    pub identities: Vec<String>,
328    /// Storage column to dense bus index.
329    pub bus_of_storage: Vec<usize>,
330    /// Storage column to source storage row.
331    pub source_rows: Vec<usize>,
332    pub p: Vec<f64>,
333    pub q: Vec<f64>,
334    pub energy: Vec<f64>,
335    pub energy_rating: Vec<f64>,
336    pub charge_rating: Vec<f64>,
337    pub discharge_rating: Vec<f64>,
338    pub charge_efficiency: Vec<f64>,
339    pub discharge_efficiency: Vec<f64>,
340    pub s_max: Vec<f64>,
341    pub qmin: Vec<f64>,
342    pub qmax: Vec<f64>,
343    pub r: Vec<f64>,
344    pub x: Vec<f64>,
345    pub p_loss: Vec<f64>,
346    pub q_loss: Vec<f64>,
347    pub in_service: Vec<bool>,
348}
349
350/// Generator data in dense bus order, aggregated over the generators at each
351/// bus. See [`AcOpfPreparation::calc_nodal_generator_data`].
352#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
353#[non_exhaustive]
354pub struct NodalAcGeneratorData {
355    pub q: Vec<f64>,
356    pub c: Vec<f64>,
357    pub c0: Vec<f64>,
358    pub pmax: Vec<f64>,
359    pub pmin: Vec<f64>,
360    pub qmax: Vec<f64>,
361    pub qmin: Vec<f64>,
362    /// Which buses host a generator. A bus without one has a zero range and a
363    /// zero cost, which a formulation must not read as a free generator. A
364    /// reactive limit loop reads it to tell a bus that holds its voltage from
365    /// one that cannot.
366    pub has_gen: Vec<bool>,
367}
368
369/// Matrix free AC OPF input data on the branch pi model.
370///
371/// Units follow [`Units`]. Under [`Units::PerUnit`], powers are per unit on
372/// `base_mva` and admittances are per unit on the system base. Under
373/// [`Units::Native`], powers stay in MW/MVAr and every admittance vector is
374/// scaled by `base_mva`, so power computed from admittances and per unit
375/// voltages lands in MW/MVAr. Voltage magnitudes are per unit and angles are
376/// radians in both systems. Relaxations of AC OPF, the SOC forms included,
377/// consume this same preparation; the relaxation is a formulation choice
378/// made downstream.
379#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
380#[non_exhaustive]
381pub struct AcOpfPreparation {
382    pub name: String,
383    pub n_buses: usize,
384    pub n_source_generators: usize,
385    pub n_source_branches: usize,
386    pub base_mva: f64,
387    pub units: Units,
388    /// The objective represented by the generator cost columns.
389    pub objective: PreparedObjective,
390    pub skip_zero_impedance: bool,
391    /// Whether absent source ratings were replaced with synthesized limits.
392    pub synthesize_unrated_limits: bool,
393    /// Whether PowerModels' angle difference correction was applied.
394    pub correct_angle_difference_bounds: bool,
395    /// Dense bus index to external bus ID.
396    pub bus_ids: Vec<BusId>,
397    /// Dense bus index to row in the star-lowered analysis network.
398    pub bus_analysis_rows: Vec<usize>,
399    /// Dense bus index to source bus row. A synthetic star bus has no source
400    /// row; an explicitly isolated source bus has no dense row here.
401    pub bus_source_rows: Vec<Option<usize>>,
402    pub reference_buses: ReferenceBuses,
403    pub buses: AcBusData,
404    pub generators: AcGeneratorData,
405    pub storage: AcStorageData,
406    pub branches: AcBranchData,
407}
408
409impl AcOpfPreparation {
410    #[must_use]
411    pub fn n_generators(&self) -> usize {
412        self.generators.q.len()
413    }
414
415    #[must_use]
416    pub fn n_branches(&self) -> usize {
417        self.branches.g.len()
418    }
419
420    #[must_use]
421    pub fn n_storage(&self) -> usize {
422        self.storage.identities.len()
423    }
424
425    /// Project generator cost and bounds to bus space.
426    ///
427    /// The bounds at a bus are the sum of the generator bounds, which is the
428    /// range the bus total can reach. The cost curves at a bus combine by the
429    /// parallel rule `q = 1 / Σ(1/qᵢ)`, the curve that the least cost split of
430    /// the bus total follows. That combination is an approximation: it agrees
431    /// with generator space only while the split stays inside the bound of
432    /// each generator. A bus with one generator keeps that generator's own
433    /// coefficients.
434    pub fn calc_nodal_generator_data(&self) -> Result<NodalAcGeneratorData> {
435        let n = self.n_buses;
436        let generators = &self.generators;
437        if let Some(gen_index) = generators.piecewise_linear.iter().position(Option::is_some) {
438            return Err(Error::PiecewiseNodalCost { gen_index });
439        }
440        let bus_of_gen = &generators.bus_of_gen;
441        let costs =
442            nodal::combine_costs(n, bus_of_gen, &generators.q, &generators.c, &generators.c0);
443        Ok(NodalAcGeneratorData {
444            q: costs.q,
445            c: costs.c,
446            c0: costs.c0,
447            pmax: nodal::sum_by_bus(n, bus_of_gen, &generators.pmax),
448            pmin: nodal::sum_by_bus(n, bus_of_gen, &generators.pmin),
449            qmax: nodal::sum_by_bus(n, bus_of_gen, &generators.qmax),
450            qmin: nodal::sum_by_bus(n, bus_of_gen, &generators.qmin),
451            has_gen: nodal::buses_with_generators(n, bus_of_gen),
452        })
453    }
454
455    /// Conventional voltage magnitude start: the case voltage, overwritten by
456    /// each generator's positive setpoint in generator column order (last
457    /// wins), with a non-positive case voltage falling back to 1.0.
458    ///
459    /// The result is not clamped to `[vm_min, vm_max]`; feasibility repair is
460    /// solver preparation and stays downstream.
461    #[must_use]
462    pub fn calc_vm_setpoints(&self) -> Vec<f64> {
463        let mut vm: Vec<f64> = self
464            .buses
465            .initial_vm
466            .iter()
467            .map(|&value| if value > 0.0 { value } else { 1.0 })
468            .collect();
469        for generator in 0..self.n_generators() {
470            let vg = self.generators.vg[generator];
471            if vg > 0.0 {
472                vm[self.generators.bus_of_gen[generator]] = vg;
473            }
474        }
475        vm
476    }
477}
478
479/// Derive the complete matrix free AC OPF arrays from the instance: demand,
480/// shunt, and voltage columns per bus, the full pi model per active branch,
481/// and generator costs, bounds, and schedules with their source row mapping.
482/// The AC counterpart of [`build_dc_opf_preparation`](crate::build_dc_opf_preparation).
483///
484/// # Errors
485/// A network the pi model cannot assemble: missing reference coverage, an
486/// unresolved zero impedance branch, or an unusable cost curve.
487pub fn build_ac_opf_preparation(
488    instance: &AcOpfInstance,
489    options: &AcOpfAssemblyOptions,
490) -> Result<AcOpfPreparation> {
491    let view = IndexedNetwork::new(instance.network());
492    let objective = crate::opf::compile_objective(instance.objective())?;
493    let mut preparation = preparation_from_view(&view, *options, objective)?;
494    apply_instance_semantics(&mut preparation, instance.network(), instance.constraints())?;
495    if let Some(point) = instance.initial_point() {
496        let (power_scale, _) = options.units.power_scales(preparation.base_mva);
497        for (dense, bus) in preparation.bus_ids.iter().copied().enumerate() {
498            if let Some(value) = point.bus_voltage_magnitude(bus) {
499                preparation.buses.initial_vm[dense] = value;
500            }
501            if let Some(value) = point.bus_voltage_angle(bus) {
502                preparation.buses.initial_va[dense] = value;
503            }
504        }
505        for (generator, identity) in preparation.generators.identities.iter().enumerate() {
506            if let Some(value) = point.generator_active_power(identity) {
507                preparation.generators.pg[generator] = value * power_scale;
508            }
509            if let Some(value) = point.generator_reactive_power(identity) {
510                preparation.generators.qg[generator] = value * power_scale;
511            }
512            if let Some(value) = point.generator_voltage_setpoint(identity) {
513                preparation.generators.vg[generator] = value;
514            }
515        }
516    }
517    Ok(preparation)
518}
519
520/// Derive the complete matrix free AC power flow arrays from an instance.
521/// The caller's bus specifications remain authoritative. Network bus types
522/// are used only by `AcPfInstance::from_network` when it creates those
523/// specifications; this function does not infer them again.
524///
525/// # Errors
526/// A specification layout that leaves an energized component without a
527/// reference bus, or a branch pi model that cannot be assembled.
528pub fn build_ac_pf_preparation(
529    instance: &AcPfInstance,
530    options: &AcPfAssemblyOptions,
531) -> Result<AcPfPreparation> {
532    let source = instance.network();
533    let mut analysis_network = source.clone();
534    for (bus, specification) in analysis_network
535        .buses_mut()
536        .iter_mut()
537        .zip(instance.specifications())
538    {
539        bus.kind = match specification {
540            AcBusSpecification::Pq { .. } => powerio_tx::BusType::Pq,
541            AcBusSpecification::Pv { .. } => powerio_tx::BusType::Pv,
542            AcBusSpecification::Reference { .. } => powerio_tx::BusType::Ref,
543            AcBusSpecification::Isolated => powerio_tx::BusType::Isolated,
544            _ => return Err(Error::UnsupportedAcPfSpecification),
545        };
546    }
547
548    let view = IndexedNetwork::new(&analysis_network);
549    let mut common = preparation_from_view(
550        &view,
551        AcOpfAssemblyOptions {
552            units: options.units,
553            skip_zero_impedance: options.skip_zero_impedance,
554            synthesize_unrated_limits: false,
555            correct_angle_difference_bounds: options.correct_angle_difference_bounds,
556        },
557        PreparedObjective::Feasibility,
558    )?;
559    apply_source_mappings(&mut common, source);
560
561    let (power_scale, _) = options.units.power_scales(view.per_unit_base());
562    let specifications = common
563        .bus_source_rows
564        .iter()
565        .map(|source_row| match source_row {
566            Some(row) => prepare_bus_specification(
567                instance.specifications()[*row],
568                power_scale,
569                source.is_normalized(),
570            ),
571            None => Ok(PreparedAcBusSpecification::Pq { p: 0.0, q: 0.0 }),
572        })
573        .collect::<Result<Vec<_>>>()?;
574
575    let mut initial_vm = common.buses.initial_vm.clone();
576    let mut initial_va = common
577        .bus_analysis_rows
578        .iter()
579        .map(|&row| view.to_radians(view.network().buses()[row].va))
580        .collect::<Vec<_>>();
581    if let Some(point) = instance.initial_point() {
582        for (dense, bus) in common.bus_ids.iter().copied().enumerate() {
583            if common.bus_source_rows[dense].is_none() {
584                continue;
585            }
586            if let Some(value) = point.bus_voltage_magnitude(bus) {
587                initial_vm[dense] = value;
588            }
589            if let Some(value) = point.bus_voltage_angle(bus) {
590                initial_va[dense] = value;
591            }
592        }
593    }
594
595    Ok(AcPfPreparation {
596        name: common.name,
597        n_buses: common.n_buses,
598        n_source_generators: common.n_source_generators,
599        n_source_branches: common.n_source_branches,
600        base_mva: common.base_mva,
601        units: common.units,
602        skip_zero_impedance: common.skip_zero_impedance,
603        correct_angle_difference_bounds: common.correct_angle_difference_bounds,
604        bus_ids: common.bus_ids,
605        bus_analysis_rows: common.bus_analysis_rows,
606        bus_source_rows: common.bus_source_rows,
607        specifications,
608        reference_buses: common.reference_buses,
609        buses: AcPfBusData {
610            q_d: common.buses.q_d,
611            g_s: common.buses.g_s,
612            b_s: common.buses.b_s,
613            initial_vm,
614            initial_va,
615        },
616        generators: AcPfGeneratorData {
617            identities: common.generators.identities,
618            bus_of_gen: common.generators.bus_of_gen,
619            analysis_rows: common.generators.analysis_rows,
620            source_rows: common.generators.source_rows,
621            qg: common.generators.qg,
622            qmax: common.generators.qmax,
623            qmin: common.generators.qmin,
624        },
625        branches: common.branches,
626    })
627}
628
629fn prepare_bus_specification(
630    specification: AcBusSpecification,
631    power_scale: f64,
632    source_is_normalized: bool,
633) -> Result<PreparedAcBusSpecification> {
634    Ok(match specification {
635        AcBusSpecification::Pq { p, q } => PreparedAcBusSpecification::Pq {
636            p: p * power_scale,
637            q: q * power_scale,
638        },
639        AcBusSpecification::Pv { p, vm } => PreparedAcBusSpecification::Pv {
640            p: p * power_scale,
641            vm,
642        },
643        AcBusSpecification::Reference { vm, va } => PreparedAcBusSpecification::Reference {
644            vm,
645            va: if source_is_normalized {
646                va
647            } else {
648                va.to_radians()
649            },
650        },
651        AcBusSpecification::Isolated => PreparedAcBusSpecification::Isolated,
652        _ => return Err(Error::UnsupportedAcPfSpecification),
653    })
654}
655
656/// Build the matrix free AC OPF arrays from an indexed network view.
657#[allow(clippy::too_many_lines)]
658fn preparation_from_view(
659    case: &IndexedNetwork,
660    options: AcOpfAssemblyOptions,
661    objective: PreparedObjective,
662) -> Result<AcOpfPreparation> {
663    case.network().check_base_mva()?;
664
665    let active_buses = crate::opf::active_bus_index(case)?;
666    let n_buses = active_buses.analysis_rows.len();
667    let base = case.per_unit_base();
668    let (p_scale, y_scale) = options.units.power_scales(base);
669    let thermal = limits::ThermalLimits {
670        synthesize_unrated: options.synthesize_unrated_limits,
671        power_scale: p_scale,
672        admittance_scale: y_scale,
673    };
674    let (q_scale, c_scale) = options.units.cost_scales(base);
675
676    let mut bus_of_gen = Vec::new();
677    let mut generator_identities = Vec::new();
678    let mut generator_rows = Vec::new();
679    let mut cost_q = Vec::new();
680    let mut cost_c = Vec::new();
681    let mut cost_c0 = Vec::new();
682    let mut piecewise_linear = Vec::new();
683    let mut pmax = Vec::new();
684    let mut pmin = Vec::new();
685    let mut qmax = Vec::new();
686    let mut qmin = Vec::new();
687    let mut pg = Vec::new();
688    let mut qg = Vec::new();
689    let mut vg = Vec::new();
690
691    for (source_row, generator) in case.in_service_gens() {
692        let analysis_bus = case
693            .bus_index(generator.bus)
694            .ok_or(powerio_tx::Error::UnknownBus {
695                bus_id: generator.bus,
696                element_index: source_row,
697            })?;
698        let Some(bus) = active_buses.dense_by_analysis[analysis_bus] else {
699            continue;
700        };
701        let terms = match objective {
702            PreparedObjective::Feasibility => nodal::GeneratorCostTerms {
703                q: 0.0,
704                c: 0.0,
705                c0: 0.0,
706                piecewise_linear: None,
707            },
708            PreparedObjective::NetworkGeneratorCost => {
709                let cost = generator
710                    .cost
711                    .as_ref()
712                    .ok_or(powerio_tx::Error::MissingGenCost {
713                        gen_index: source_row,
714                    })?;
715                nodal::generator_cost_terms(cost, source_row, p_scale)?
716            }
717        };
718        generator_identities.push(crate::opf::row_identity(
719            generator.uid.as_deref(),
720            "generators",
721            source_row,
722        ));
723        bus_of_gen.push(bus);
724        generator_rows.push(source_row);
725        cost_q.push(terms.q * q_scale);
726        cost_c.push(terms.c * c_scale);
727        cost_c0.push(terms.c0);
728        piecewise_linear.push(terms.piecewise_linear);
729        pmax.push(generator.pmax * p_scale);
730        pmin.push(generator.pmin * p_scale);
731        qmax.push(generator.qmax * p_scale);
732        qmin.push(generator.qmin * p_scale);
733        pg.push(generator.pg * p_scale);
734        qg.push(generator.qg * p_scale);
735        vg.push(generator.vg);
736    }
737
738    let mut storage_identities = Vec::new();
739    let mut bus_of_storage = Vec::new();
740    let mut storage_source_rows = Vec::new();
741    let mut storage_p = Vec::new();
742    let mut storage_q = Vec::new();
743    let mut storage_energy = Vec::new();
744    let mut storage_energy_rating = Vec::new();
745    let mut storage_charge_rating = Vec::new();
746    let mut storage_discharge_rating = Vec::new();
747    let mut storage_charge_efficiency = Vec::new();
748    let mut storage_discharge_efficiency = Vec::new();
749    let mut storage_s_max = Vec::new();
750    let mut storage_qmin = Vec::new();
751    let mut storage_qmax = Vec::new();
752    let mut storage_r = Vec::new();
753    let mut storage_x = Vec::new();
754    let mut storage_p_loss = Vec::new();
755    let mut storage_q_loss = Vec::new();
756    let mut storage_in_service = Vec::new();
757    for (source_row, storage) in case.network().storage().iter().enumerate() {
758        if !storage.in_service {
759            continue;
760        }
761        let analysis_bus = case
762            .bus_index(storage.bus)
763            .ok_or(powerio_tx::Error::UnknownBus {
764                bus_id: storage.bus,
765                element_index: source_row,
766            })?;
767        let Some(bus) = active_buses.dense_by_analysis[analysis_bus] else {
768            continue;
769        };
770        storage_identities.push(crate::opf::row_identity(
771            storage.uid.as_deref(),
772            "storage",
773            source_row,
774        ));
775        bus_of_storage.push(bus);
776        storage_source_rows.push(source_row);
777        storage_p.push(storage.ps * p_scale);
778        storage_q.push(storage.qs * p_scale);
779        storage_energy.push(storage.energy * p_scale);
780        storage_energy_rating.push(storage.energy_rating * p_scale);
781        storage_charge_rating.push(storage.charge_rating * p_scale);
782        storage_discharge_rating.push(storage.discharge_rating * p_scale);
783        storage_charge_efficiency.push(storage.charge_efficiency);
784        storage_discharge_efficiency.push(storage.discharge_efficiency);
785        storage_s_max.push(storage.thermal_rating * p_scale);
786        storage_qmin.push(storage.qmin * p_scale);
787        storage_qmax.push(storage.qmax * p_scale);
788        storage_r.push(storage.r);
789        storage_x.push(storage.x);
790        storage_p_loss.push(storage.p_loss * p_scale);
791        storage_q_loss.push(storage.q_loss * p_scale);
792        storage_in_service.push(storage.in_service);
793    }
794    let mut g_s: Vec<f64> = active_buses
795        .analysis_rows
796        .iter()
797        .map(|&row| case.gs()[row] * p_scale)
798        .collect();
799    let mut b_s: Vec<f64> = active_buses
800        .analysis_rows
801        .iter()
802        .map(|&row| case.bs()[row] * p_scale)
803        .collect();
804
805    let mut from_bus = Vec::new();
806    let mut branch_identities = Vec::new();
807    let mut to_bus = Vec::new();
808    let mut g = Vec::new();
809    let mut b = Vec::new();
810    let mut g_fr = Vec::new();
811    let mut b_fr = Vec::new();
812    let mut g_to = Vec::new();
813    let mut b_to = Vec::new();
814    let mut tap = Vec::new();
815    let mut shift = Vec::new();
816    let mut s_max = Vec::new();
817    let mut angle_min = Vec::new();
818    let mut angle_max = Vec::new();
819    let mut branch_rows = Vec::new();
820    let mut skipped_zero_impedance = Vec::new();
821    // Dense bus order is the position order of `network().buses()`; the view
822    // already holds the star-lowered network when 3-winding expansion ran.
823    let network = case.network();
824
825    for (source_row, branch) in case.in_service_branches() {
826        let from_analysis = case
827            .bus_index(branch.from)
828            .ok_or(powerio_tx::Error::UnknownBus {
829                bus_id: branch.from,
830                element_index: source_row,
831            })?;
832        let to_analysis = case
833            .bus_index(branch.to)
834            .ok_or(powerio_tx::Error::UnknownBus {
835                bus_id: branch.to,
836                element_index: source_row,
837            })?;
838        let (Some(from), Some(to)) = (
839            active_buses.dense_by_analysis[from_analysis],
840            active_buses.dense_by_analysis[to_analysis],
841        ) else {
842            continue;
843        };
844        let Some((series_g, series_b)) = branch.calc_series_admittance(source_row)? else {
845            if options.skip_zero_impedance {
846                skipped_zero_impedance.push(source_row);
847                continue;
848            }
849            return Err(powerio_tx::Error::ZeroImpedance { row: source_row }.into());
850        };
851        let charging = branch.calc_terminal_charging();
852        if from == to {
853            // A self-loop is not a flow element; its whole pi model stamp
854            // lands on the bus diagonal, exactly as `calc_admittance_matrix` folds it.
855            // With t = tap·e^{jθ}: Yff + Yft + Ytf + Ytt
856            //   = (y + y_fr)/tap² + (y + y_to) − y·2cos(θ)/tap.
857            let tap = branch.calc_divisible_tap(source_row)?;
858            let tap_squared = tap * tap;
859            let cross = 2.0 * case.to_radians(branch.shift).cos() / tap;
860            g_s[from] += ((series_g + charging.g_fr) / tap_squared + (series_g + charging.g_to)
861                - series_g * cross)
862                * y_scale;
863            b_s[from] += ((series_b + charging.b_fr) / tap_squared + (series_b + charging.b_to)
864                - series_b * cross)
865                * y_scale;
866            continue;
867        }
868        from_bus.push(from);
869        branch_identities.push(crate::opf::row_identity(
870            branch.uid.as_deref(),
871            "branches",
872            source_row,
873        ));
874        to_bus.push(to);
875        g.push(series_g * y_scale);
876        b.push(series_b * y_scale);
877        g_fr.push(charging.g_fr * y_scale);
878        b_fr.push(charging.b_fr * y_scale);
879        g_to.push(charging.g_to * y_scale);
880        b_to.push(charging.b_to * y_scale);
881        let source_amin = case.to_radians(branch.angmin);
882        let source_amax = case.to_radians(branch.angmax);
883        let (amin, amax) = if options.correct_angle_difference_bounds {
884            powerio_tx::correct_angle_difference_bounds(source_amin, source_amax)
885        } else {
886            (source_amin, source_amax)
887        };
888        tap.push(branch.calc_divisible_tap(source_row)?);
889        shift.push(case.to_radians(branch.shift));
890        s_max.push(thermal.of(
891            branch,
892            source_amin,
893            source_amax,
894            &network.buses()[from_analysis],
895            &network.buses()[to_analysis],
896        ));
897        angle_min.push(amin);
898        angle_max.push(amax);
899        branch_rows.push(source_row);
900    }
901
902    let mut vm_min = Vec::with_capacity(n_buses);
903    let mut vm_max = Vec::with_capacity(n_buses);
904    let mut initial_vm = Vec::with_capacity(n_buses);
905    let mut initial_va = Vec::with_capacity(n_buses);
906    for &analysis_row in &active_buses.analysis_rows {
907        let bus = &network.buses()[analysis_row];
908        vm_min.push(bus.vmin);
909        vm_max.push(bus.vmax);
910        initial_vm.push(bus.vm);
911        initial_va.push(case.to_radians(bus.va));
912    }
913    let n_active_generators = cost_q.len();
914    let n_active_branches = g.len();
915    let p_d = active_buses
916        .analysis_rows
917        .iter()
918        .map(|&row| case.pd()[row] * p_scale)
919        .collect();
920    let q_d = active_buses
921        .analysis_rows
922        .iter()
923        .map(|&row| case.qd()[row] * p_scale)
924        .collect();
925    let bus_analysis_rows = active_buses.analysis_rows;
926    let bus_source_rows = bus_analysis_rows.iter().copied().map(Some).collect();
927    Ok(AcOpfPreparation {
928        name: case.name().to_owned(),
929        n_buses,
930        n_source_generators: case.generators().len(),
931        n_source_branches: case.branches().len(),
932        base_mva: case.base_mva(),
933        units: options.units,
934        objective,
935        skip_zero_impedance: options.skip_zero_impedance,
936        synthesize_unrated_limits: options.synthesize_unrated_limits,
937        correct_angle_difference_bounds: options.correct_angle_difference_bounds,
938        bus_ids: active_buses.bus_ids,
939        bus_analysis_rows,
940        bus_source_rows,
941        reference_buses: active_buses.reference_buses,
942        buses: AcBusData {
943            p_d,
944            q_d,
945            g_s,
946            b_s,
947            vm_min,
948            vm_max,
949            initial_vm,
950            initial_va,
951            voltage_bound_active: vec![true; n_buses],
952        },
953        generators: AcGeneratorData {
954            identities: generator_identities,
955            bus_of_gen,
956            analysis_rows: generator_rows.clone(),
957            source_rows: generator_rows.into_iter().map(Some).collect(),
958            q: cost_q,
959            c: cost_c,
960            c0: cost_c0,
961            piecewise_linear,
962            pmax,
963            pmin,
964            qmax,
965            qmin,
966            pg,
967            qg,
968            vg,
969            capability_active: vec![true; n_active_generators],
970        },
971        storage: AcStorageData {
972            identities: storage_identities,
973            bus_of_storage,
974            source_rows: storage_source_rows,
975            p: storage_p,
976            q: storage_q,
977            energy: storage_energy,
978            energy_rating: storage_energy_rating,
979            charge_rating: storage_charge_rating,
980            discharge_rating: storage_discharge_rating,
981            charge_efficiency: storage_charge_efficiency,
982            discharge_efficiency: storage_discharge_efficiency,
983            s_max: storage_s_max,
984            qmin: storage_qmin,
985            qmax: storage_qmax,
986            r: storage_r,
987            x: storage_x,
988            p_loss: storage_p_loss,
989            q_loss: storage_q_loss,
990            in_service: storage_in_service,
991        },
992        branches: AcBranchData {
993            identities: branch_identities,
994            from_bus,
995            to_bus,
996            g,
997            b,
998            g_fr,
999            b_fr,
1000            g_to,
1001            b_to,
1002            tap,
1003            shift,
1004            s_max,
1005            angle_min,
1006            angle_max,
1007            analysis_rows: branch_rows.clone(),
1008            analysis_sources: branch_rows
1009                .iter()
1010                .copied()
1011                .map(|row| AnalysisBranchSource::Branch { row })
1012                .collect(),
1013            skipped_zero_impedance,
1014            thermal_limit_active: vec![true; n_active_branches],
1015            angle_bound_active: vec![true; n_active_branches],
1016        },
1017    })
1018}
1019
1020fn apply_instance_semantics(
1021    preparation: &mut AcOpfPreparation,
1022    source: &BalancedNetwork,
1023    constraints: &powerio_prob::ActiveConstraints,
1024) -> Result<()> {
1025    let source_generator_ids: Vec<String> = source
1026        .generators()
1027        .iter()
1028        .enumerate()
1029        .map(|(row, generator)| {
1030            crate::opf::row_identity(generator.uid.as_deref(), "generators", row)
1031        })
1032        .collect();
1033    let source_branch_ids: Vec<String> = source
1034        .branches()
1035        .iter()
1036        .enumerate()
1037        .map(|(row, branch)| crate::opf::row_identity(branch.uid.as_deref(), "branches", row))
1038        .collect();
1039    let mut analysis_bus_ids: Vec<String> = source
1040        .buses()
1041        .iter()
1042        .map(|bus| bus.id.to_string())
1043        .collect();
1044    for bus in &preparation.bus_ids {
1045        let identity = bus.to_string();
1046        if !analysis_bus_ids.iter().any(|known| known == &identity) {
1047            analysis_bus_ids.push(identity);
1048        }
1049    }
1050
1051    preparation.buses.voltage_bound_active = crate::opf::constraint_mask(
1052        "bus voltage bounds",
1053        &constraints.voltage_bounds,
1054        &analysis_bus_ids,
1055        &preparation
1056            .bus_ids
1057            .iter()
1058            .map(ToString::to_string)
1059            .collect::<Vec<_>>(),
1060    )?;
1061    preparation.generators.capability_active = crate::opf::constraint_mask(
1062        "generator capability",
1063        &constraints.generator_capability,
1064        &source_generator_ids,
1065        &preparation.generators.identities,
1066    )?;
1067    let mut analysis_branch_ids = source_branch_ids;
1068    analysis_branch_ids.extend(
1069        preparation
1070            .branches
1071            .identities
1072            .iter()
1073            .zip(&preparation.branches.analysis_rows)
1074            .filter(|(_, row)| **row >= source.branches().len())
1075            .map(|(identity, _)| identity.clone()),
1076    );
1077    preparation.branches.thermal_limit_active = crate::opf::constraint_mask(
1078        "branch thermal limits",
1079        &constraints.thermal_limits,
1080        &analysis_branch_ids,
1081        &preparation.branches.identities,
1082    )?;
1083    for (active, limit) in preparation
1084        .branches
1085        .thermal_limit_active
1086        .iter_mut()
1087        .zip(&preparation.branches.s_max)
1088    {
1089        *active &= *limit > 0.0;
1090    }
1091    preparation.branches.angle_bound_active = crate::opf::constraint_mask(
1092        "branch angle bounds",
1093        &constraints.angle_bounds,
1094        &analysis_branch_ids,
1095        &preparation.branches.identities,
1096    )?;
1097
1098    apply_source_mappings(preparation, source);
1099    Ok(())
1100}
1101
1102fn apply_source_mappings(preparation: &mut AcOpfPreparation, source: &BalancedNetwork) {
1103    preparation.n_source_generators = source.generators().len();
1104    preparation.n_source_branches = source.branches().len();
1105    preparation.bus_source_rows = preparation
1106        .bus_analysis_rows
1107        .iter()
1108        .map(|&row| (row < source.buses().len()).then_some(row))
1109        .collect();
1110    preparation.generators.source_rows = preparation
1111        .generators
1112        .analysis_rows
1113        .iter()
1114        .map(|&row| (row < source.generators().len()).then_some(row))
1115        .collect();
1116    let analysis_sources = crate::opf::analysis_branch_sources(source);
1117    preparation.branches.analysis_sources = preparation
1118        .branches
1119        .analysis_rows
1120        .iter()
1121        .map(|&row| analysis_sources[row])
1122        .collect();
1123}