use std::collections::HashSet;
use surge_network::Network;
use surge_network::network::LccHvdcControlMode;
pub struct BprimeSparseCsc {
pub dim: usize,
pub col_ptrs: Vec<usize>,
pub row_indices: Vec<usize>,
pub values: Vec<f64>,
pub full_to_reduced: Vec<Option<usize>>,
pub slack_idx: usize,
}
fn reduced_bus_map(
n: usize,
slack_idx: usize,
included_buses: Option<&HashSet<usize>>,
) -> Vec<Option<usize>> {
let mut full_to_reduced: Vec<Option<usize>> = vec![None; n];
let mut reduced_idx = 0usize;
for (i, slot) in full_to_reduced.iter_mut().enumerate() {
if i == slack_idx {
continue;
}
if included_buses.is_some_and(|buses| !buses.contains(&i)) {
continue;
}
*slot = Some(reduced_idx);
reduced_idx += 1;
}
full_to_reduced
}
#[cfg(test)]
pub fn build_bprime_sparse(network: &Network) -> BprimeSparseCsc {
let slack_idx = network
.slack_bus_index()
.expect("network must have a slack bus");
build_bprime_sparse_for_buses(network, None, slack_idx)
}
pub(crate) fn build_bprime_sparse_for_buses(
network: &Network,
included_buses: Option<&HashSet<usize>>,
slack_idx: usize,
) -> BprimeSparseCsc {
let n = network.n_buses();
let bus_map = network.bus_index_map();
let full_to_reduced = reduced_bus_map(n, slack_idx, included_buses);
let dim = full_to_reduced.iter().filter(|x| x.is_some()).count();
let mut coo: Vec<(usize, usize, f64)> = Vec::with_capacity(4 * network.branches.len());
for branch in &network.branches {
if !branch.in_service {
continue;
}
if branch.x.abs() < crate::types::MIN_REACTANCE {
continue;
}
let from_idx = *bus_map
.get(&branch.from_bus)
.expect("from_bus not found in bus map");
let to_idx = *bus_map
.get(&branch.to_bus)
.expect("to_bus not found in bus map");
if included_buses
.is_some_and(|buses| !buses.contains(&from_idx) || !buses.contains(&to_idx))
{
continue;
}
let b = branch.b_dc();
let ri = full_to_reduced[from_idx];
let rj = full_to_reduced[to_idx];
match (ri, rj) {
(Some(ri), Some(rj)) => {
coo.push((ri, ri, b));
coo.push((rj, rj, b));
coo.push((ri, rj, -b));
coo.push((rj, ri, -b));
}
(Some(ri), None) => {
coo.push((ri, ri, b));
}
(None, Some(rj)) => {
coo.push((rj, rj, b));
}
(None, None) => {}
}
}
coo.sort_unstable_by_key(|&(r, c, _)| (c, r));
let mut col_ptrs = vec![0usize; dim + 1];
let mut row_indices = Vec::with_capacity(coo.len());
let mut values = Vec::with_capacity(coo.len());
let mut cur_col = 0usize;
col_ptrs[0] = 0;
for &(r, c, v) in &coo {
while cur_col < c {
cur_col += 1;
col_ptrs[cur_col] = row_indices.len();
}
if let Some(last_row) = row_indices.last() {
if *last_row == r && col_ptrs[cur_col] < row_indices.len() {
*values.last_mut().unwrap() += v;
continue;
}
}
row_indices.push(r);
values.push(v);
}
while cur_col < dim {
cur_col += 1;
col_ptrs[cur_col] = row_indices.len();
}
let nnz = row_indices.len();
col_ptrs[dim] = nnz;
BprimeSparseCsc {
dim,
col_ptrs,
row_indices,
values,
full_to_reduced,
slack_idx,
}
}
pub(crate) fn build_p_injection_for_buses(
network: &Network,
full_to_reduced: &[Option<usize>],
slack_idx: usize,
included_buses: Option<&HashSet<usize>>,
) -> Vec<f64> {
let p_full = network.bus_p_injection_pu();
let n_reduced = full_to_reduced.iter().filter(|x| x.is_some()).count();
let mut p = vec![0.0; n_reduced];
for (full_idx, &p_val) in p_full.iter().enumerate() {
if full_idx == slack_idx {
continue;
}
if included_buses.is_some_and(|buses| !buses.contains(&full_idx)) {
continue;
}
if let Some(ri) = full_to_reduced[full_idx] {
let gs_pu = network.buses[full_idx].shunt_conductance_mw / network.base_mva;
p[ri] = p_val - gs_pu;
}
}
let bus_map = network.bus_index_map();
for branch in &network.branches {
if !branch.in_service || branch.x.abs() < crate::types::MIN_REACTANCE {
continue;
}
if branch.phase_shift_rad.abs() < 1e-12 {
continue; }
let phi_rad = branch.phase_shift_rad;
let b = branch.b_dc(); let correction = b * phi_rad;
let from_idx = bus_map[&branch.from_bus];
let to_idx = bus_map[&branch.to_bus];
if included_buses.is_none_or(|buses| buses.contains(&from_idx))
&& let Some(ri) = full_to_reduced[from_idx]
{
p[ri] += correction;
}
if included_buses.is_none_or(|buses| buses.contains(&to_idx))
&& let Some(rj) = full_to_reduced[to_idx]
{
p[rj] -= correction;
}
}
for dc in network.hvdc.links.iter().filter_map(|link| link.as_lcc()) {
if dc.mode == LccHvdcControlMode::Blocked {
continue;
}
if !dc.rectifier.in_service || !dc.inverter.in_service {
continue;
}
let p_dc_mw = match dc.mode {
LccHvdcControlMode::PowerControl => dc.scheduled_setpoint,
LccHvdcControlMode::CurrentControl => {
dc.scheduled_setpoint * dc.scheduled_voltage_kv.max(1.0)
}
LccHvdcControlMode::Blocked => continue,
};
let p_dc_pu = p_dc_mw / network.base_mva;
if let Some(&from_full) = bus_map.get(&dc.rectifier.bus)
&& included_buses.is_none_or(|buses| buses.contains(&from_full))
&& let Some(ri) = full_to_reduced[from_full]
{
p[ri] -= p_dc_pu;
}
if let Some(&to_full) = bus_map.get(&dc.inverter.bus)
&& included_buses.is_none_or(|buses| buses.contains(&to_full))
&& let Some(ri) = full_to_reduced[to_full]
{
p[ri] += p_dc_pu;
}
}
p
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_util::*;
#[test]
fn test_bprime_case9() {
skip_if_no_data!();
let net = load_net("case9");
let bprime = build_bprime_sparse(&net);
assert_eq!(bprime.dim, 8);
assert_eq!(bprime.slack_idx, 0);
assert_eq!(bprime.col_ptrs.len(), 9);
assert_eq!(*bprime.col_ptrs.last().unwrap(), bprime.values.len());
for col in 0..bprime.dim {
let start = bprime.col_ptrs[col];
let end = bprime.col_ptrs[col + 1];
let diag_val = bprime.row_indices[start..end]
.iter()
.zip(&bprime.values[start..end])
.find(|&(&r, _)| r == col)
.map(|(_, &v)| v)
.unwrap_or(0.0);
assert!(diag_val > 0.0, "diagonal at col {} should be positive", col);
}
}
}