Skip to content

PV node: Support voltage-regulator Q limits and PV→PQ switching - #1452

Open
scud-soptim wants to merge 16 commits into
PowerGridModel:mainfrom
scud-soptim:feature/q_limit_handling_1236
Open

PV node: Support voltage-regulator Q limits and PV→PQ switching #1452
scud-soptim wants to merge 16 commits into
PowerGridModel:mainfrom
scud-soptim:feature/q_limit_handling_1236

Conversation

@scud-soptim

Copy link
Copy Markdown
Contributor

Implements #1236

Summary

This PR adds reactive-power limit handling for voltage-regulated nodes in Newton-Raphson power-flow calculations.

An active voltage_regulator now models the regulated node as a PV node by enforcing the configured voltage magnitude u_ref. If the reactive power required to maintain this voltage violates q_min or q_max, the regulated object is clamped to the violated limit and the effective node type is switched from PV to PQ.

The PR also exposes the effective node type in steady-state node output and documents the voltage-regulator behavior, including PV→PQ switching and reactive-power allocation when multiple regulators are connected to the same node.

Main changes

  • Add effective bus_type to steady-state node output.

    • 0: PQ
    • 1: PV
    • 2: Source/Slack
  • Extend voltage-regulator steady-state output with a limit indicator.

    • -1: lower reactive-power limit q_min reached
    • 0: no limit violation
    • 1: upper reactive-power limit q_max reached
  • Implement PV-node handling in Newton-Raphson power flow for active voltage regulators.

  • Implement PV→PQ switching when the required reactive power violates configured Q limits.

  • Clamp the regulated object to the violated Q limit after switching to PQ.

  • Propagate final bus type and Q-limit status to steady-state output.

  • Add and update tests for voltage-regulator Q-limit behavior, PV→PQ switching, batch updates, and output metadata.

  • Update user documentation for voltage-regulator behavior and power-flow algorithm details.

Reactive-power allocation with multiple voltage regulators

When multiple active voltage regulators are connected to the same node, they jointly regulate the same node voltage. The required reactive power is initially distributed equally over the active regulated objects. If one regulator reaches its individual q_min or q_max, it is clamped at that limit and removed from the remaining allocation. The unallocated reactive power is redistributed over the remaining regulators until all required reactive power has been allocated or all available regulators have reached a limit.

For asymmetric calculations, limit checks use the total three-phase reactive power. The phase distribution follows the available phase proportions where possible; if no usable phase proportion is available, the value is distributed equally over the three phases.

Documentation updates

The documentation now describes:

  • voltage-regulator PV-node behavior;
  • PV→PQ switching on reactive-power limit violation;
  • the meaning of node.bus_type;
  • the meaning of voltage_regulator.limit_violated;
  • reactive-power allocation for multiple voltage regulators at the same node;
  • the fact that voltage-controlled buses are supported through active voltage_regulator components in Newton-Raphson power flow.

Testing

Added and updated tests cover:

  • node output metadata for bus_type;
  • voltage-regulator output metadata for limit_violated;
  • Newton-Raphson PV-node handling;
  • PV→PQ switching on lower and upper Q-limit violations;
  • multiple voltage regulators at the same node;
  • batch calculation scenarios with voltage-regulator updates;
  • reference power-flow test data for the new PV→PQ switching case.

Notes for reviewers

  • The implementation is limited to Newton-Raphson power flow.
  • This PR does not change the public input model beyond the voltage-regulator Q-limit behavior already represented by q_min and q_max.
  • The reactive-power value is reported on the regulated load/generator output. The voltage_regulator output only reports the limit status.
  • If this PR fully resolves issue [FEATURE] Q limit handling for PV nodes #1236, the PR description can be changed from Related to #1236 to Closes #1236.

@figueroa1395 figueroa1395 added documentation Improvements or additions to documentation feature New feature or request improvement Improvement on internal implementation labels Jun 26, 2026
@figueroa1395

Copy link
Copy Markdown
Member

Hello @scud-soptim and @frie-soptim,

Thank you once more for your contribution!

Since this is a large and very involved PR, I'll give some initial comments:

  • We will start now the reviewing progress, but it will take us some time. Please be patient.
  • We'll address any question/comment presented during the review process itself.
  • As usual, feel free to ping us at any point in time if there is any urgency or you need help.

Additionally, we notice that this PR only includes the Q-limit handling and not yet the voltage-setpoint controller as described in #1236. We agree with this decision. Let's keep the scope of this PR strictly to the Q-limit handling and leave the voltage-setpoint controller for a follow up discussion. That said, I'll add a few additional remarks about this in the issue itself.

@figueroa1395 figueroa1395 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@scud-soptim , @frie-soptim As always, great job!

Some additional comments:

  • CI is broken, but it's just minor stuff.
  • I see merge conflicts due to a recently merged documentation PR. Let us know if you want us to help resolving those.
  • There's some TODOs in power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/common_solver_functions.hpp, I agree that those should be implemented.
  • The logic looks good, no comment yet about that part.
  • The rest of the review will follow tomorrow :)

Comment thread docs/algorithms/pf-algorithms.md Outdated
Comment thread code_generation/data/attribute_classes/output.json Outdated
Comment thread docs/user_manual/components.md Outdated
Comment thread docs/user_manual/components.md
Comment thread power_grid_model_c/power_grid_model/include/power_grid_model/main_core/output.hpp Outdated

@figueroa1395 figueroa1395 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some additional comments

Comment thread tests/cpp_unit_tests/component/test_voltage_regulator.cpp

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the useful description of what's going on.

Additionally, would it make sense that for the node with two regulators, one violates q_max and the other violates q_min but they compensate each other? If this makes sense, can you add such a test? The same for the situation in which both violate opposite boundaries but they aren't able to compensate and the node becomes PQ instead.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added test with two regulators on a node where one has the limis [-50, 0] and the other [0, 50]. Then, instead of distributing the calculated Q of 10 to both generators (5 each), all of it lands on one generator.

@figueroa1395 figueroa1395 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be continued. I'm almost done now :)

Comment thread power_grid_model_c/power_grid_model/include/power_grid_model/main_model_impl.hpp Outdated
Comment thread power_grid_model_c/power_grid_model/include/power_grid_model/main_model_impl.hpp Outdated
auto& bus_control = bus_control_[bus_idx];
if (!sources.empty()) {
bus_types_[bus_idx] = BusType::slack;
bus_control.type = BusType::slack;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reinforcing on earlier comments. slack type is only here for convenience but can be easily deduced as well starting from your comment. In addition, unless we would also make such changes in other solvers, I don't think we should add BusType to output types.

In addition, we had added such a check at construction time already, right? As in PGM would throw if a regulator is added where there is a source, right?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I extended SolverOutput with

template <symmetry_tag sym_type> struct SolverOutput {
    ....
    std::vector<ComplexValue<sym>> u;
    std::vector<ComplexValue<sym>> bus_injection;
    std::vector<BusSolverOutput> bus; // <-- NEW
    ....
};

struct BusSolverOutput {
    BusType bus_type{BusType::pq};
    LimitViolation q_limit_violated{LimitViolation::none};
};

because I needed at least the q_limit_violated value on the bus to control the Q distribution in common_solver_functions::calculate_voltage_regulator_result.

The problem is that common_solver_functions::calculate_pf_result is called from the class IterativePFSolver and not from NewtonRaphsonPFSolver, and in the the base class I don't have immediate access to the NR's internal array.

Alternatively, I would have to update the generator/load output.S values in the NT solver instead of in the calculate_pf_result function. And I would have to make sure that calculate_pf_result does not overwrite the calculated Q with the specified values. And, at least to me, it feels wrong to do that in the solver.

Therefore, I extended the SolverOuput as the option with the presumably least impact. But if you have a better proposal how to pass on NR values to the common_solver_functions::calculate_voltage_regulator_result function, then I would do that instead.

@frie-soptim frie-soptim Jul 20, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, we added a check that there cannot be an active Source AND and active VoltageRegulator in the same node in calculation_preparation::check_sources_and_voltage_regulators. If one of them is disabled, then it is OK.

@figueroa1395 figueroa1395 Jul 21, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me see if I can come up with something else. I'll get back to you

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit confused with the answer. I see you don't use output.bus_type on common_solver_functions::calculate_voltage_regulator_result, or did I miss it? How is that related with the question above? I think you have already access to q_limit_violated there, no?

On the other hand, maybe check by adding a small test, if not there yet, what happens when there is a regulator on a source. This should hard crash and also be added to the validator (I believe it is, but let's confirm).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that is correct, I used bus_type only for the output of node data (output.hpp and node.hpp). But I used q_limit_violated in calculate_voltage_regulator_result, which is located in the same object as the bus_type in the extended SolverOutput / BusSolverOutput (see above).
So, I did not just had access to the q_limit_violated, I still had to extend the SolverOutput. And if I would remove the bus_type, then q_limit_violated would have to remain. And that was my problem or question, how to pass a value from the NR solver to the functions in common_solver_function.hpp.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wouldn't using a function override solve this ambiguity? there is now a good reason why calculate_result for NRPF should be different from the one for the iterative current PF solver. The main reason that we didn't make the function virtual before was that there was no difference. Or do you foresee a world in which the iterative current PF solver will also have support for PV nodes?

@scud-soptim

Copy link
Copy Markdown
Contributor Author

Hi, we'll answer your questions when Frie returns from vacation.

@figueroa1395 figueroa1395 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Double checking, but is there any additional validation needed on the python side? Say anything regarding the q-limits for instance? What would happen if q_min is set to 1 and q_max to -1 by mistake? I imagine this should be catched by both the validator and a hard error thrown?

Can you add some unit testing handling sym and asym regulated loads on a node? one violating limits, both violating limits, and both not violating limits?

I think I should be done with the overall review. If I have anything else to comment on, I'll mention it once Frie is back so this doesn't grow even more.

Comment on lines +104 to +105
CHECK(imag(output.load_gen[0].s) > -1.0);
CHECK(imag(output.load_gen[0].s) < 1.0);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I imaging checking for the exact value is not practical? But double checking why a range instead? Also, why not check for q directly?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

q is an output parameter, i don't think it's available here.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is indeed checking the output though. In any case, if what I'm asking doesn't make sense, feel free to resolve this comment :)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was simply a test that the calculated Q is within boundaries. But I can check the actual value too.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's easy to do, it'd be nice to check the value, yes.

Comment thread tests/cpp_unit_tests/math_solver/test_math_solver_pf_newton_raphson.cpp Outdated
Comment thread tests/cpp_unit_tests/math_solver/test_math_solver_pf_newton_raphson.cpp Outdated
Comment thread tests/cpp_unit_tests/math_solver/test_math_solver_pf_newton_raphson.cpp Outdated
@figueroa1395

figueroa1395 commented Jul 14, 2026

Copy link
Copy Markdown
Member

Hello @scud-soptim and @frie-soptim,

Once small remark for when you return. We have enabled mandatory cryptographic signing for the PGM-org. Could you please amend your commits and either ssh or gpg sign them? This is additional to DCO signing which was already in place.

This was announced during the last community meeting, where @scud-soptim was present.

frie-soptim and others added 11 commits July 24, 2026 11:37
- add bus_type to node output
- implement feature in newton_raphson solver
- implmentet iterative equal distribution of Q at the end
  - try distributing equally
  - if individual regulator limit is violated, then save unallocated
    amount and distribute again in next iteration

Signed-off-by: Eduard Fried <eduard.fried@soptim.de>
Signed-off-by: Eduard Fried <eduard.fried@soptim.de>
Signed-off-by: Eduard Fried <eduard.fried@soptim.de>
Signed-off-by: SCUD-SOPTIM <udo.schmitz@soptim.de>
Signed-off-by: Eduard Fried <eduard.fried@soptim.de>
`cache_run` needed in Newton Raphson PF solver to determine that in case
of batching the max deviation should not be artificially set to infinity
(to trigger q limit checking) and then run into a IterationDiverge error,
because with cache_run also max_iter is set to 1

Signed-off-by: Eduard Fried <eduard.fried@soptim.de>
- cache relevant IDs of voltage_regualtors and load_gens initially
- simplify distribution by iterating over cached regulators, instead of
  iterating over load_gens and looking up regulators over and over again
- check for unallocated power after distribution and throw
  (should be purely technical, cases where distribution actually not possible
  are handled in another function)

Signed-off-by: Eduard Fried <eduard.fried@soptim.de>
- refactor q limit check triggering by using countdown variable, that starts
  limit check at iteration N, or earlier if solver converger before that
- split bus type and bus limit determination from settung reference voltage for PV buses
- remove noop call for function finalize_result() in non-NR solver
- use enum values for limit violations

Signed-off-by: Eduard Fried <eduard.fried@soptim.de>
- mark node that it required recalculation after limit violation
- in setup of jacobian only calculate row for marked buses

Signed-off-by: Eduard Fried <eduard.fried@soptim.de>
… load_gens

- specified_q on regulated load_gens should have no effect on the solution
- however, the initial linear solver considered it and calculated "wrong" start
  values for voltage. For PV buses, this voltage was replaced by the reference
  voltage of the regulator, but for PQ buses the "wrong" voltages remained
- this can lead to larger mismatches in the first iterations and thus to a potential
  limit violation and a bus switch to PQ.
- solution is to ignore specified_q values for regulated load_gens for the linear
  solver

Signed-off-by: Eduard Fried <eduard.fried@soptim.de>
Signed-off-by: Eduard Fried <eduard.fried@soptim.de>
@frie-soptim
frie-soptim force-pushed the feature/q_limit_handling_1236 branch from 8efbef8 to 153af49 Compare July 24, 2026 12:28
@frie-soptim

Copy link
Copy Markdown
Contributor

Hello @scud-soptim and @frie-soptim,

Once small remark for when you return. We have enabled mandatory cryptographic signing for the PGM-org. Could you please amend your commits and either ssh or gpg sign them? This is additional to DCO signing which was already in place.

This was announced during the last community meeting, where @scud-soptim was present.

@figueroa1395 I have pushed some new commits and rebased the branch on current main, in order to sign the older commits too. If you had checked out the branch before, then you will have to update it now.

@figueroa1395

Copy link
Copy Markdown
Member

@figueroa1395 I have pushed some new commits and rebased the branch on current main, in order to sign the older commits too. If you had checked out the branch before, then you will have to update it now.

@frie-soptim Thanks for the heads up. I'll review the new updated PR next week + resolve any outdated comments that are no longer relevant.

In addition, I've enabled the CI to run.

@mgovers mgovers changed the title Support voltage-regulator Q limits and PV→PQ switching PV node: Support voltage-regulator Q limits and PV→PQ switching Jul 27, 2026
Signed-off-by: Eduard Fried <eduard.fried@soptim.de>
Signed-off-by: Eduard Fried <eduard.fried@soptim.de>
Comment thread tests/data/power_flow/pv-node/pv-node-load-gen-const-i-y/sym_output.json Outdated
Signed-off-by: Eduard Fried <eduard.fried@soptim.de>
Signed-off-by: Eduard Fried <eduard.fried@soptim.de>
…ath_solver/newton_raphson_pf_solver.hpp

Co-authored-by: Santiago Figueroa Manrique <22895394+figueroa1395@users.noreply.github.com>
Signed-off-by: Eduard Fried <146418093+frie-soptim@users.noreply.github.com>

constexpr Idx no_limit_check = -1;
constexpr Idx do_limit_check = 0;
constexpr Idx limit_check_at_iteration = 2; // TODO(frie-soptim): maybe consider making this a parameter in the future

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean with "a parameter"?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean an option the user can specify. Currently limit checks start at iteration 2, but maybe someone wants it to start at iteration 3.

Or, in general, complex features might need a complex configuration. I don't think it is currently possible to provide such options. So, I added that todo as a reminder that this could be such a configuration option. In a way, it would be similar to a global option to disable Q-limit checks.

@frie-soptim

Copy link
Copy Markdown
Contributor

@figueroa1395 I believe I have addressed all comments or issues in my last commits. At the end, I also removed the the experimental feature check for Q limits.

I don't think I have the option to resolve any discussions, you will have to do it yourself.

If there are still any issues regarding naming or location of functions, feel free to change it as you see fit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation feature New feature or request improvement Improvement on internal implementation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants