use std::collections::HashMap;
use crate::linear::btheta::DcBranch;
use crate::opf::model::CostCurve;
use crate::opf::{LinearProgram, OpfError, OptStatus, Solution};
#[derive(Clone, Debug)]
pub struct DcGenerator {
pub index: usize,
pub bus: usize,
pub p_min: f64,
pub p_max: f64,
pub cost: Option<CostCurve>,
}
#[derive(Clone, Debug)]
pub struct DcLoad {
pub bus: usize,
pub p: f64,
}
#[derive(Clone, Debug)]
pub struct DcOpfNetwork {
pub n_buses: usize,
pub reference: usize,
pub branches: Vec<DcBranch>,
pub limits: HashMap<usize, Option<f64>>,
pub generators: Vec<DcGenerator>,
pub loads: Vec<DcLoad>,
pub base_mva: f64,
}
#[derive(Clone, Copy, Debug)]
pub struct DcOpfOptions {
pub shed_price: f64,
pub allow_shedding: bool,
}
impl Default for DcOpfOptions {
fn default() -> Self {
Self {
shed_price: 10_000.0,
allow_shedding: true,
}
}
}
#[derive(Clone, Debug)]
struct Layout {
theta: usize,
generator: usize,
shed: usize,
n_shed: usize,
cost_col: Vec<Option<usize>>,
balance_row: usize,
limit_rows: Vec<(usize, usize)>,
}
pub struct DcOpf {
network: DcOpfNetwork,
options: DcOpfOptions,
layout: Layout,
problem: LinearProgram,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Binding {
pub branch: usize,
pub flow: f64,
pub rate: f64,
pub price: f64,
}
#[derive(Clone, Debug)]
pub struct DcOpfResult {
pub status: OptStatus,
pub objective: f64,
pub dispatch: Vec<f64>,
pub angles: Vec<f64>,
pub flows: Vec<f64>,
pub lmp: Vec<f64>,
pub shed: Vec<f64>,
pub binding: Vec<Binding>,
}
impl DcOpf {
pub fn build(network: DcOpfNetwork, options: DcOpfOptions) -> Result<Self, OpfError> {
for g in &network.generators {
if let Some(cost) = &g.cost {
if !cost.is_convex() {
return Err(OpfError::Backend(format!(
"generator {} has a non-convex cost curve; the optimum of a \
non-convex problem cannot be certified by the checks this \
formulation relies on",
g.index
)));
}
}
}
let n_buses = network.n_buses;
let n_gen = network.generators.len();
let n_shed = if options.allow_shedding {
network.loads.len()
} else {
0
};
let mut cost_col = vec![None; n_gen];
let mut n_cost = 0;
for (g, generator) in network.generators.iter().enumerate() {
if matches!(generator.cost, Some(CostCurve::PiecewiseLinear { .. })) {
cost_col[g] = Some(n_buses + n_gen + n_shed + n_cost);
n_cost += 1;
}
}
let layout = Layout {
theta: 0,
generator: n_buses,
shed: n_buses + n_gen,
n_shed,
cost_col,
balance_row: 0,
limit_rows: Vec::new(),
};
let n_vars = n_buses + n_gen + n_shed + n_cost;
let mut lp = LinearProgram::new(n_vars);
for i in 0..n_buses {
lp.col_lower[layout.theta + i] = f64::NEG_INFINITY;
lp.col_upper[layout.theta + i] = f64::INFINITY;
}
lp.col_lower[layout.theta + network.reference] = 0.0;
lp.col_upper[layout.theta + network.reference] = 0.0;
let mut hessian: Vec<(usize, usize, f64)> = Vec::new();
for (g, generator) in network.generators.iter().enumerate() {
let col = layout.generator + g;
lp.col_lower[col] = generator.p_min;
lp.col_upper[col] = generator.p_max;
match &generator.cost {
Some(CostCurve::Polynomial { coefficients }) => {
if let Some(&c2) = coefficients.get(2) {
if c2 != 0.0 {
hessian.push((col, col, 2.0 * c2));
}
}
lp.col_cost[col] = coefficients.get(1).copied().unwrap_or(0.0);
lp.offset += coefficients.first().copied().unwrap_or(0.0);
}
Some(CostCurve::PiecewiseLinear { .. }) => {
let epigraph = layout.cost_col[g].expect("counted above");
lp.col_cost[epigraph] = 1.0;
lp.col_lower[epigraph] = f64::NEG_INFINITY;
lp.col_upper[epigraph] = f64::INFINITY;
}
None => {}
}
}
for d in 0..n_shed {
let col = layout.shed + d;
lp.col_lower[col] = 0.0;
lp.col_upper[col] = network.loads[d].p.max(0.0);
lp.col_cost[col] = options.shed_price * network.base_mva;
}
if !hessian.is_empty() {
lp.hessian = Some(hessian);
}
let mut b_matrix: Vec<HashMap<usize, f64>> = vec![HashMap::new(); n_buses];
let mut shift_injection = vec![0.0; n_buses];
for branch in &network.branches {
let (f, t, b) = (branch.from, branch.to, branch.b);
*b_matrix[f].entry(f).or_insert(0.0) += b;
*b_matrix[f].entry(t).or_insert(0.0) -= b;
*b_matrix[t].entry(t).or_insert(0.0) += b;
*b_matrix[t].entry(f).or_insert(0.0) -= b;
shift_injection[f] += b * branch.shift;
shift_injection[t] -= b * branch.shift;
}
let mut load_at = vec![0.0; n_buses];
for load in &network.loads {
load_at[load.bus] += load.p;
}
let mut gens_at: Vec<Vec<usize>> = vec![Vec::new(); n_buses];
for (g, generator) in network.generators.iter().enumerate() {
gens_at[generator.bus].push(g);
}
let mut sheds_at: Vec<Vec<usize>> = vec![Vec::new(); n_buses];
for d in 0..n_shed {
sheds_at[network.loads[d].bus].push(d);
}
for i in 0..n_buses {
let mut coefficients: Vec<(usize, f64)> = Vec::new();
for (&j, &value) in &b_matrix[i] {
if value != 0.0 {
coefficients.push((layout.theta + j, -value));
}
}
for &g in &gens_at[i] {
coefficients.push((layout.generator + g, 1.0));
}
for &d in &sheds_at[i] {
coefficients.push((layout.shed + d, 1.0));
}
let rhs = load_at[i] - shift_injection[i];
lp.add_row(&coefficients, rhs, rhs);
}
let mut limit_rows = Vec::new();
for branch in &network.branches {
let Some(Some(rate)) = network.limits.get(&branch.index).copied() else {
continue;
};
let offset = branch.b * branch.shift;
let row = lp.add_row(
&[
(layout.theta + branch.from, branch.b),
(layout.theta + branch.to, -branch.b),
],
-rate + offset,
rate + offset,
);
limit_rows.push((row, branch.index));
}
for (g, generator) in network.generators.iter().enumerate() {
let Some(CostCurve::PiecewiseLinear { points }) = &generator.cost else {
continue;
};
let epigraph = layout.cost_col[g].expect("assigned above");
let column = layout.generator + g;
if points.len() < 2 {
lp.col_cost[epigraph] = 0.0;
lp.offset += points.first().map(|p| p.1).unwrap_or(0.0);
continue;
}
for pair in points.windows(2) {
let ((p0, c0), (p1, c1)) = (pair[0], pair[1]);
if p1 == p0 {
continue;
}
let slope = (c1 - c0) / (p1 - p0);
let intercept = c0 - slope * p0;
lp.add_row(
&[(epigraph, 1.0), (column, -slope)],
intercept,
f64::INFINITY,
);
}
}
let layout = Layout {
limit_rows,
..layout
};
lp.validate()?;
Ok(Self {
network,
options,
layout,
problem: lp,
})
}
pub fn problem(&self) -> &LinearProgram {
&self.problem
}
pub fn network(&self) -> &DcOpfNetwork {
&self.network
}
pub fn options(&self) -> &DcOpfOptions {
&self.options
}
pub fn interpret(&self, solution: &Solution) -> DcOpfResult {
let base = self.network.base_mva;
if !solution.is_optimal() {
return DcOpfResult {
status: solution.status,
objective: f64::NAN,
dispatch: Vec::new(),
angles: Vec::new(),
flows: Vec::new(),
lmp: Vec::new(),
shed: Vec::new(),
binding: Vec::new(),
};
}
let angles: Vec<f64> = (0..self.network.n_buses)
.map(|i| solution.primal[self.layout.theta + i])
.collect();
let dispatch: Vec<f64> = (0..self.network.generators.len())
.map(|g| solution.primal[self.layout.generator + g] * base)
.collect();
let shed: Vec<f64> = (0..self.layout.n_shed)
.map(|d| solution.primal[self.layout.shed + d] * base)
.collect();
let n_branches = self
.network
.branches
.iter()
.map(|b| b.index + 1)
.max()
.unwrap_or(0);
let mut flows = vec![0.0; n_branches];
for branch in &self.network.branches {
let flow = branch.b * (angles[branch.from] - angles[branch.to] - branch.shift);
flows[branch.index] = flow * base;
}
let lmp: Vec<f64> = (0..self.network.n_buses)
.map(|i| solution.row_dual[self.layout.balance_row + i] / base)
.collect();
let binding = self
.layout
.limit_rows
.iter()
.filter_map(|&(row, branch_index)| {
let price = solution.row_dual[row];
let rate = self.network.limits.get(&branch_index).copied().flatten()?;
let flow = flows[branch_index].abs();
let rate_mw = rate * base;
if (flow - rate_mw).abs() > 1e-6 * rate_mw.max(1.0) {
return None;
}
Some(Binding {
branch: branch_index,
flow: flows[branch_index],
rate: rate_mw,
price: -price.abs() / base,
})
})
.collect();
DcOpfResult {
status: solution.status,
objective: solution.objective,
dispatch,
angles,
flows,
lmp,
shed,
binding,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn two_bus(rate: Option<f64>) -> DcOpfNetwork {
DcOpfNetwork {
n_buses: 2,
reference: 0,
branches: vec![DcBranch {
index: 0,
from: 0,
to: 1,
b: 10.0,
shift: 0.0,
}],
limits: HashMap::from([(0, rate)]),
generators: vec![
DcGenerator {
index: 0,
bus: 0,
p_min: 0.0,
p_max: 2.0,
cost: Some(CostCurve::Polynomial {
coefficients: vec![0.0, 1000.0],
}),
},
DcGenerator {
index: 1,
bus: 1,
p_min: 0.0,
p_max: 2.0,
cost: Some(CostCurve::Polynomial {
coefficients: vec![0.0, 3000.0],
}),
},
],
loads: vec![DcLoad { bus: 1, p: 1.0 }],
base_mva: 100.0,
}
}
#[test]
fn the_program_has_the_expected_shape() {
let opf = DcOpf::build(two_bus(Some(2.0)), DcOpfOptions::default()).unwrap();
let lp = opf.problem();
assert_eq!(lp.n_vars, 5);
assert_eq!(lp.n_rows, 3);
assert_eq!(lp.col_lower[0], 0.0);
assert_eq!(lp.col_upper[0], 0.0);
}
#[test]
fn an_unlimited_branch_adds_no_constraint() {
let opf = DcOpf::build(two_bus(None), DcOpfOptions::default()).unwrap();
assert_eq!(opf.problem().n_rows, 2, "only the two balance rows");
}
#[test]
fn shedding_can_be_switched_off() {
let options = DcOpfOptions {
allow_shedding: false,
..Default::default()
};
let opf = DcOpf::build(two_bus(Some(2.0)), options).unwrap();
assert_eq!(opf.problem().n_vars, 4, "no shedding column");
}
#[test]
fn a_quadratic_cost_is_doubled_into_the_hessian() {
let mut network = two_bus(None);
network.generators[0].cost = Some(CostCurve::Polynomial {
coefficients: vec![5.0, 1000.0, 7.0],
});
let opf = DcOpf::build(network, DcOpfOptions::default()).unwrap();
let hessian = opf.problem().hessian.as_ref().expect("a quadratic term");
assert_eq!(hessian.len(), 1);
let (row, col, value) = hessian[0];
assert_eq!((row, col), (2, 2), "generator 0 is the third column");
assert_eq!(value, 14.0, "2 * 7.0");
assert_eq!(opf.problem().offset, 5.0);
}
#[test]
fn a_non_convex_cost_is_refused_rather_than_solved() {
let mut network = two_bus(None);
network.generators[0].cost = Some(CostCurve::Polynomial {
coefficients: vec![0.0, 1000.0, -1.0],
});
assert!(DcOpf::build(network, DcOpfOptions::default()).is_err());
}
#[test]
fn a_phase_shift_moves_to_the_right_hand_side() {
let mut network = two_bus(Some(2.0));
network.branches[0].shift = 0.1;
let opf = DcOpf::build(network, DcOpfOptions::default()).unwrap();
let lp = opf.problem();
assert!(
(lp.row_lower[0] - (-1.0)).abs() < 1e-12,
"{:?}",
lp.row_lower
);
assert!((lp.row_lower[1] - 2.0).abs() < 1e-12, "{:?}", lp.row_lower);
assert!((lp.row_lower[2] - (-2.0 + 1.0)).abs() < 1e-12);
assert!((lp.row_upper[2] - (2.0 + 1.0)).abs() < 1e-12);
assert_eq!(lp.n_vars, 5, "the shift adds no variable");
}
}