[DOCUMENTED] design note: enforcing control limits inside the Newton-Raphson - #208
Merged
Merged
Conversation
docs/comparison_with_pypowsybl.rst ends its "outer loops" section on an open question -- whether a given OLF outer loop could be folded into the inner NR formulation the way distributed slack was, or fundamentally needs iteration around the solve. This note argues it is largely NOT case-by-case. Reactive limits, tap-ratio control, area interchange and secondary voltage control are the same mathematical object: a bounded control resource complementary to a regulated quantity. That is a complementarity condition, expressible as a projection (normal-map) row and solved by a semismooth Newton, which gives the switching logic a Jacobian instead of a heuristic. The note covers why the classical heuristics cycle (two tests evaluated on two different iterates; with two coupled controls, no correct ordering exists -- which is why OLF exposes transformerVoltageControlMode), the NCP / semismooth machinery and its convergence theorem, and one instantiation per control. It also covers two adjacent questions: pmin / pmax on distributed-slack participants (GeneratorContainer has no active limits at all today), and state-dependent slack weights, where margin-proportional weighting turns out to enforce pmax exactly without any complementarity -- provided the normalisation is dropped inside the NR, since it is a gauge and carrying it in would make the slack border dense. The formulation fits the existing contracts unusually well: every pin / release is a VALUE inside a fixed sparsity pattern, so one analyze + refactorize survives a whole sweep, and the pv/pq vector disappears entirely, which makes has_pv_changed() structurally unable to fire on a reactive-limit event. VoltageControl already reserves both Jacobian slots the three-way branch needs, and its "stranded" path is a hand-rolled instance of the same row swap; Hvdc is the precedent for an extension owning a state-dependent contribution without touching Ybus. Costs are stated plainly and none of them is benchmarked: dimension growth on the common grid (free in the batch path, real for a plain ac_pf), the scaling constant, degeneracy at a bound, non-uniqueness, and tap discreteness -- the one part that genuinely does need round-and-re-solve. Nothing here is implemented. Added as a [TODO] entry with the smallest useful first step named (pmin / pmax on generators plus clamp-and-renormalise on the slack), a label + cross-reference on comparison_with_pypowsybl.rst, and the page in the technical-documentation toctree. Assisted-by: Claude Code (claude-opus-5) Claude-Session: https://claude.ai/code/session_01CDrx7k9yW7ppP16bid8ZDN Signed-off-by: Benjamin Donnot <benjamin.donnot@rte-france.com>
…ghts-newton-raphson-xlkzku Signed-off-by: Benjamin Donnot <benjamin.donnot@rte-france.com>
dev_1.0.1 has moved since this branch started, in two ways that both bear on
the note:
- docs/dev_notes/ now exists as the home for exactly this kind of content
("Status note, not documentation ... not part of the built documentation
(docs/*.rst)"), so the note moves there as markdown and comes back out of
the technical-documentation toctree. The label and cross-reference added to
comparison_with_pypowsybl.rst are reverted with it: dev notes are not
referenced from the built docs, and the sibling note is not either.
- generators DO now have active power limits -- GeneratorContainer::set_p_limits
/ get_min_p / get_max_p, optional and NaN where never given -- and
compute_physical_violations already DETECTS a slack machine driven past them
(GenPCheck) and a bus past its reactive capability (BusQCheck). The note
claimed there were no active limits at all, which was true at its branch
point and is not now.
Correcting that changes the note's spine for the better: it is no longer "add
limits", it is the enforcement half of a split the sibling note keeps naming --
"b_min / b_max are checked, not enforced", "this only becomes meaningful the
day the limit is enforced". Step 1 of the incremental path becomes enforcing
the limits that exist, with GenPCheck as a ready-made oracle: every row it
flags today is a row enforcement should silence.
Two further corrections from the sibling note's findings, both about
sequencing: tap voltage control, area interchange and secondary voltage control
are blocked on the MODEL, not the formulation (a transformer stores no regulated
bus, target or deadband; there is no Area concept and there are no control zones
or pilot points), so the note now says so rather than implying the Jacobian work
is the obstacle. Added a section on how enforcement interacts with BusQCheck's
per-bus argument: the check is per bus because the per-machine split is a
convention, while enforcement is genuinely per machine, so the two should not be
expected to agree machine-by-machine until it exists.
Re-verified against the merged base: the VoltageControl ledger slots and the
"stranded" row swap, TrafoContainer's "no discrete tap", HvdcLineContainer's
"does NOT stamp the AC active power in Sbus" and _masked_slack_weights all still
read as the note describes.
Assisted-by: Claude Code (claude-opus-5)
Claude-Session: https://claude.ai/code/session_01CDrx7k9yW7ppP16bid8ZDN
Signed-off-by: Benjamin Donnot <benjamin.donnot@rte-france.com>
BDonnot
commented
Sep 18, 2026
BDonnot
left a comment
Collaborator
Author
There was a problem hiding this comment.
check the ref in the docs
…behind Both are review findings, and both are correct: docs/nr_control_limits.rst was deleted when the note moved to docs/dev_notes/, but docs/index.rst still listed `nr_control_limits` in the toctree and comparison_with_pypowsybl.rst still had a `:ref:`nr_control_limits``. That is a broken docs build -- a toctree entry for a nonexisting document, and an undefined label -- not a cosmetic leftover. The previous commit claimed to revert both. It did not: it ran `git checkout HEAD -- <those two files>` while HEAD was the merge commit, which already contained the very edits being reverted, so the checkout restored them instead of removing them. `git status` then showed the files as unmodified, which is what a checkout from HEAD guarantees whatever the content, so the check proved nothing. Reverting against origin/dev_1.0.1 is what was needed. Also drops the `.. _comparison_with_pypowsybl:` label added in the first commit: its only consumer was the `:ref:` in the note, and the note is not part of the built documentation any more. Verified this time against the base rather than against HEAD: the branch now touches exactly two files versus origin/dev_1.0.1 (CHANGELOG.rst and docs/dev_notes/nr_control_limits.md), and no docs/*.rst mentions nr_control_limits at all. Assisted-by: Claude Code (claude-opus-5) Claude-Session: https://claude.ai/code/session_01CDrx7k9yW7ppP16bid8ZDN Signed-off-by: Benjamin Donnot <benjamin.donnot@rte-france.com>
BDonnot
added a commit
that referenced
this pull request
Sep 19, 2026
Only conflict was the top of CHANGELOG.rst's [TODO], where both sides added entries: dev_1.0.1 the control-limits design note from #208, this branch the four written up while going through the review of #207. Both kept, the base branch's first. No code came in with the merge -- dev_1.0.1 added a docs file and that changelog entry, nothing else. Assisted-by: Claude Code (claude-opus-5) Claude-Session: https://claude.ai/code/session_01PC8RAAVDZEqq2UENu7jKNZ Signed-off-by: Benjamin Donnot <benjamin.donnot@rte-france.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Docs only. No code changes, nothing implemented. Two files touched versus
dev_1.0.1:CHANGELOG.rst(a[TODO]entry) anddocs/dev_notes/nr_control_limits.md(new).What this is
It is the companion to
outer_loop_checks_missing_data.md. That note asks the detectionquestion — what a post-solve "would this outer loop have fired?" check needs — and ends
several sections on the same sentence in different words: "
b_min/b_maxare checked,not enforced", "this only becomes meaningful the day the limit is enforced". This
note is a proposal for that day.
The argument
docs/comparison_with_pypowsybl.rstends its "outer loops" section on an open question:whether a given OLF outer loop could be folded into the inner NR formulation the way
distributed slack was, or fundamentally needs iteration around the solve. The note argues
it is largely not case-by-case — five controls are the same mathematical object:
[get_min_p, get_max_p][min_q_, max_q_][rho_min, rho_max]Each is a bounded control resource complementary to a regulated quantity, which is a
complementarity condition, which can be written as a projection (normal-map) row and
solved by a semismooth Newton. One mechanism, instantiated five times. Only tap
discreteness (as opposed to tap limits) genuinely needs iteration around the solve.
The note covers why the classical heuristics cycle — two tests evaluated on two different
iterates, and with two coupled controls no correct ordering exists, which is why OLF
exposes
transformerVoltageControlMode— the NCP / semismooth machinery and itsconvergence theorem, one instantiation per control, and two adjacent questions: OLF's
state-dependent slack weights (
balanceType) and the hydro produce / absorb mode case.Why it might matter here specifically
analyze+refactorizesurvives a whole sweep. The pv/pq vector disappears entirely, which makeshas_pv_changed()structurally unable to fire on a reactive-limit event —set_switchable_vm_buses/set_pv_pinned_busesstop being workarounds and become theformulation.
VoltageControlalready reserves both Jacobian slots the three-way branch needs(
h_vm_,h_slope_), and itsstrandedpath is a hand-rolled instance of the same rowswap — for a lone controller the reactive-limit case would cost zero new rows, columns
or nonzeros.
Hvdcis the existing precedent for an extension owning a state-dependent contributionwithout touching
Ybus, which is what a continuous tap ratio needs.Costs, stated in the note and not benchmarked
Dimension growth on the common grid (free in the batch path where the union layout is
already reserved, real for a plain
ac_pf); the per-control scaling constant, whoseomission is why textbook implementations fail on real grids; degeneracy exactly at a bound
and the smoothing homotopy that cures it; non-uniqueness of the AC solution set, which
this does not fix; and tap discreteness. Also a short section on why no production load
flow works this way.
History of this branch, for review
The commits are worth skipping — review the merged result. Two things went wrong and
were corrected:
branch point, false on current
dev_1.0.1:set_p_limits/get_min_p/get_max_pexist, andGenPCheck/BusQCheckalready detect violations. Thesecond commit merges
dev_1.0.1and reframes the note around thedetection → enforcement split — which improved the plan: step 1 is now "enforce the
limits that exist", with
GenPCheckas a ready-made oracle, since every row it flagstoday is a row enforcement should silence.
docs/dev_notes/(matching the convention thatdirectory established: not built, not in the toctree, not cross-referenced from the
built docs) but failed to remove the toctree entry and the
:ref:the first commithad added — a broken docs build, caught in review. Fixed in c9d4389; see the two
resolved threads for the mechanism.
Other content corrections from the sibling note's findings: tap control, area interchange
and secondary voltage control are blocked on the model, not the formulation (a
transformer stores no regulated bus / target / deadband; there is no
Areaconcept, nocontrol zones or pilot points). And a section on how enforcement interacts with
BusQCheck's per-bus argument — the check is per bus because the per-machine split is aconvention, whereas enforcement is genuinely per machine, so the two should not be
expected to agree machine-by-machine until it exists.
Verification
git diff --stat origin/dev_1.0.1...HEADtouches exactlyCHANGELOG.rstanddocs/dev_notes/nr_control_limits.md; nodocs/*.rstmentionsnr_control_limits.CHANGELOG.rstproduces the same 11 warnings asorigin/dev_1.0.1, i.e. no new ones. The docs were not built (no Sphinx in thecontainer), but nothing added here is built — Sphinx's default
source_suffixdoes notpick up
.md.VoltageControlledger slots, thestrandedrow swap,TrafoContainer's "no discrete tap",HvdcLineContainer's "doesNOT stamp the AC active power in Sbus" and
_masked_slack_weightsall still read as thenote describes.
🤖 Generated with Claude Code
https://claude.ai/code/session_01CDrx7k9yW7ppP16bid8ZDN