use crate::PowerFlowOptions;
use crate::network::{ShuntAdm, build_ybus, effective_injection, power_injections, stamp_shunts};
use crate::solver::SolveStatus;
use crate::types::{Bus, BusType, Line, Transformer};
#[derive(Clone, Debug)]
pub struct QvOptions {
pub pf: PowerFlowOptions,
pub v_max: f64,
pub v_min: f64,
pub step: f64,
pub refine: bool,
}
impl Default for QvOptions {
fn default() -> Self {
Self {
pf: PowerFlowOptions {
max_iter: 60,
..Default::default()
},
v_max: 1.10,
v_min: 0.40,
step: 0.01,
refine: true,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct QvPoint {
pub voltage: f64,
pub q: f64,
pub iterations: usize,
}
#[derive(Clone, Debug, PartialEq)]
pub struct QvNose {
pub voltage: f64,
pub q: f64,
pub margin_pu: f64,
pub refined: bool,
}
impl QvNose {
pub fn margin_mvar(&self, base_mva: f64) -> f64 {
self.margin_pu * base_mva
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum QvStatus {
NoseFound,
NoseNotReached,
Failed,
Rejected(QvError),
}
#[derive(Clone, Debug, PartialEq)]
pub enum QvError {
SlackBus(usize),
BusOutOfRange {
bus: usize,
n: usize,
},
EmptyRange,
MethodUnsupported(crate::PowerFlowMethod),
}
#[derive(Clone, Debug)]
pub struct QvCurve {
pub bus: usize,
pub points: Vec<QvPoint>,
pub nose: Option<QvNose>,
pub status: QvStatus,
pub base_voltage: f64,
pub failed: Vec<f64>,
pub already_controlled: bool,
}
impl QvCurve {
fn rejected(bus: usize, why: QvError) -> Self {
Self {
bus,
points: Vec::new(),
nose: None,
status: QvStatus::Rejected(why),
base_voltage: f64::NAN,
failed: Vec::new(),
already_controlled: false,
}
}
pub fn margin_pu(&self) -> Option<f64> {
self.nose.as_ref().map(|n| n.margin_pu)
}
}
pub fn qv_curve(
buses: &[Bus],
lines: &[Line],
transformers: &[Transformer],
shunts: &[ShuntAdm],
bus: usize,
opts: QvOptions,
) -> QvCurve {
if bus >= buses.len() {
return QvCurve::rejected(
bus,
QvError::BusOutOfRange {
bus,
n: buses.len(),
},
);
}
if buses[bus].bus_type == BusType::Slack {
return QvCurve::rejected(bus, QvError::SlackBus(bus));
}
if opts.pf.method != crate::PowerFlowMethod::NewtonRaphson {
return QvCurve::rejected(bus, QvError::MethodUnsupported(opts.pf.method));
}
let range_ok = opts.v_min.is_finite() && opts.v_max.is_finite() && opts.v_min < opts.v_max;
if !opts.step.is_finite() || opts.step <= 0.0 || !range_ok {
return QvCurve::rejected(bus, QvError::EmptyRange);
}
let mut y = build_ybus(buses.len(), lines, transformers);
stamp_shunts(&mut y, shunts);
let ybus = y.finish();
let base = crate::run_power_flow(
buses.to_vec(),
lines,
transformers,
shunts,
crate::TapData::none(),
opts.pf.clone(),
);
let base_voltage = if base.stats.status == SolveStatus::Converged {
base.buses[bus].voltage_mag
} else {
f64::NAN
};
let already_controlled = buses[bus].bus_type == BusType::PV;
let mut points: Vec<QvPoint> = Vec::new();
let mut failed: Vec<f64> = Vec::new();
let steps = ((opts.v_max - opts.v_min) / opts.step).floor() as usize;
for k in 0..=steps {
let v = opts.v_max - opts.step * k as f64;
let mut trial = buses.to_vec();
trial[bus].bus_type = BusType::PV;
trial[bus].voltage_mag = v;
trial[bus].q_min = f64::NEG_INFINITY;
trial[bus].q_max = f64::INFINITY;
let report = crate::run_power_flow(
trial,
lines,
transformers,
shunts,
crate::TapData::none(),
opts.pf.clone(),
);
if report.stats.status != SolveStatus::Converged {
failed.push(v);
continue;
}
let (_, q_calc) = power_injections(&report.buses, &ybus);
let own = Bus {
voltage_mag: report.buses[bus].voltage_mag,
..buses[bus].clone()
};
let (_, q_own) = effective_injection(&own);
points.push(QvPoint {
voltage: report.buses[bus].voltage_mag,
q: q_calc[bus] - q_own,
iterations: report.stats.iterations(),
});
}
let (nose, status) = locate_nose(&points, opts.refine);
QvCurve {
bus,
points,
nose,
status,
base_voltage,
failed,
already_controlled,
}
}
fn locate_nose(points: &[QvPoint], refine: bool) -> (Option<QvNose>, QvStatus) {
if points.len() < 3 {
return (None, QvStatus::Failed);
}
let mut lowest = 0usize;
for (i, p) in points.iter().enumerate() {
if p.q < points[lowest].q {
lowest = i;
}
}
if lowest == 0 || lowest == points.len() - 1 {
return (
Some(QvNose {
voltage: points[lowest].voltage,
q: points[lowest].q,
margin_pu: -points[lowest].q,
refined: false,
}),
QvStatus::NoseNotReached,
);
}
let (a, b, c) = (&points[lowest - 1], &points[lowest], &points[lowest + 1]);
if !refine {
return (
Some(QvNose {
voltage: b.voltage,
q: b.q,
margin_pu: -b.q,
refined: false,
}),
QvStatus::NoseFound,
);
}
let (x1, y1) = (a.voltage, a.q);
let (x2, y2) = (b.voltage, b.q);
let (x3, y3) = (c.voltage, c.q);
let denom = (x1 - x2) * (y2 - y3) - (x2 - x3) * (y1 - y2);
let nose = if denom.abs() < 1e-15 {
QvNose {
voltage: x2,
q: y2,
margin_pu: -y2,
refined: false,
}
} else {
let vertex = x2
- 0.5 * ((x2 - x1).powi(2) * (y2 - y3) - (x2 - x3).powi(2) * (y2 - y1))
/ ((x2 - x1) * (y2 - y3) - (x2 - x3) * (y2 - y1));
if vertex > x1.max(x3) || vertex < x1.min(x3) || !vertex.is_finite() {
QvNose {
voltage: x2,
q: y2,
margin_pu: -y2,
refined: false,
}
} else {
let q = y1 * (vertex - x2) * (vertex - x3) / ((x1 - x2) * (x1 - x3))
+ y2 * (vertex - x1) * (vertex - x3) / ((x2 - x1) * (x2 - x3))
+ y3 * (vertex - x1) * (vertex - x2) / ((x3 - x1) * (x3 - x2));
QvNose {
voltage: vertex,
q,
margin_pu: -q,
refined: true,
}
}
};
(Some(nose), QvStatus::NoseFound)
}