Skip to main content

gridoxide_core/outerloop/
qlimits.rs

1//! Reactive-limit enforcement: the `PV → PQ` switch.
2//!
3//! A `PV` bus holds its voltage by producing whatever reactive power that
4//! takes. A real machine cannot: past `q_max` (or below `q_min`) it saturates,
5//! the voltage falls away from target, and the bus is no longer a `PV` bus at
6//! all — it is a `PQ` bus injecting exactly its limit.
7//!
8//! This is the standard MATPOWER-style **one-directional** switch: a bus that
9//! moves `PV → PQ` stays there for the rest of the run. The reverse switch
10//! (a `PQ`-clamped bus whose voltage overshoots its target, which should
11//! return to `PV`) is deliberately not implemented — it is where the
12//! literature's oscillation reports come from, and the one-directional form is
13//! what every reference gridoxide is checked against uses by default.
14
15use crate::network::power_injections;
16use crate::types::BusType;
17
18use super::{Invalidates, OuterLoop, OuterLoopContext, OuterLoopStatus};
19
20/// Which buses this loop moved off voltage control, and whether it settled.
21#[derive(Clone, Debug, Default)]
22pub struct QLimitReport {
23    /// Buses switched `PV → PQ`, in switch order.
24    pub switches: Vec<usize>,
25}
26
27/// Enforces `q_min`/`q_max` on every `PV` bus by switching violators to `PQ`.
28///
29/// Reports [`Invalidates::Pattern`]: a bus type change moves `n_unknowns`, so
30/// the Jacobian's sparsity pattern changes and the cached symbolic
31/// factorization cannot be kept. That is the opposite of
32/// [`DistributedSlack`](super::DistributedSlack), which only moves `p_spec`.
33#[derive(Clone, Debug, Default)]
34pub struct ReactiveLimits {
35    switches: Vec<usize>,
36}
37
38impl ReactiveLimits {
39    pub fn new() -> Self {
40        Self::default()
41    }
42
43    /// What it did, readable after the solve.
44    pub fn report(&self) -> QLimitReport {
45        QLimitReport {
46            switches: self.switches.clone(),
47        }
48    }
49
50    /// Buses switched `PV → PQ`, in switch order.
51    pub fn switches(&self) -> &[usize] {
52        &self.switches
53    }
54}
55
56impl OuterLoop for ReactiveLimits {
57    fn name(&self) -> &'static str {
58        "ReactiveLimits"
59    }
60
61    fn invalidates(&self) -> Invalidates {
62        Invalidates::Pattern
63    }
64
65    fn check(&mut self, ctx: &mut OuterLoopContext<'_, '_>) -> OuterLoopStatus {
66        let (_, q_calc) = power_injections(ctx.net.buses, ctx.net.ybus);
67        let mut switched = false;
68        for b in ctx.net.buses.iter_mut() {
69            if b.bus_type != BusType::PV {
70                continue;
71            }
72            let q = q_calc[b.idx];
73            if q < b.q_min {
74                b.bus_type = BusType::PQ;
75                b.q_spec = b.q_min;
76                self.switches.push(b.idx);
77                switched = true;
78            } else if q > b.q_max {
79                b.bus_type = BusType::PQ;
80                b.q_spec = b.q_max;
81                self.switches.push(b.idx);
82                switched = true;
83            }
84        }
85        if switched {
86            OuterLoopStatus::Unstable
87        } else {
88            OuterLoopStatus::Stable
89        }
90    }
91}