Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
a1f0e28
#185 initial PV Node implementation
frie-soptim Dec 11, 2025
8223f34
#185 improve calculation of jacobian matrix, distribute bus injection…
frie-soptim Dec 11, 2025
14385e9
#185 adjust result calculation for pv generators and source
frie-soptim Dec 11, 2025
7018653
#185 add asymmetric case for distribution of q on pv generators
frie-soptim Dec 11, 2025
f0e3e3d
#185 handle disconnected pv generators by changing their type to cons…
frie-soptim Dec 11, 2025
f941c6a
#185 simplify delta q calculation for pv bus
frie-soptim Dec 11, 2025
75e2712
- add new component voltage_regulator and the necessary boilerpate
frie-soptim Dec 11, 2025
74855cc
#185 add python tests
frie-soptim Dec 11, 2025
6bd31b2
#185 extend update object for voltage_regulation
frie-soptim Dec 11, 2025
87968f6
#185 add documentation for the voltage_regulator
frie-soptim Dec 11, 2025
e910ac0
#185 added test networks for pv node tests
frie-soptim Dec 11, 2025
f427319
fix windows build
frie-soptim Dec 11, 2025
17e9f31
Merge branch 'main' into feature/#185PV-Node
scud-soptim Dec 12, 2025
a39d17e
#185 changes from code review
frie-soptim Dec 16, 2025
3b5a694
#185 declare voltage regulation as an experimental feature for now
frie-soptim Dec 16, 2025
5d60a9b
#185 add more unit/validation tests
frie-soptim Dec 16, 2025
a29ba67
#185 update documentation
frie-soptim Dec 16, 2025
b63dbb8
fix code quality error
frie-soptim Dec 16, 2025
eb9e104
#185 move voltage regulator code into newton_raphson_pf_solver
frie-soptim Dec 16, 2025
e22d164
update tests
frie-soptim Dec 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions code_generation/data/attribute_classes/input.json
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@
"name": "GenericBranchInput",
"base": "BranchInput",
"attributes": [
{
{
"data_type": "double",
"names": ["r1","x1","g1","b1"],
"description": "positive sequence parameters"
Expand Down Expand Up @@ -540,6 +540,28 @@
}
]
},
{
"name": "VoltageRegulatorInput",
"base": "RegulatorInput",
"is_template": false,
"attributes": [
{
"data_type": "double",
"names": [
"u_ref"
],
"description": "reference voltage"
},
{
"data_type": "double",
"names": [
"q_min",
"q_max"
],
"description": "reactive power limits"
}
]
},
{
"name": "GenericCurrentSensorInput",
"base": "SensorInput",
Expand All @@ -557,7 +579,7 @@
{
"data_type": "double",
"names": [
"i_sigma",
"i_sigma",
"i_angle_sigma"
],
"description": "sigma of error margin of current (angle) measurement"
Expand Down
19 changes: 19 additions & 0 deletions code_generation/data/attribute_classes/output.json
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,25 @@
}
]
},
{
"name": "VoltageRegulatorOutput",
"base": "BaseOutput",
"is_template": true,
"attributes": [
{
"data_type": "IntS",
"names": "limit_violated",
"description": "indicates whether voltage limits are violated"
},
{
"data_type": "RealValue<sym>",
"names": [
"q"
],
"description": "additional reactive power injected by the voltage regulator"
}
]
},
{
"name": "RegulatorShortCircuitOutput",
"base": "BaseOutput",
Expand Down
21 changes: 20 additions & 1 deletion code_generation/data/attribute_classes/update.json
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,25 @@
}
]
},
{
"name": "VoltageRegulatorUpdate",
"base": "RegulatorUpdate",
"attributes": [
{
"data_type": "double",
"names": "u_ref",
"description": "reference voltage"
},
{
"data_type": "double",
"names": [
"q_min",
"q_max"
],
"description": "reactive power limits"
}
]
},
{
"name": "CurrentSensorUpdate",
"base": "BaseUpdate",
Expand All @@ -252,7 +271,7 @@
{
"data_type": "double",
"names": [
"i_sigma",
"i_sigma",
"i_angle_sigma"
],
"description": "sigma of error margin of current (angle) measurement"
Expand Down
12 changes: 12 additions & 0 deletions code_generation/data/dataset_class_maps/dataset_definitions.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@
"names": ["sym_load", "sym_gen", "asym_load", "asym_gen"],
"class_name": "LoadGenInput"
},
{
"names": ["voltage_regulator"],
"class_name": "VoltageRegulatorInput"
},
{
"names": ["shunt"],
"class_name": "ShuntInput"
Expand Down Expand Up @@ -90,6 +94,10 @@
"names": ["sym_load", "sym_gen", "asym_load", "asym_gen", "shunt", "source"],
"class_name": "ApplianceOutput"
},
{
"names": ["voltage_regulator"],
"class_name": "VoltageRegulatorOutput"
},
{
"names": ["sym_voltage_sensor", "asym_voltage_sensor"],
"class_name": "VoltageSensorOutput"
Expand Down Expand Up @@ -144,6 +152,10 @@
"names": ["sym_load", "sym_gen", "asym_load", "asym_gen"],
"class_name": "LoadGenUpdate"
},
{
"names": ["voltage_regulator"],
"class_name": "VoltageRegulatorUpdate"
},
{
"names": ["shunt"],
"class_name": "ApplianceUpdate"
Expand Down
81 changes: 78 additions & 3 deletions docs/user_manual/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ Physically a node can be a busbar, a joint, or other similar component.
| `q` | `RealValueOutput` | volt-ampere-reactive (var) | reactive power injection |

```{note}
The `p` and `q` output of injection follows the `generator` reference direction as mentioned in
The `p` and `q` output of injection follows the `generator` reference direction as mentioned in
{hoverxreftooltip}`user_manual/data-model:Reference Direction`
```

Expand Down Expand Up @@ -256,7 +256,7 @@ $$

where $z_{\text{base,transformer}} = 1 / y_{\text{base,transformer}} = {u_{\text{2}}}^2 / s_{\text{n}}$.

### Generic Branch
### Generic Branch

* type name: `generic_branch`

Expand Down Expand Up @@ -992,7 +992,7 @@ per method on how the variances are taken into account for both the global and l
individual phases.

```{note}
The combination of `i_measured=0` and `i_angle_measured=nπ/2` renders the current sensor invalid for PGM.
The combination of `i_measured=0` and `i_angle_measured=nπ/2` renders the current sensor invalid for PGM.
See [State estimate sensor transformations](calculations.md#state-estimate-sensor-transformations).
```

Expand Down Expand Up @@ -1245,3 +1245,78 @@ node_1 --- transformer_4 --- node_2 --- line_5 --- node_3
source_6 | load_7
transformer_tap_regulator_8
```

### Voltage Regulator
Copy link
Member

Choose a reason for hiding this comment

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

probably good to explicitly mention that only Newton-Raphson calculation method is supported

Choose a reason for hiding this comment

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

done


* type name: `voltage_regulator`
* base: {hoverxreftooltip}`user_manual/components:regulator`

`voltage_regulator` defines a regulator for voltage-controlled generators in the grid.
A voltage regulator adjusts the reactive power output of a generator to maintain the voltage at its connection node at a specified setpoint.

The voltage regulator changes the reactive power output of the generator it regulates to achieve the reference voltage `u_ref` at the generator's node.
If `q_min` and `q_max` are provided, the reactive power is constrained within this range (i.e., `q_min <= q <= q_max` or `q_min >= q >= q_max`).
If these limits are not provided, the reactive power can take any value needed to maintain the voltage setpoint.

```{warning}
Voltage regulation is only supported by the [Newton-Raphson](#newton-raphson-power-flow) method as an experimental feature.
```

```{note}
The `regulated_object` must reference a generator (`sym_gen` or `asym_gen`) or a load (`sym_load` or `asym_load`).
Each generator or load can have at most one voltage regulator.
When multiple voltage-regulated generators are connected to the same node, they should all specify the same `u_ref` value to avoid conflicting voltage setpoints.
```

```{warning}
Reactive power limit checking is not yet fully implemented. When `q_min` and `q_max` are specified, the intended behavior is that if the required reactive power to maintain `u_ref` exceeds these limits, the voltage regulator should operate at the limit and the voltage may deviate from `u_ref`.
```

#### Input

| name | data type | unit | description | required | update | valid values |
| -------- | --------- | -------------------------- | --------------------------------------------------- | :--------------------------: | :------: | :----------: |
| `u_ref` | `double` | - | reference voltage in per-unit at the generator node | &#10024; only for power flow | &#10004; | `> 0` |
| `q_min` | `double` | volt-ampere-reactive (var) | minimum reactive power limit of the generator | &#10060; | &#10004; | |
| `q_max` | `double` | volt-ampere-reactive (var) | maximum reactive power limit of the generator | &#10060; | &#10004; | |

#### Steady state output

| name | data type | unit | description |
| ----------------- | ----------------- | -------------------------- | -------------------------------------------------------------------- |
| `q` | `RealValueOutput` | volt-ampere-reactive (var) | reactive power provided by the voltage regulator |
| `limit_violated` | `int8_t` | - | reactive power limit violation indicator (not yet fully implemented) |
Copy link
Member

Choose a reason for hiding this comment

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

unrelated to this PR but maybe we should add such a flag to the tap regulator as well. Relates to #1215

Copy link
Contributor Author

Choose a reason for hiding this comment

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

this comment does not relate to SOPTIM


#### Short circuit output

A `voltage_regulator` has no short circuit output.

#### Electric Model

The voltage regulator controls the generator to behave as a **PV node** in power flow calculations:

$$
\begin{eqnarray}
& P_{\text{gen}} = P_{\text{specified}} \\
& |U_{\text{node}}| = U_{\text{ref}} \\
& Q_{\text{gen}} = \text{calculated to satisfy } U_{\text{ref}}
\end{eqnarray}
$$

When `q_min` and `q_max` are provided, the reactive power should be constrained:

$$
Q_{\text{min}} \leq Q_{\text{gen}} \leq Q_{\text{max}}
$$

When fully implemented, if the reactive power constraints are violated, the generator will operate at the limit and the node becomes a PQ node:

$$
\begin{eqnarray}
& P_{\text{gen}} = P_{\text{specified}} \\
& Q_{\text{gen}} = Q_{\text{min}} \text{ or } Q_{\text{max}} \\
& |U_{\text{node}}| = \text{calculated from power flow}
\end{eqnarray}
$$

In this case, `limit_violated` will indicate which limit was exceeded, and the actual voltage at the node may differ from `u_ref`.
Comment on lines +1312 to +1322
Copy link
Member

Choose a reason for hiding this comment

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

since this is not fully implemented yet, do we want to explicitly mention the current behavior with a big warning note that this is temporary behavior?

Copy link
Member

Choose a reason for hiding this comment

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

Alternatively, you may add this as an experimental feature temporarily. That way, it's fine if things will change but we can already merge it and it can already be tested by users, but we do not settle on final results yet.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hi, could you describe how to implement an experimental feature - otherwise we would prefer a warn-message.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

a warning-note!

Copy link
Member

@mgovers mgovers Dec 16, 2025

Choose a reason for hiding this comment

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

We have a function check_no_experimental_features_used in main_model_impl.hpp.

See e.g. #1062 in which we removed the experimental feature flag for current sensors: ( https://github.com/PowerGridModel/power-grid-model/pull/1062/changes#diff-cc6d6a55667a569640705486e5990498b32f2a3ecf9fec770922a5b69a71920a )

You can add a similar check that searches for voltage regulators in power flow calculations.

Choose a reason for hiding this comment

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

using experimental feature

Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,15 @@
#include "component/three_winding_transformer.hpp"
#include "component/transformer.hpp"
#include "component/transformer_tap_regulator.hpp"
#include "component/voltage_regulator.hpp"
#include "component/voltage_sensor.hpp"

namespace power_grid_model {

using AllComponents =
ComponentList<Node, Line, AsymLine, Link, GenericBranch, Transformer, ThreeWindingTransformer, Shunt, Source,
SymGenerator, AsymGenerator, SymLoad, AsymLoad, SymPowerSensor, AsymPowerSensor, SymVoltageSensor,
AsymVoltageSensor, SymCurrentSensor, AsymCurrentSensor, Fault, TransformerTapRegulator>;
AsymVoltageSensor, SymCurrentSensor, AsymCurrentSensor, Fault, TransformerTapRegulator, VoltageRegulator>;
Copy link
Member

Choose a reason for hiding this comment

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

Also Nice to add a check in dependent_type_check at power_grid_model_c/power_grid_model/include/power_grid_model/main_core/main_model_type.hpp

In this case it should be dependent_type_check<CompList, VoltageRegulator, SymGenerator, AsymGenerator, SymLoad, AsymLoad> alongside dependent checks for other components.

Choose a reason for hiding this comment

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

done


using AllExtraRetrievableTypes =
ExtraRetrievableTypes<Base, Node, Branch, Branch3, Appliance, GenericLoadGen, GenericLoad, GenericGenerator,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,23 @@ struct TransformerTapRegulatorInput {
operator RegulatorInput const&() const { return reinterpret_cast<RegulatorInput const&>(*this); }
};

struct VoltageRegulatorInput {
ID id{na_IntID}; // ID of the object
ID regulated_object{na_IntID}; // ID of the regulated object
IntS status{na_IntS}; // regulator enabled
double u_ref{nan}; // reference voltage
double q_min{nan}; // reactive power limits
double q_max{nan}; // reactive power limits

// implicit conversions to BaseInput
operator BaseInput&() { return reinterpret_cast<BaseInput&>(*this); }
operator BaseInput const&() const { return reinterpret_cast<BaseInput const&>(*this); }

// implicit conversions to RegulatorInput
operator RegulatorInput&() { return reinterpret_cast<RegulatorInput&>(*this); }
operator RegulatorInput const&() const { return reinterpret_cast<RegulatorInput const&>(*this); }
};

struct GenericCurrentSensorInput {
ID id{na_IntID}; // ID of the object
ID measured_object{na_IntID}; // ID of the measured object
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,20 @@ struct get_attributes_list<TransformerTapRegulatorInput> {
};
};

template<>
struct get_attributes_list<VoltageRegulatorInput> {
static constexpr std::array<MetaAttribute, 6> value{
// all attributes including base class

meta_data_gen::get_meta_attribute<&VoltageRegulatorInput::id>(offsetof(VoltageRegulatorInput, id), "id"),
meta_data_gen::get_meta_attribute<&VoltageRegulatorInput::regulated_object>(offsetof(VoltageRegulatorInput, regulated_object), "regulated_object"),
meta_data_gen::get_meta_attribute<&VoltageRegulatorInput::status>(offsetof(VoltageRegulatorInput, status), "status"),
meta_data_gen::get_meta_attribute<&VoltageRegulatorInput::u_ref>(offsetof(VoltageRegulatorInput, u_ref), "u_ref"),
meta_data_gen::get_meta_attribute<&VoltageRegulatorInput::q_min>(offsetof(VoltageRegulatorInput, q_min), "q_min"),
meta_data_gen::get_meta_attribute<&VoltageRegulatorInput::q_max>(offsetof(VoltageRegulatorInput, q_max), "q_max"),
};
};

template<>
struct get_attributes_list<GenericCurrentSensorInput> {
static constexpr std::array<MetaAttribute, 6> value{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,20 @@ struct get_attributes_list<TransformerTapRegulatorOutput> {
};
};

template <symmetry_tag sym_type>
struct get_attributes_list<VoltageRegulatorOutput<sym_type>> {
using sym = sym_type;

static constexpr std::array<MetaAttribute, 4> value{
// all attributes including base class

meta_data_gen::get_meta_attribute<&VoltageRegulatorOutput<sym>::id>(offsetof(VoltageRegulatorOutput<sym>, id), "id"),
meta_data_gen::get_meta_attribute<&VoltageRegulatorOutput<sym>::energized>(offsetof(VoltageRegulatorOutput<sym>, energized), "energized"),
meta_data_gen::get_meta_attribute<&VoltageRegulatorOutput<sym>::limit_violated>(offsetof(VoltageRegulatorOutput<sym>, limit_violated), "limit_violated"),
meta_data_gen::get_meta_attribute<&VoltageRegulatorOutput<sym>::q>(offsetof(VoltageRegulatorOutput<sym>, q), "q"),
};
};

template<>
struct get_attributes_list<RegulatorShortCircuitOutput> {
static constexpr std::array<MetaAttribute, 2> value{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,19 @@ struct get_attributes_list<TransformerTapRegulatorUpdate> {
};
};

template<>
struct get_attributes_list<VoltageRegulatorUpdate> {
static constexpr std::array<MetaAttribute, 5> value{
// all attributes including base class

meta_data_gen::get_meta_attribute<&VoltageRegulatorUpdate::id>(offsetof(VoltageRegulatorUpdate, id), "id"),
meta_data_gen::get_meta_attribute<&VoltageRegulatorUpdate::status>(offsetof(VoltageRegulatorUpdate, status), "status"),
meta_data_gen::get_meta_attribute<&VoltageRegulatorUpdate::u_ref>(offsetof(VoltageRegulatorUpdate, u_ref), "u_ref"),
meta_data_gen::get_meta_attribute<&VoltageRegulatorUpdate::q_min>(offsetof(VoltageRegulatorUpdate, q_min), "q_min"),
meta_data_gen::get_meta_attribute<&VoltageRegulatorUpdate::q_max>(offsetof(VoltageRegulatorUpdate, q_max), "q_max"),
};
};

template <symmetry_tag sym_type>
struct get_attributes_list<CurrentSensorUpdate<sym_type>> {
using sym = sym_type;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,23 @@ struct TransformerTapRegulatorOutput {
operator BaseOutput const&() const { return reinterpret_cast<BaseOutput const&>(*this); }
};

template <symmetry_tag sym_type>
struct VoltageRegulatorOutput {
using sym = sym_type;

ID id{na_IntID}; // ID of the object
IntS energized{na_IntS}; // whether the object is energized
IntS limit_violated{na_IntS}; // indicates whether voltage limits are violated
RealValue<sym> q{nan}; // additional reactive power injected by the voltage regulator

// implicit conversions to BaseOutput
operator BaseOutput&() { return reinterpret_cast<BaseOutput&>(*this); }
operator BaseOutput const&() const { return reinterpret_cast<BaseOutput const&>(*this); }
};

using SymVoltageRegulatorOutput = VoltageRegulatorOutput<symmetric_t>;
using AsymVoltageRegulatorOutput = VoltageRegulatorOutput<asymmetric_t>;

struct RegulatorShortCircuitOutput {
ID id{na_IntID}; // ID of the object
IntS energized{na_IntS}; // whether the object is energized
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,21 @@ static_assert(offsetof(TransformerTapRegulatorInput, id) == offsetof(RegulatorIn
static_assert(offsetof(TransformerTapRegulatorInput, regulated_object) == offsetof(RegulatorInput, regulated_object));
static_assert(offsetof(TransformerTapRegulatorInput, status) == offsetof(RegulatorInput, status));

// static asserts for VoltageRegulatorInput
static_assert(std::is_standard_layout_v<VoltageRegulatorInput>);
// static asserts for conversion of VoltageRegulatorInput to BaseInput
static_assert(std::alignment_of_v<VoltageRegulatorInput> >= std::alignment_of_v<RegulatorInput>);
static_assert(std::same_as<decltype(VoltageRegulatorInput::id), decltype(BaseInput::id)>);
static_assert(offsetof(VoltageRegulatorInput, id) == offsetof(BaseInput, id));
// static asserts for conversion of VoltageRegulatorInput to RegulatorInput
static_assert(std::alignment_of_v<VoltageRegulatorInput> >= std::alignment_of_v<RegulatorInput>);
static_assert(std::same_as<decltype(VoltageRegulatorInput::id), decltype(RegulatorInput::id)>);
static_assert(std::same_as<decltype(VoltageRegulatorInput::regulated_object), decltype(RegulatorInput::regulated_object)>);
static_assert(std::same_as<decltype(VoltageRegulatorInput::status), decltype(RegulatorInput::status)>);
static_assert(offsetof(VoltageRegulatorInput, id) == offsetof(RegulatorInput, id));
static_assert(offsetof(VoltageRegulatorInput, regulated_object) == offsetof(RegulatorInput, regulated_object));
static_assert(offsetof(VoltageRegulatorInput, status) == offsetof(RegulatorInput, status));

// static asserts for GenericCurrentSensorInput
static_assert(std::is_standard_layout_v<GenericCurrentSensorInput>);
// static asserts for conversion of GenericCurrentSensorInput to BaseInput
Expand Down
Loading
Loading