use serde::{Deserialize, Serialize};
use powerio::{BusId, IndexedNetwork};
use crate::{Error, Result};
use crate::{ReferenceBuses, Units, limits, nodal};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct AcOpfOptions {
pub units: Units,
pub skip_zero_impedance: bool,
pub synthesize_unrated_limits: bool,
}
impl Default for AcOpfOptions {
fn default() -> Self {
Self {
units: Units::default(),
skip_zero_impedance: true,
synthesize_unrated_limits: false,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct AcBusData {
pub p_d: Vec<f64>,
pub q_d: Vec<f64>,
pub g_s: Vec<f64>,
pub b_s: Vec<f64>,
pub vm_min: Vec<f64>,
pub vm_max: Vec<f64>,
pub vm: Vec<f64>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct AcBranchData {
pub from_bus: Vec<usize>,
pub to_bus: Vec<usize>,
pub g: Vec<f64>,
pub b: Vec<f64>,
pub g_fr: Vec<f64>,
pub b_fr: Vec<f64>,
pub g_to: Vec<f64>,
pub b_to: Vec<f64>,
pub tap: Vec<f64>,
pub shift: Vec<f64>,
pub s_max: Vec<f64>,
pub angle_min: Vec<f64>,
pub angle_max: Vec<f64>,
pub source_rows: Vec<usize>,
pub skipped_zero_impedance: Vec<usize>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct AcGeneratorData {
pub bus_of_gen: Vec<usize>,
pub source_rows: Vec<usize>,
pub q: Vec<f64>,
pub c: Vec<f64>,
pub c0: Vec<f64>,
pub pmax: Vec<f64>,
pub pmin: Vec<f64>,
pub qmax: Vec<f64>,
pub qmin: Vec<f64>,
pub pg: Vec<f64>,
pub qg: Vec<f64>,
pub vg: Vec<f64>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct NodalAcGeneratorData {
pub q: Vec<f64>,
pub c: Vec<f64>,
pub c0: Vec<f64>,
pub pmax: Vec<f64>,
pub pmin: Vec<f64>,
pub qmax: Vec<f64>,
pub qmin: Vec<f64>,
pub has_gen: Vec<bool>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct AcOpfInstance {
pub name: String,
pub n_buses: usize,
pub n_source_generators: usize,
pub n_source_branches: usize,
pub base_mva: f64,
pub units: Units,
pub skip_zero_impedance: bool,
pub bus_ids: Vec<BusId>,
pub reference_buses: ReferenceBuses,
pub buses: AcBusData,
pub generators: AcGeneratorData,
pub branches: AcBranchData,
}
impl AcOpfInstance {
#[must_use]
pub fn n_generators(&self) -> usize {
self.generators.q.len()
}
#[must_use]
pub fn n_branches(&self) -> usize {
self.branches.g.len()
}
#[must_use]
pub fn nodal_generator_data(&self) -> NodalAcGeneratorData {
let n = self.n_buses;
let generators = &self.generators;
let bus_of_gen = &generators.bus_of_gen;
let costs =
nodal::combine_costs(n, bus_of_gen, &generators.q, &generators.c, &generators.c0);
NodalAcGeneratorData {
q: costs.q,
c: costs.c,
c0: costs.c0,
pmax: nodal::sum_by_bus(n, bus_of_gen, &generators.pmax),
pmin: nodal::sum_by_bus(n, bus_of_gen, &generators.pmin),
qmax: nodal::sum_by_bus(n, bus_of_gen, &generators.qmax),
qmin: nodal::sum_by_bus(n, bus_of_gen, &generators.qmin),
has_gen: nodal::buses_with_generators(n, bus_of_gen),
}
}
#[must_use]
pub fn vm_setpoints(&self) -> Vec<f64> {
let mut vm: Vec<f64> = self
.buses
.vm
.iter()
.map(|&value| if value > 0.0 { value } else { 1.0 })
.collect();
for generator in 0..self.n_generators() {
let vg = self.generators.vg[generator];
if vg > 0.0 {
vm[self.generators.bus_of_gen[generator]] = vg;
}
}
vm
}
}
#[allow(clippy::too_many_lines)]
pub fn build_ac_opf_instance(
case: &IndexedNetwork,
options: &AcOpfOptions,
) -> Result<AcOpfInstance> {
case.check_reference_coverage()?;
case.network().check_base_mva()?;
let n_buses = case.n();
let base = case.per_unit_base();
let (p_scale, y_scale) = options.units.power_scales(base);
let thermal = limits::ThermalLimits {
synthesize_unrated: options.synthesize_unrated_limits,
power_scale: p_scale,
admittance_scale: y_scale,
};
let (q_scale, c_scale) = options.units.cost_scales(base);
let mut bus_of_gen = Vec::new();
let mut generator_rows = Vec::new();
let mut cost_q = Vec::new();
let mut cost_c = Vec::new();
let mut cost_c0 = Vec::new();
let mut pmax = Vec::new();
let mut pmin = Vec::new();
let mut qmax = Vec::new();
let mut qmin = Vec::new();
let mut pg = Vec::new();
let mut qg = Vec::new();
let mut vg = Vec::new();
for (source_row, generator) in case.in_service_gens() {
let bus = case
.bus_index(generator.bus)
.ok_or(powerio::Error::UnknownBus {
bus_id: generator.bus,
element_index: source_row,
})?;
let cost = generator
.cost
.as_ref()
.ok_or(powerio::Error::MissingGenCost {
gen_index: source_row,
})?;
let (q_raw, c_raw, c0_raw) = nodal::quadratic_terms(cost, source_row)?;
bus_of_gen.push(bus);
generator_rows.push(source_row);
cost_q.push(q_raw * q_scale);
cost_c.push(c_raw * c_scale);
cost_c0.push(c0_raw);
pmax.push(generator.pmax * p_scale);
pmin.push(generator.pmin * p_scale);
qmax.push(generator.qmax * p_scale);
qmin.push(generator.qmin * p_scale);
pg.push(generator.pg * p_scale);
qg.push(generator.qg * p_scale);
vg.push(generator.vg);
}
if cost_q.is_empty() {
return Err(Error::NoGenerators);
}
let mut g_s: Vec<f64> = case.gs().iter().map(|value| value * p_scale).collect();
let mut b_s: Vec<f64> = case.bs().iter().map(|value| value * p_scale).collect();
let mut from_bus = Vec::new();
let mut to_bus = Vec::new();
let mut g = Vec::new();
let mut b = Vec::new();
let mut g_fr = Vec::new();
let mut b_fr = Vec::new();
let mut g_to = Vec::new();
let mut b_to = Vec::new();
let mut tap = Vec::new();
let mut shift = Vec::new();
let mut s_max = Vec::new();
let mut angle_min = Vec::new();
let mut angle_max = Vec::new();
let mut branch_rows = Vec::new();
let mut skipped_zero_impedance = Vec::new();
let network = case.network();
for (source_row, branch) in case.in_service_branches() {
let from = case
.bus_index(branch.from)
.ok_or(powerio::Error::UnknownBus {
bus_id: branch.from,
element_index: source_row,
})?;
let to = case
.bus_index(branch.to)
.ok_or(powerio::Error::UnknownBus {
bus_id: branch.to,
element_index: source_row,
})?;
let Some((series_g, series_b)) = branch.series_admittance(source_row)? else {
if options.skip_zero_impedance {
skipped_zero_impedance.push(source_row);
continue;
}
return Err(powerio::Error::ZeroImpedance { row: source_row }.into());
};
let charging = branch.terminal_charging();
if from == to {
let tap = branch.divisible_tap(source_row)?;
let tap_squared = tap * tap;
let cross = 2.0 * case.angle_radians(branch.shift).cos() / tap;
g_s[from] += ((series_g + charging.g_fr) / tap_squared + (series_g + charging.g_to)
- series_g * cross)
* y_scale;
b_s[from] += ((series_b + charging.b_fr) / tap_squared + (series_b + charging.b_to)
- series_b * cross)
* y_scale;
continue;
}
from_bus.push(from);
to_bus.push(to);
g.push(series_g * y_scale);
b.push(series_b * y_scale);
g_fr.push(charging.g_fr * y_scale);
b_fr.push(charging.b_fr * y_scale);
g_to.push(charging.g_to * y_scale);
b_to.push(charging.b_to * y_scale);
let amin = case.angle_radians(branch.angmin);
let amax = case.angle_radians(branch.angmax);
tap.push(branch.divisible_tap(source_row)?);
shift.push(case.angle_radians(branch.shift));
s_max.push(thermal.of(branch, amin, amax, &network.buses[from], &network.buses[to]));
angle_min.push(amin);
angle_max.push(amax);
branch_rows.push(source_row);
}
let mut vm_min = Vec::with_capacity(n_buses);
let mut vm_max = Vec::with_capacity(n_buses);
let mut vm = Vec::with_capacity(n_buses);
for bus in &network.buses {
vm_min.push(bus.vmin);
vm_max.push(bus.vmax);
vm.push(bus.vm);
}
Ok(AcOpfInstance {
name: case.name().to_owned(),
n_buses,
n_source_generators: case.generators().len(),
n_source_branches: case.branches().len(),
base_mva: case.base_mva(),
units: options.units,
skip_zero_impedance: options.skip_zero_impedance,
bus_ids: (0..n_buses).map(|index| case.bus_id(index)).collect(),
reference_buses: ReferenceBuses::new(case.reference_bus_indices()),
buses: AcBusData {
p_d: case.pd().iter().map(|value| value * p_scale).collect(),
q_d: case.qd().iter().map(|value| value * p_scale).collect(),
g_s,
b_s,
vm_min,
vm_max,
vm,
},
generators: AcGeneratorData {
bus_of_gen,
source_rows: generator_rows,
q: cost_q,
c: cost_c,
c0: cost_c0,
pmax,
pmin,
qmax,
qmin,
pg,
qg,
vg,
},
branches: AcBranchData {
from_bus,
to_bus,
g,
b,
g_fr,
b_fr,
g_to,
b_to,
tap,
shift,
s_max,
angle_min,
angle_max,
source_rows: branch_rows,
skipped_zero_impedance,
},
})
}