use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::Diagnostic;
use crate::diagnostics::codes;
use crate::network::{BalancedNetwork, BusId, BusType, Hvdc};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum HvdcTreatment {
#[default]
FixedInjection,
Ignore,
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub struct HvdcTerminalInjection {
pub row: usize,
pub bus: BusId,
pub p: f64,
pub q: f64,
}
#[derive(Debug, Clone, Default, PartialEq)]
#[non_exhaustive]
pub struct HvdcInjections {
pub terminals: Vec<HvdcTerminalInjection>,
pub out_of_service: Vec<usize>,
pub inactive_terminal: Vec<usize>,
pub per_unit: bool,
}
impl HvdcInjections {
#[must_use]
pub fn n_injecting(&self) -> usize {
self.terminals.len() / 2
}
fn sending_ends(&self) -> impl Iterator<Item = &HvdcTerminalInjection> {
self.terminals.iter().step_by(2)
}
fn receiving_ends(&self) -> impl Iterator<Item = &HvdcTerminalInjection> {
self.terminals.iter().skip(1).step_by(2)
}
#[must_use]
pub fn calc_sent_power(&self) -> f64 {
-self.sending_ends().map(|t| t.p).sum::<f64>()
}
#[must_use]
pub fn calc_delivered_power(&self) -> f64 {
self.receiving_ends().map(|t| t.p).sum()
}
#[must_use]
pub fn calc_reactive_injection(&self) -> f64 {
self.terminals.iter().map(|t| t.q).sum()
}
#[must_use]
pub fn to_diagnostics(&self, treatment: HvdcTreatment) -> Vec<Diagnostic> {
let (p_unit, q_unit) = if self.per_unit {
("per unit", "per unit")
} else {
("MW", "MVAr")
};
let mut diagnostics = Vec::new();
let n = self.n_injecting();
if n > 0 {
let sent = self.calc_sent_power();
let delivered = self.calc_delivered_power();
let lost = amount(sent - delivered);
let reactive = amount(self.calc_reactive_injection());
let (sent, delivered) = (amount(sent), amount(delivered));
diagnostics.push(match treatment {
HvdcTreatment::FixedInjection => Diagnostic::of(
&codes::BUILD_HVDC_FIXED_INJECTION,
format!(
"{n} in service HVDC line(s) enter the bus balance as fixed injections: \
{sent} {p_unit} withdrawn at the from buses, {delivered} {p_unit} \
delivered at the to buses ({lost} {p_unit} lost), and {reactive} \
{q_unit} reactive injection over both ends"
),
),
HvdcTreatment::Ignore => Diagnostic::of(
&codes::BUILD_HVDC_IGNORED,
format!(
"{n} in service HVDC line(s) were left out of the bus balance by \
request; {sent} {p_unit} sent and {delivered} {p_unit} delivered are \
absent from the calculation"
),
),
});
}
for row in &self.inactive_terminal {
diagnostics.push(Diagnostic::of(
&codes::BUILD_HVDC_TERMINAL_INACTIVE,
format!(
"HVDC line row {row} is in service, but a terminal bus is isolated or not \
declared; it injects nothing"
),
));
}
diagnostics
}
}
fn amount(value: f64) -> String {
let text = format!("{value:.6}");
let text = text.trim_end_matches('0').trim_end_matches('.');
if text == "-0" {
"0".to_owned()
} else {
text.to_owned()
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub struct HvdcTerminalPowers {
pub p_from: f64,
pub q_from: f64,
pub p_to: f64,
pub q_to: f64,
}
impl HvdcTerminalPowers {
#[must_use]
pub fn calc_active_loss(&self) -> f64 {
self.p_from + self.p_to
}
#[must_use]
pub fn calc_bus_injections(&self) -> [(f64, f64); 2] {
[(-self.p_from, -self.q_from), (-self.p_to, -self.q_to)]
}
}
impl Hvdc {
#[must_use]
pub fn calc_bus_to_device_powers(&self) -> HvdcTerminalPowers {
HvdcTerminalPowers {
p_from: self.pf,
q_from: -self.qf,
p_to: -self.pt,
q_to: -self.qt,
}
}
#[must_use]
pub fn calc_terminal_injections(&self) -> [(BusId, f64, f64); 2] {
let [(p_from, q_from), (p_to, q_to)] =
self.calc_bus_to_device_powers().calc_bus_injections();
[(self.from, p_from, q_from), (self.to, p_to, q_to)]
}
}
impl BalancedNetwork {
#[must_use]
pub fn calc_hvdc_injections(&self) -> HvdcInjections {
let mut injections = HvdcInjections {
per_unit: self.is_normalized(),
..HvdcInjections::default()
};
if self.hvdc().is_empty() {
return injections;
}
let kinds: HashMap<BusId, BusType> = self.buses().iter().map(|b| (b.id, b.kind)).collect();
let active = |bus: BusId| {
kinds
.get(&bus)
.is_some_and(|&kind| kind != BusType::Isolated)
};
for (row, line) in self.hvdc().iter().enumerate() {
if !line.in_service {
injections.out_of_service.push(row);
} else if !(active(line.from) && active(line.to)) {
injections.inactive_terminal.push(row);
} else {
for (bus, p, q) in line.calc_terminal_injections() {
injections
.terminals
.push(HvdcTerminalInjection { row, bus, p, q });
}
}
}
injections
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::network::Bus;
fn bus(id: usize, kind: BusType) -> Bus {
Bus::new(BusId(id), kind, 230.0)
}
fn line(from: usize, to: usize, pf: f64, pt: f64, qf: f64, qt: f64) -> Hvdc {
let mut line = Hvdc::new(BusId(from), BusId(to));
line.pf = pf;
line.pt = pt;
line.qf = qf;
line.qt = qt;
line
}
#[test]
fn terminal_injections_follow_the_dcline_convention() {
let injections = line(1, 2, 100.0, 97.0, -40.0, -35.0).calc_terminal_injections();
assert_eq!(injections[0], (BusId(1), -100.0, -40.0));
assert_eq!(injections[1], (BusId(2), 97.0, -35.0));
}
fn assert_injections_negate_terminals(line: &Hvdc) {
let terminal = line.calc_bus_to_device_powers();
let [from, to] = line.calc_terminal_injections();
assert_eq!(from, (line.from, -terminal.p_from, -terminal.q_from));
assert_eq!(to, (line.to, -terminal.p_to, -terminal.q_to));
}
#[test]
fn forward_transfer_terminal_powers_point_from_each_bus_into_the_line() {
let forward = line(1, 2, 100.0, 97.0, -30.0, -25.0);
let terminal = forward.calc_bus_to_device_powers();
assert_eq!((terminal.p_from, terminal.q_from), (100.0, 30.0));
assert_eq!((terminal.p_to, terminal.q_to), (-97.0, 25.0));
assert!((terminal.calc_active_loss() - 3.0).abs() < 1e-12);
let [from, to] = forward.calc_terminal_injections();
assert_eq!(from, (BusId(1), -100.0, -30.0));
assert_eq!(to, (BusId(2), 97.0, -25.0));
assert_injections_negate_terminals(&forward);
}
#[test]
fn reverse_transfer_keeps_the_ends_and_a_positive_loss() {
let reverse = line(1, 2, -97.0, -100.0, -25.0, -30.0);
let terminal = reverse.calc_bus_to_device_powers();
assert_eq!((terminal.p_from, terminal.q_from), (-97.0, 25.0));
assert_eq!((terminal.p_to, terminal.q_to), (100.0, 30.0));
assert!((terminal.calc_active_loss() - 3.0).abs() < 1e-12);
let [from, to] = reverse.calc_terminal_injections();
assert_eq!(from, (BusId(1), 97.0, -25.0));
assert_eq!(to, (BusId(2), -100.0, -30.0));
assert_injections_negate_terminals(&reverse);
}
#[test]
fn reactive_absorption_and_injection_at_either_end() {
let mixed = line(1, 2, 50.0, 49.0, 10.0, -5.0);
let terminal = mixed.calc_bus_to_device_powers();
assert_eq!((terminal.q_from, terminal.q_to), (-10.0, 5.0));
let [from, to] = mixed.calc_terminal_injections();
assert_eq!((from.2, to.2), (10.0, -5.0));
assert_injections_negate_terminals(&mixed);
let opposite = line(1, 2, 50.0, 49.0, -10.0, 5.0);
let terminal = opposite.calc_bus_to_device_powers();
assert_eq!((terminal.q_from, terminal.q_to), (10.0, -5.0));
let [from, to] = opposite.calc_terminal_injections();
assert_eq!((from.2, to.2), (-10.0, 5.0));
assert_injections_negate_terminals(&opposite);
for (qf, qt) in [(12.0, 7.0), (-12.0, -7.0)] {
let both = line(1, 2, 50.0, 49.0, qf, qt);
let terminal = both.calc_bus_to_device_powers();
assert_eq!((terminal.q_from, terminal.q_to), (-qf, -qt));
assert_injections_negate_terminals(&both);
}
}
#[test]
fn network_injections_negate_the_terminal_powers() {
let mut net = BalancedNetwork::in_memory(
"hvdc",
100.0,
vec![bus(1, BusType::Ref), bus(2, BusType::Pq)],
Vec::new(),
);
let forward = line(1, 2, 100.0, 97.0, -30.0, -25.0);
net.hvdc_mut().push(forward.clone());
let injections = net.calc_hvdc_injections();
let terminal = forward.calc_bus_to_device_powers();
let [from, to] = &injections.terminals[..] else {
panic!("one injecting line has two terminals");
};
assert_eq!((from.p, from.q), (-terminal.p_from, -terminal.q_from));
assert_eq!((to.p, to.q), (-terminal.p_to, -terminal.q_to));
let lost = injections.calc_sent_power() - injections.calc_delivered_power();
assert!((lost - terminal.calc_active_loss()).abs() < 1e-12);
assert!((injections.calc_reactive_injection() + 55.0).abs() < 1e-12);
}
#[test]
fn out_of_service_and_inactive_terminals_inject_nothing() {
let mut net = BalancedNetwork::in_memory(
"hvdc",
100.0,
vec![
bus(1, BusType::Ref),
bus(2, BusType::Pq),
bus(3, BusType::Isolated),
],
Vec::new(),
);
let mut off = line(1, 2, 50.0, 49.0, 0.0, 0.0);
off.in_service = false;
net.hvdc_mut().extend([
line(1, 2, 100.0, 97.0, 0.0, 0.0),
off,
line(1, 3, 10.0, 10.0, 0.0, 0.0),
line(1, 9, 10.0, 10.0, 0.0, 0.0),
]);
let injections = net.calc_hvdc_injections();
assert_eq!(injections.n_injecting(), 1);
assert_eq!(injections.out_of_service, vec![1]);
assert_eq!(injections.inactive_terminal, vec![2, 3]);
assert!((injections.calc_sent_power() - 100.0).abs() < 1e-12);
assert!((injections.calc_delivered_power() - 97.0).abs() < 1e-12);
let fixed = injections.to_diagnostics(HvdcTreatment::FixedInjection);
assert_eq!(fixed.len(), 3);
assert_eq!(fixed[0].code(), "BUILD.HVDC.FIXED_INJECTION");
assert!(
fixed[0].message().contains("100 MW withdrawn"),
"{}",
fixed[0].message()
);
assert!(
fixed[0].message().contains("97 MW"),
"{}",
fixed[0].message()
);
assert_eq!(fixed[1].code(), "BUILD.HVDC.TERMINAL_INACTIVE");
let ignored = injections.to_diagnostics(HvdcTreatment::Ignore);
assert_eq!(ignored[0].code(), "BUILD.HVDC.IGNORED");
}
#[test]
fn a_network_without_hvdc_reports_nothing() {
let net =
BalancedNetwork::in_memory("empty", 100.0, vec![bus(1, BusType::Ref)], Vec::new());
let injections = net.calc_hvdc_injections();
assert_eq!(injections, HvdcInjections::default());
assert_eq!(injections.to_diagnostics(HvdcTreatment::FixedInjection), []);
}
}