# Q-V Curves and the Reactive Margin
## Motivation
[Continuation power flow](./continuation.md) asks how much further the *system*
can be loaded before it collapses. This asks a narrower and more operational
question about one bus: **how much reactive support would it take to hold this
bus at a given voltage, and how much margin is there before that stops being
possible?**
It is the question a reactive-planning study actually asks. A bus with 200 MVAr
of margin can absorb a contingency; one with 20 cannot, and no amount of
system-wide loadability tells you which is which.
## The method
Put a fictitious synchronous condenser at the bus — an ideal machine that holds
a voltage and produces whatever reactive power that takes — by retyping the bus
to `PV`. Then sweep its setpoint downward and read the reactive power it has to
supply at each step:
```text
Q
| \
| \___ Q > 0: the bus must be supported to hold it here
| \___
| \___ a condenser here has nothing to do
| \__.--
| ^
| the nose: dQ/d|V| = 0.
| No setpoint needs less reactive power than this,
| and −Q there is the margin.
```
## Why this needs no continuation
unknown — which is exactly a `PV` bus — so the Jacobian stays non-singular all
the way through the nose.
A P-V curve needs a predictor–corrector precisely because it does *not* fix a
magnitude: λ is a free parameter and the Jacobian goes singular at the fold.
Here the fold is in \\(Q\\), which is an **output** rather than an unknown, so
nothing goes singular and a sweep is enough.
That is a difference in kind, not an implementation shortcut, and it is why this
reuses [`run_power_flow`](./index.md) rather than continuation's bordered
corrector.
### Interpolating the minimum
The nose falls between samples, and pinning it to the nearest one wastes
accuracy the sweep already paid for. A parabola through the three bracketing
samples is exact where the curve is locally quadratic, which it is at a smooth
minimum, and costs no extra solves. Measured on the two-bus case with samples
0.01 apart, the interpolated nose lands within \\(4 \times 10^{-6}\\) of the
analytic voltage where the nearest sample can be out by 0.005.
The vertex is refused if it falls outside the bracketing samples — that means the
three points do not describe a minimum and the parabola is extrapolating.
## A curve in closed form
On two buses the whole curve can be written down, which is what the solver is
gated against. Slack at \\(E\angle 0\\), a lossless line \\(jx\\), one bus with
net injection \\(P + jQ\\). Eliminating the angle gives
\\[ u^{2} - (2a + E^{2})u + (a^{2} + b^{2}) = 0, \qquad a = xQ,\ b = xP,\ u = |V|^{2} \\]
Read as a quadratic in \\(Q\\) instead of in \\(u\\), the operating branch is
\\[ Q(u) = \frac{u - \sqrt{E^{2}u - x^{2}P^{2}}}{x} \\]
and \\(dQ/du = 0\\) gives the nose directly:
\\[ u_{nose} = \frac{E^{4} + 4x^{2}P^{2}}{4E^{2}}, \qquad
Q_{nose} = \frac{4x^{2}P^{2} - E^{4}}{4E^{2}x} \\]
`tests/qv_test.rs` matches both to \\(10^{-5}\\) in \\(Q\\) across a range of
reactances and loadings — the solver checked against arithmetic rather than
against itself.
## Q-V and P-V do not agree on the weakest bus
This is worth stating plainly, because the opposite is the natural assumption and
it is false.
Both are voltage-stability measures, so it is tempting to expect the bus a P-V
trace names critical — the largest component of the tangent at the nose — to be
the bus with the smallest Q-V margin. Measured on `case14`:
| P-V (continuation tangent at the nose) | 4, 3, 8, 9, 6 |
| Q-V (reactive margin at base loading) | 7, 13, 5, 9, 11 |
One bus in common out of five. The obvious explanation — that they are evaluated
at different operating points, one at the nose and one at base loading — does not
rescue it either: re-running the Q-V sweep at 98% of \\(\lambda_{max}\\) makes
the agreement *worse*, not better.
They measure related but distinct things. A P-V critical bus is a property of the
**system-wide collapse mode** along one particular loading direction. A Q-V
margin is one bus's **local reactive headroom** at the current operating point.
A bus can be locally weak without participating much in the system's collapse
mode, and the reverse.
This is why utilities run both rather than either, and why `tests/qv_test.rs`
deliberately does *not* assert that they agree — a test to that effect would have
been asserting something untrue about the physics.
## Reading the result
- **The margin is a positive number of MVAr**, and larger is stronger.
- **`nose_not_reached`** means the sweep stopped while the curve was still
falling. The number is then a *lower bound* on the margin, not the margin, and
the CLI says so — understating a bus's weakness is the direction that misleads.
- **`already_controlled`** means the bus was already `PV`, so the curve describes
moving an existing machine's setpoint rather than adding a condenser, and
\\(Q\\) includes what that machine was already producing.
- The condenser is **deliberately unlimited**. The curve measures how much
reactive power holding a voltage *would* take, which a limit would truncate
rather than answer. Other buses keep their own limits.
## Using it
```bash
gridoxide qv network.json --weakest 10 # rank buses by margin
gridoxide qv network.json --bus 13 --curve # one bus, with the curve
gridoxide qv network.json --bus 13 --v-min 0.2 # sweep further down
```
```python
curve = model.qv_curve(13, v_min=0.20)
print(curve["status"], curve["nose"]["margin_pu"])
for p in curve["points"]:
print(p["voltage"], p["q"])
```
```rust
use gridoxide::qv::{qv_curve, QvOptions};
let curve = qv_curve(&buses, &lines, &transformers, &shunts, 13, QvOptions::default());
if let Some(nose) = &curve.nose {
println!("{:.1} MVAr at |V| = {:.4}", nose.margin_mvar(100.0), nose.voltage);
}
```
## Limitations
- **One bus at a time.** A ranking is the caller looping, and each point is a
full solve, so a sweep over every bus of a large network is
`buses × steps` power flows. Embarrassingly parallel, and not parallelized.
- **The margin is measured at one operating point.** It says nothing about the
margin after a contingency, which is the question a security study asks — that
would be this sweep inside a contingency loop.
- **A slack bus is refused**: it already fixes its own magnitude and already has a
free reactive injection, so there is no condenser to add.