use std::collections::HashMap;
use std::ops::Index;
use faer::Mat;
use serde::{Deserialize, Serialize};
use surge_network::AngleReference;
pub(crate) const MIN_REACTANCE: f64 = 1e-20;
#[derive(Debug, thiserror::Error)]
pub enum DcError {
#[error("network has no buses")]
EmptyNetwork,
#[error("network has no slack bus")]
NoSlackBus,
#[error("singular B' matrix -- network may be disconnected")]
SingularMatrix,
#[error("N-2 LODF computation error: {0}")]
Infeasible(String),
#[error("invalid network: {0}")]
InvalidNetwork(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DcPfSolution {
pub theta: Vec<f64>,
pub branch_p_flow: Vec<f64>,
pub slack_p_injection: f64,
pub solve_time_secs: f64,
#[serde(default)]
pub total_generation_mw: f64,
#[serde(default)]
pub slack_distribution: HashMap<usize, f64>,
pub p_inject_pu: Vec<f64>,
#[serde(default)]
pub island_ids: Vec<usize>,
}
#[derive(Debug, Clone, Default)]
pub struct DcPfOptions {
pub headroom_slack_bus_indices: Option<Vec<usize>>,
pub participation_factors: Option<Vec<(usize, f64)>>,
pub angle_reference: AngleReference,
pub external_p_injections_mw: Vec<(u32, f64)>,
}
impl DcPfOptions {
pub fn with_headroom_slack(participating_bus_indices: &[usize]) -> Self {
if participating_bus_indices.is_empty() {
return Self::default();
}
DcPfOptions {
headroom_slack_bus_indices: Some(participating_bus_indices.to_vec()),
..Self::default()
}
}
pub fn with_participation_factors(weights: &[(usize, f64)]) -> Self {
let filtered: Vec<(usize, f64)> = weights
.iter()
.copied()
.filter(|&(_, w)| w > 0.0 && w.is_finite())
.collect();
if filtered.is_empty() {
return Self::default();
}
DcPfOptions {
participation_factors: Some(filtered),
..Self::default()
}
}
pub fn with_network_participation(network: &surge_network::Network) -> Self {
let weights = network.agc_participation_by_bus();
if weights.is_empty() {
return Self::default();
}
DcPfOptions {
participation_factors: Some(weights),
..Self::default()
}
}
pub fn with_angle_reference(mut self, angle_reference: AngleReference) -> Self {
self.angle_reference = angle_reference;
self
}
}
#[derive(Debug, Clone, Default)]
pub struct DcAnalysisRequest {
pub monitored_branch_indices: Option<Vec<usize>>,
pub ptdf_bus_indices: Option<Vec<usize>>,
pub lodf_outage_branch_indices: Vec<usize>,
pub otdf_outage_branch_indices: Vec<usize>,
pub otdf_bus_indices: Option<Vec<usize>>,
pub n2_outage_pairs: Vec<(usize, usize)>,
pub pf_options: DcPfOptions,
pub sensitivity_options: Option<crate::sensitivity::DcSensitivityOptions>,
}
impl DcAnalysisRequest {
pub fn all_branches() -> Self {
Self::default()
}
pub fn with_monitored_branches(monitored_branch_indices: &[usize]) -> Self {
Self {
monitored_branch_indices: Some(monitored_branch_indices.to_vec()),
..Self::default()
}
}
pub fn with_ptdf_buses(mut self, bus_indices: &[usize]) -> Self {
self.ptdf_bus_indices = Some(bus_indices.to_vec());
self
}
pub fn with_lodf_outages(mut self, outage_branch_indices: &[usize]) -> Self {
self.lodf_outage_branch_indices = outage_branch_indices.to_vec();
self
}
pub fn with_otdf_outages(mut self, outage_branch_indices: &[usize]) -> Self {
self.otdf_outage_branch_indices = outage_branch_indices.to_vec();
self
}
pub fn with_otdf_buses(mut self, bus_indices: &[usize]) -> Self {
self.otdf_bus_indices = Some(bus_indices.to_vec());
self
}
pub fn with_n2_outage_pairs(mut self, outage_pairs: &[(usize, usize)]) -> Self {
self.n2_outage_pairs = outage_pairs.to_vec();
self
}
pub fn with_pf_options(mut self, pf_options: DcPfOptions) -> Self {
self.pf_options = pf_options;
self
}
pub fn with_sensitivity_options(
mut self,
sensitivity_options: crate::sensitivity::DcSensitivityOptions,
) -> Self {
self.sensitivity_options = Some(sensitivity_options);
self
}
}
#[derive(Debug, Clone)]
pub struct DcAnalysisResult {
pub power_flow: DcPfSolution,
pub monitored_branch_indices: Vec<usize>,
pub ptdf: PtdfRows,
pub ptdf_bus_indices: Vec<usize>,
pub otdf: Option<OtdfResult>,
pub otdf_outage_branch_indices: Vec<usize>,
pub otdf_bus_indices: Vec<usize>,
pub lodf: Option<LodfResult>,
pub lodf_outage_branch_indices: Vec<usize>,
pub n2_lodf: Option<N2LodfBatchResult>,
pub n2_outage_pairs: Vec<(usize, usize)>,
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct PtdfRows {
monitored_branch_indices: Vec<usize>,
row_positions: Vec<Option<usize>>,
bus_indices: Vec<usize>,
bus_positions: Vec<Option<usize>>,
n_values_per_row: usize,
values: Vec<f64>,
}
impl PtdfRows {
pub(crate) fn new(
monitored_branch_indices: &[usize],
bus_indices: &[usize],
n_branches: usize,
n_total_buses: usize,
) -> Self {
let mut row_positions = vec![None; n_branches];
for (row_pos, &branch_idx) in monitored_branch_indices.iter().enumerate() {
if branch_idx < n_branches && row_positions[branch_idx].is_none() {
row_positions[branch_idx] = Some(row_pos);
}
}
let mut bus_positions = vec![None; n_total_buses];
for (bus_pos, &bus_idx) in bus_indices.iter().enumerate() {
if bus_idx < n_total_buses && bus_positions[bus_idx].is_none() {
bus_positions[bus_idx] = Some(bus_pos);
}
}
Self {
monitored_branch_indices: monitored_branch_indices.to_vec(),
row_positions,
bus_indices: bus_indices.to_vec(),
bus_positions,
n_values_per_row: bus_indices.len(),
values: vec![0.0; monitored_branch_indices.len() * bus_indices.len()],
}
}
#[inline]
pub fn monitored_branches(&self) -> &[usize] {
&self.monitored_branch_indices
}
#[inline]
pub fn bus_indices(&self) -> &[usize] {
&self.bus_indices
}
#[inline]
pub fn n_rows(&self) -> usize {
self.monitored_branch_indices.len()
}
#[inline]
pub fn n_cols(&self) -> usize {
self.n_values_per_row
}
#[inline]
pub fn row(&self, branch_idx: usize) -> Option<&[f64]> {
let row_pos = self.row_positions.get(branch_idx).copied().flatten()?;
Some(self.row_at(row_pos))
}
#[inline]
pub fn row_at(&self, row_pos: usize) -> &[f64] {
let start = row_pos * self.n_values_per_row;
&self.values[start..start + self.n_values_per_row]
}
#[inline(always)]
pub(crate) fn row_at_mut(&mut self, row_pos: usize) -> &mut [f64] {
let start = row_pos * self.n_values_per_row;
&mut self.values[start..start + self.n_values_per_row]
}
#[inline]
pub fn get(&self, branch_idx: usize, bus_idx: usize) -> f64 {
let Some(bus_pos) = self.bus_positions.get(bus_idx).copied().flatten() else {
return 0.0;
};
self.row(branch_idx)
.and_then(|row| row.get(bus_pos))
.copied()
.unwrap_or(0.0)
}
pub fn into_parts(self) -> (Vec<usize>, Vec<usize>, Vec<f64>) {
(self.monitored_branch_indices, self.bus_indices, self.values)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct OtdfResult {
monitored_branch_indices: Vec<usize>,
outage_branch_indices: Vec<usize>,
bus_indices: Vec<usize>,
monitored_positions: Vec<Option<usize>>,
outage_positions: Vec<Option<usize>>,
bus_positions: Vec<Option<usize>>,
n_values_per_vector: usize,
values: Vec<f64>,
}
impl OtdfResult {
pub(crate) fn new(
monitored_branch_indices: &[usize],
outage_branch_indices: &[usize],
bus_indices: &[usize],
n_branches: usize,
n_total_buses: usize,
) -> Self {
let mut monitored_positions = vec![None; n_branches];
for (row_pos, &branch_idx) in monitored_branch_indices.iter().enumerate() {
if branch_idx < n_branches && monitored_positions[branch_idx].is_none() {
monitored_positions[branch_idx] = Some(row_pos);
}
}
let mut outage_positions = vec![None; n_branches];
for (col_pos, &branch_idx) in outage_branch_indices.iter().enumerate() {
if branch_idx < n_branches && outage_positions[branch_idx].is_none() {
outage_positions[branch_idx] = Some(col_pos);
}
}
let mut bus_positions = vec![None; n_total_buses];
for (bus_pos, &bus_idx) in bus_indices.iter().enumerate() {
if bus_idx < n_total_buses && bus_positions[bus_idx].is_none() {
bus_positions[bus_idx] = Some(bus_pos);
}
}
Self {
monitored_branch_indices: monitored_branch_indices.to_vec(),
outage_branch_indices: outage_branch_indices.to_vec(),
bus_indices: bus_indices.to_vec(),
monitored_positions,
outage_positions,
bus_positions,
n_values_per_vector: bus_indices.len(),
values: vec![
0.0;
monitored_branch_indices.len()
* outage_branch_indices.len()
* bus_indices.len()
],
}
}
#[inline]
pub fn monitored_branches(&self) -> &[usize] {
&self.monitored_branch_indices
}
#[inline]
pub fn outage_branches(&self) -> &[usize] {
&self.outage_branch_indices
}
#[inline]
pub fn bus_indices(&self) -> &[usize] {
&self.bus_indices
}
#[inline]
pub fn n_monitored(&self) -> usize {
self.monitored_branch_indices.len()
}
#[inline]
pub fn n_outages(&self) -> usize {
self.outage_branch_indices.len()
}
#[inline]
pub fn n_buses(&self) -> usize {
self.n_values_per_vector
}
#[inline]
pub fn vector(&self, monitored_branch_idx: usize, outage_branch_idx: usize) -> Option<&[f64]> {
let monitored_pos = self
.monitored_positions
.get(monitored_branch_idx)
.copied()
.flatten()?;
let outage_pos = self
.outage_positions
.get(outage_branch_idx)
.copied()
.flatten()?;
Some(self.vector_at(monitored_pos, outage_pos))
}
#[inline]
pub fn vector_at(&self, monitored_pos: usize, outage_pos: usize) -> &[f64] {
let start = (monitored_pos * self.n_outages() + outage_pos) * self.n_values_per_vector;
&self.values[start..start + self.n_values_per_vector]
}
#[inline(always)]
pub(crate) fn vector_at_mut(&mut self, monitored_pos: usize, outage_pos: usize) -> &mut [f64] {
let start = (monitored_pos * self.n_outages() + outage_pos) * self.n_values_per_vector;
&mut self.values[start..start + self.n_values_per_vector]
}
#[inline]
pub fn get(
&self,
monitored_branch_idx: usize,
outage_branch_idx: usize,
bus_idx: usize,
) -> f64 {
let Some(bus_pos) = self.bus_positions.get(bus_idx).copied().flatten() else {
return 0.0;
};
self.vector(monitored_branch_idx, outage_branch_idx)
.and_then(|vector| vector.get(bus_pos))
.copied()
.unwrap_or(0.0)
}
pub fn into_parts(self) -> (Vec<usize>, Vec<usize>, Vec<usize>, Vec<f64>) {
(
self.monitored_branch_indices,
self.outage_branch_indices,
self.bus_indices,
self.values,
)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct LodfResult {
monitored_branch_indices: Vec<usize>,
outage_branch_indices: Vec<usize>,
values: Mat<f64>,
}
impl LodfResult {
pub(crate) fn new(
monitored_branch_indices: &[usize],
outage_branch_indices: &[usize],
values: Mat<f64>,
) -> Self {
Self {
monitored_branch_indices: monitored_branch_indices.to_vec(),
outage_branch_indices: outage_branch_indices.to_vec(),
values,
}
}
#[inline]
pub fn monitored_branches(&self) -> &[usize] {
&self.monitored_branch_indices
}
#[inline]
pub fn outage_branches(&self) -> &[usize] {
&self.outage_branch_indices
}
#[inline]
pub fn n_rows(&self) -> usize {
self.values.nrows()
}
#[inline]
pub fn n_cols(&self) -> usize {
self.values.ncols()
}
#[inline]
pub fn matrix(&self) -> &Mat<f64> {
&self.values
}
#[inline]
pub fn into_parts(self) -> (Vec<usize>, Vec<usize>, Mat<f64>) {
(
self.monitored_branch_indices,
self.outage_branch_indices,
self.values,
)
}
}
impl Index<(usize, usize)> for LodfResult {
type Output = f64;
fn index(&self, index: (usize, usize)) -> &Self::Output {
&self.values[index]
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct LodfMatrixResult {
branch_indices: Vec<usize>,
values: Mat<f64>,
}
impl LodfMatrixResult {
pub(crate) fn new(branch_indices: &[usize], values: Mat<f64>) -> Self {
Self {
branch_indices: branch_indices.to_vec(),
values,
}
}
#[inline]
pub fn branches(&self) -> &[usize] {
&self.branch_indices
}
#[inline]
pub fn n_rows(&self) -> usize {
self.values.nrows()
}
#[inline]
pub fn n_cols(&self) -> usize {
self.values.ncols()
}
#[inline]
pub fn matrix(&self) -> &Mat<f64> {
&self.values
}
#[inline]
pub fn into_parts(self) -> (Vec<usize>, Mat<f64>) {
(self.branch_indices, self.values)
}
}
impl Index<(usize, usize)> for LodfMatrixResult {
type Output = f64;
fn index(&self, index: (usize, usize)) -> &Self::Output {
&self.values[index]
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct LodfPairs {
monitored_branch_indices: Vec<usize>,
outage_branch_indices: Vec<usize>,
values: HashMap<(usize, usize), f64>,
}
impl LodfPairs {
pub(crate) fn new(
monitored_branch_indices: &[usize],
outage_branch_indices: &[usize],
values: HashMap<(usize, usize), f64>,
) -> Self {
Self {
monitored_branch_indices: monitored_branch_indices.to_vec(),
outage_branch_indices: outage_branch_indices.to_vec(),
values,
}
}
#[inline]
pub fn monitored_branches(&self) -> &[usize] {
&self.monitored_branch_indices
}
#[inline]
pub fn outage_branches(&self) -> &[usize] {
&self.outage_branch_indices
}
#[inline]
pub fn len(&self) -> usize {
self.values.len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.values.is_empty()
}
#[inline]
pub fn get(&self, monitored_branch_idx: usize, outage_branch_idx: usize) -> Option<f64> {
self.values
.get(&(monitored_branch_idx, outage_branch_idx))
.copied()
}
#[inline]
pub fn entries(&self) -> &HashMap<(usize, usize), f64> {
&self.values
}
#[allow(clippy::type_complexity)]
#[inline]
pub fn into_parts(self) -> (Vec<usize>, Vec<usize>, HashMap<(usize, usize), f64>) {
(
self.monitored_branch_indices,
self.outage_branch_indices,
self.values,
)
}
}
impl IntoIterator for LodfPairs {
type Item = ((usize, usize), f64);
type IntoIter = std::collections::hash_map::IntoIter<(usize, usize), f64>;
fn into_iter(self) -> Self::IntoIter {
self.values.into_iter()
}
}
impl<'a> IntoIterator for &'a LodfPairs {
type Item = (&'a (usize, usize), &'a f64);
type IntoIter = std::collections::hash_map::Iter<'a, (usize, usize), f64>;
fn into_iter(self) -> Self::IntoIter {
self.values.iter()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct N2LodfResult {
monitored_branch_indices: Vec<usize>,
outage_pair: (usize, usize),
values: Vec<f64>,
}
impl N2LodfResult {
pub(crate) fn new(
monitored_branch_indices: &[usize],
outage_pair: (usize, usize),
values: Vec<f64>,
) -> Self {
Self {
monitored_branch_indices: monitored_branch_indices.to_vec(),
outage_pair,
values,
}
}
#[inline]
pub fn monitored_branches(&self) -> &[usize] {
&self.monitored_branch_indices
}
#[inline]
pub fn outage_pair(&self) -> (usize, usize) {
self.outage_pair
}
#[inline]
pub fn values(&self) -> &[f64] {
&self.values
}
#[inline]
pub fn len(&self) -> usize {
self.values.len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.values.is_empty()
}
#[inline]
pub fn iter(&self) -> std::slice::Iter<'_, f64> {
self.values.iter()
}
#[inline]
pub fn into_parts(self) -> (Vec<usize>, (usize, usize), Vec<f64>) {
(self.monitored_branch_indices, self.outage_pair, self.values)
}
}
impl Index<usize> for N2LodfResult {
type Output = f64;
fn index(&self, index: usize) -> &Self::Output {
&self.values[index]
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct N2LodfBatchResult {
monitored_branch_indices: Vec<usize>,
outage_pairs: Vec<(usize, usize)>,
values: Mat<f64>,
}
impl N2LodfBatchResult {
pub(crate) fn new(
monitored_branch_indices: &[usize],
outage_pairs: &[(usize, usize)],
values: Mat<f64>,
) -> Self {
Self {
monitored_branch_indices: monitored_branch_indices.to_vec(),
outage_pairs: outage_pairs.to_vec(),
values,
}
}
#[inline]
pub fn monitored_branches(&self) -> &[usize] {
&self.monitored_branch_indices
}
#[inline]
pub fn outage_pairs(&self) -> &[(usize, usize)] {
&self.outage_pairs
}
#[inline]
pub fn n_rows(&self) -> usize {
self.values.nrows()
}
#[inline]
pub fn n_cols(&self) -> usize {
self.values.ncols()
}
#[inline]
pub fn matrix(&self) -> &Mat<f64> {
&self.values
}
#[inline]
pub fn into_parts(self) -> (Vec<usize>, Vec<(usize, usize)>, Mat<f64>) {
(
self.monitored_branch_indices,
self.outage_pairs,
self.values,
)
}
}
impl Index<(usize, usize)> for N2LodfBatchResult {
type Output = f64;
fn index(&self, index: (usize, usize)) -> &Self::Output {
&self.values[index]
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct BranchDcMetadata {
pub(crate) from_full: usize,
pub(crate) to_full: usize,
pub(crate) b_dc: f64,
pub(crate) in_service: bool,
pub(crate) has_reactance: bool,
}
impl BranchDcMetadata {
#[inline(always)]
pub(crate) fn is_sensitivity_active(self) -> bool {
self.in_service && self.has_reactance
}
}