Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix secondary voltage control in case of generator remote voltage control #1165

Merged
merged 6 commits into from
Jan 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 8 additions & 0 deletions docs/loadflow/parameters.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,16 @@ The default value is `false`.

**secondaryVoltageControl**
Whether simulation of secondary voltage control should be enabled.
Modeling of secondary voltage control has been designed to provide a fast, static, approximation of the equilibrium state of the generator reactive power
alignment process that controls the voltage of a remote pilot point.
This reactive power alignment process typically takes several minutes on the network.
The default value is `false`.

Please note that the secondaryVoltageControl implementation has the folowing limitation:
Generators that belongs to a secondary voltage control zone should be in local voltage control only.
If secondaryVoltageControl is set to `true`, generators that belongs to a secondary voltage control zone and that are configured
for remote voltage control are switched to local voltage control with an initial local target equals to remoteTarget / remoteNominalV * localNominalV .

**reactiveLimitsMaxPqPvSwitch**
When `useReactiveLimits` is set to `true`, this parameter is used to limit the number of times an equipment performing voltage control
is switching from PQ to PV type. After this number of PQ/PV type switch, the equipment will not change PV/PQ type anymore.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
import com.powsybl.openloadflow.lf.outerloop.OuterLoopStatus;
import com.powsybl.openloadflow.network.*;
import org.apache.commons.lang3.mutable.MutableDouble;
import org.apache.commons.lang3.tuple.Pair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -65,22 +64,20 @@ private static Map<Integer, Integer> buildBusIndex(List<LfBus> buses) {

static class SensitivityContext {

private final Map<Integer, Integer> busNumToSensiColumn;
private final Map<Integer, Integer> controlledBusIndex;

private final DenseMatrix sensitivities;

SensitivityContext(Map<Integer, Integer> busNumToSensiColumn, DenseMatrix sensitivities) {
this.busNumToSensiColumn = Objects.requireNonNull(busNumToSensiColumn);
SensitivityContext(Map<Integer, Integer> controlledBusIndex, DenseMatrix sensitivities) {
this.controlledBusIndex = Objects.requireNonNull(controlledBusIndex);
this.sensitivities = Objects.requireNonNull(sensitivities);
}

static SensitivityContext create(List<LfBus> controlledBuses, AcLoadFlowContext context) {
var busNumToSensiColumn = buildBusIndex(controlledBuses);

DenseMatrix sensitivities = calculateSensitivityValues(controlledBuses, busNumToSensiColumn, context.getEquationSystem(),
static SensitivityContext create(List<LfBus> controlledBuses, Map<Integer, Integer> controlledBusIndex, AcLoadFlowContext context) {
DenseMatrix sensitivities = calculateSensitivityValues(controlledBuses, controlledBusIndex, context.getEquationSystem(),
context.getJacobianMatrix());

return new SensitivityContext(busNumToSensiColumn, sensitivities);
return new SensitivityContext(controlledBusIndex, sensitivities);
}

private static DenseMatrix calculateSensitivityValues(List<LfBus> controlledBuses, Map<Integer, Integer> busNumToSensiColumn,
Expand Down Expand Up @@ -124,7 +121,7 @@ private static EquationTerm<AcEquationType, AcEquationType> getQ(LfShunt shunt)
* Calculate controlled bus voltage to controller bus reactive power injection sensitivity.
*/
double calculateSensiQ(LfBus controllerBus, LfBus controlledBus) {
int controlledBusSensiColumn = busNumToSensiColumn.get(controlledBus.getNum());
int controlledBusSensiColumn = controlledBusIndex.get(controlledBus.getNum());

MutableDouble sq = new MutableDouble();
for (LfBranch branch : controllerBus.getBranches()) {
Expand All @@ -147,7 +144,7 @@ private static EquationTerm<AcEquationType, AcEquationType> getQ(LfShunt shunt)
* Calculate controlled buses voltage to pilot bus voltage sensitivities.
*/
double calculateSensiVpp(LfBus controlledBus, LfBus pilotBus) {
int controlledBusSensiColumn = busNumToSensiColumn.get(controlledBus.getNum());
int controlledBusSensiColumn = controlledBusIndex.get(controlledBus.getNum());
return getCalculatedV(pilotBus).calculateSensi(sensitivities, controlledBusSensiColumn);
}
}
Expand All @@ -162,12 +159,12 @@ private static double calculateK(LfBus controllerBus) {
return qToK(q, controllerBus);
}

private static DenseMatrix createA(List<LfSecondaryVoltageControl> secondaryVoltageControls, Map<Integer, Integer> controllerBusIndex) {
int n = controllerBusIndex.size();
DenseMatrix a = new DenseMatrix(n, n);
private static DenseMatrix createA(List<LfSecondaryVoltageControl> secondaryVoltageControls,
List<LfBus> allControllerBuses, Map<Integer, Integer> controllerBusIndex) {
DenseMatrix a = new DenseMatrix(allControllerBuses.size(), allControllerBuses.size());
// build a block in the global matrix for each of the secondary voltage control
for (LfSecondaryVoltageControl secondaryVoltageControl : secondaryVoltageControls) {
List<LfBus> controllerBuses = secondaryVoltageControl.getControllerBuses();
List<LfBus> controllerBuses = secondaryVoltageControl.getEnabledControllerBuses();
int nControl = controllerBuses.size();
for (LfBus controllerBusI : controllerBuses) {
for (LfBus controllerBusJ : controllerBuses) {
Expand All @@ -181,35 +178,33 @@ private static DenseMatrix createA(List<LfSecondaryVoltageControl> secondaryVolt
}

private static DenseMatrix createK0(List<LfBus> controllerBuses, Map<Integer, Integer> controllerBusIndex) {
int n = controllerBuses.size();
DenseMatrix k0 = new DenseMatrix(n, 1);
DenseMatrix k0 = new DenseMatrix(controllerBuses.size(), 1);
for (LfBus controllerBus : controllerBuses) {
int i = controllerBusIndex.get(controllerBus.getNum());
k0.set(i, 0, calculateK(controllerBus));
}
return k0;
}

private static DenseMatrix createJk(SensitivityContext sensitivityContext, List<LfBus> controllerBuses, Map<Integer, Integer> controllerBusIndex) {
int n = controllerBuses.size();
DenseMatrix jK = new DenseMatrix(n, n);
for (LfBus controllerBusI : controllerBuses) {
private static DenseMatrix createJk(SensitivityContext sensitivityContext,
List<LfBus> controllerBuses, List<LfBus> controlledBuses,
Map<Integer, Integer> controllerBusIndex, Map<Integer, Integer> controlledBusIndex) {
vidaldid-rte marked this conversation as resolved.
Show resolved Hide resolved
DenseMatrix jK = new DenseMatrix(controlledBuses.size(), controllerBuses.size());
for (LfBus controlledBusI : controlledBuses) {
for (LfBus controllerBusJ : controllerBuses) {
int i = controllerBusIndex.get(controllerBusI.getNum());
int i = controlledBusIndex.get(controlledBusI.getNum());
int j = controllerBusIndex.get(controllerBusJ.getNum());
LfBus controlledBus2 = controllerBusJ.getGeneratorVoltageControl().orElseThrow().getControlledBus();
jK.set(i, j, sensitivityContext.calculateSensiK(controllerBusI, controlledBus2));
jK.set(i, j, sensitivityContext.calculateSensiK(controllerBusJ, controlledBusI));
}
}
return jK;
}

private static DenseMatrix createJvpp(SensitivityContext sensitivityContext, LfBus pilotBus, List<LfBus> controllerBuses, Map<Integer, Integer> controllerBusIndex) {
int n = controllerBuses.size();
DenseMatrix jVpp = new DenseMatrix(n, 1);
for (LfBus controllerBus : controllerBuses) {
int i = controllerBusIndex.get(controllerBus.getNum());
LfBus controlledBus = controllerBus.getGeneratorVoltageControl().orElseThrow().getControlledBus();
private static DenseMatrix createJvpp(SensitivityContext sensitivityContext, LfBus pilotBus,
List<LfBus> controlledBuses, Map<Integer, Integer> controlledBusIndex) {
DenseMatrix jVpp = new DenseMatrix(controlledBuses.size(), 1);
for (LfBus controlledBus : controlledBuses) {
int i = controlledBusIndex.get(controlledBus.getNum());
jVpp.set(i, 0, sensitivityContext.calculateSensiVpp(controlledBus, pilotBus));
}
return jVpp;
Expand All @@ -231,7 +226,7 @@ private static double calculatePilotPointDv(LfSecondaryVoltageControl secondaryV
}

private Optional<List<String>> processSecondaryVoltageControl(List<LfSecondaryVoltageControl> secondaryVoltageControls,
SensitivityContext sensitivityContext) {
AcLoadFlowContext loadFlowContext) {
List<String> adjustedZoneNames = new ArrayList<>();

for (LfSecondaryVoltageControl secondaryVoltageControl : secondaryVoltageControls) {
Expand All @@ -257,25 +252,37 @@ private Optional<List<String>> processSecondaryVoltageControl(List<LfSecondaryVo
.flatMap(control -> control.getEnabledControllerBuses().stream())
.toList();

List<LfBus> allControlledBuses = allControllerBuses.stream()
.map(b -> b.getGeneratorVoltageControl().orElseThrow().getControlledBus())
.distinct()
.toList();

var controllerBusIndex = buildBusIndex(allControllerBuses);
var controlledBusIndex = buildBusIndex(allControlledBuses);

// compute target voltage sensitivities for all controlled buses
SensitivityContext sensitivityContext = SensitivityContext.create(allControlledBuses, controlledBusIndex, loadFlowContext);

DenseMatrix a = createA(secondaryVoltageControls, controllerBusIndex);
DenseMatrix a = createA(secondaryVoltageControls, allControllerBuses, controllerBusIndex);

DenseMatrix k0 = createK0(allControllerBuses, controllerBusIndex);

DenseMatrix rhs = a.times(k0, -1);

DenseMatrix jK = createJk(sensitivityContext, allControllerBuses, controllerBusIndex);

DenseMatrix jK = createJk(sensitivityContext, allControllerBuses, allControlledBuses, controllerBusIndex, controlledBusIndex).transpose();
DenseMatrix b = a.times(jK);

// replace last row for each of the secondary voltage control block
for (LfSecondaryVoltageControl secondaryVoltageControl : secondaryVoltageControls) {
List<LfBus> controllerBuses = secondaryVoltageControl.getControllerBuses();
LfBus lastControllerBus = controllerBuses.get(controllerBuses.size() - 1);
int i = controllerBusIndex.get(lastControllerBus.getNum());

DenseMatrix jVpp = createJvpp(sensitivityContext, secondaryVoltageControl.getPilotBus(), allControllerBuses, controllerBusIndex);
List<LfBus> controllerBuses = secondaryVoltageControl.getEnabledControllerBuses();
List<LfBus> controlledBuses = controllerBuses.stream()
.map(bus -> bus.getGeneratorVoltageControl().orElseThrow().getControlledBus())
.distinct()
.toList();
LfBus lastControlledBus = controlledBuses.get(controlledBuses.size() - 1);
int i = controlledBusIndex.get(lastControlledBus.getNum());

DenseMatrix jVpp = createJvpp(sensitivityContext, secondaryVoltageControl.getPilotBus(), allControlledBuses, controlledBusIndex);
for (int j = 0; j < b.getColumnCount(); j++) {
b.set(i, j, jVpp.get(j, 0));
}
Expand All @@ -292,10 +299,9 @@ private Optional<List<String>> processSecondaryVoltageControl(List<LfSecondaryVo

// compute new controlled bus target voltages
Map<LfBus, Double> newControlledBusTargetV = new HashMap<>(allControllerBuses.size());
for (LfBus controllerBus : allControllerBuses) {
int i = controllerBusIndex.get(controllerBus.getNum());
var vc = controllerBus.getGeneratorVoltageControl().orElseThrow();
LfBus controlledBus = vc.getControlledBus();
for (LfBus controlledBus : allControlledBuses) {
int i = controlledBusIndex.get(controlledBus.getNum());
var vc = controlledBus.getGeneratorVoltageControl().orElseThrow();
double newTargetV = vc.getTargetValue() + dv.get(i, 0);
newControlledBusTargetV.put(controlledBus, newTargetV);
}
Expand All @@ -306,13 +312,7 @@ private Optional<List<String>> processSecondaryVoltageControl(List<LfSecondaryVo
double newTargetV = e.getValue();
return !VoltageControl.isTargetVoltagePlausible(newTargetV, minPlausibleTargetVoltage, maxPlausibleTargetVoltage);
})
.map(e -> {
// convert target to Kv for better display
LfBus controlledBus = e.getKey();
double newTargetV = e.getValue();
return Pair.of(controlledBus, newTargetV * controlledBus.getNominalV());
})
.collect(Collectors.toMap(Pair::getKey, Pair::getValue));
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
if (notPlausibleNewControlledBusTargetV.isEmpty()) {
for (var e : newControlledBusTargetV.entrySet()) {
LfBus controlledBus = e.getKey();
Expand Down Expand Up @@ -369,15 +369,9 @@ public OuterLoopResult check(AcOuterLoopContext context, ReportNode reportNode)
return new OuterLoopResult(this, OuterLoopStatus.STABLE);
}

// compute target voltage sensitivities for all controlled buses
List<LfBus> allControlledBuses = secondaryVoltageControls.stream()
.flatMap(control -> control.getControlledBuses().stream())
.toList();
SensitivityContext sensitivityContext = SensitivityContext.create(allControlledBuses, context.getLoadFlowContext());

OuterLoopStatus status = OuterLoopStatus.STABLE;

List<String> adjustedZoneNames = processSecondaryVoltageControl(secondaryVoltageControls, sensitivityContext).orElse(null);
List<String> adjustedZoneNames = processSecondaryVoltageControl(secondaryVoltageControls, context.getLoadFlowContext()).orElse(null);
if (adjustedZoneNames == null) {
status = OuterLoopStatus.FAILED;
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/
package com.powsybl.openloadflow.network;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

Expand Down Expand Up @@ -80,4 +81,23 @@ public void updateReactiveKeys() {
controllerBus.setRemoteControlReactivePercent(reactiveKeysSum == 0 ? 0 : reactiveKeys[i] / reactiveKeysSum);
}
}

/**
* Returns itself in case of local control, split into several local voltage controls in case of remote control.
*/
public List<GeneratorVoltageControl> toLocalVoltageControls() {
if (isLocalControl()) {
return List.of(this);
} else {
List<GeneratorVoltageControl> generatorVoltageControls = new ArrayList<>(controllerElements.size());
// create one (local) generator control per controller bus and remove this one
controlledBus.setGeneratorVoltageControl(null);
for (LfBus controllerBus : controllerElements) {
var generatorVoltageControl = new GeneratorVoltageControl(controllerBus, targetPriority, targetValue);
generatorVoltageControl.addControllerElement(controllerBus);
vidaldid-rte marked this conversation as resolved.
Show resolved Hide resolved
generatorVoltageControls.add(generatorVoltageControl);
}
return generatorVoltageControls;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ public void removeParticipatingControlUnit(String id) {
public Set<GeneratorVoltageControl> getGeneratorVoltageControls() {
return generatorVoltageControls.stream()
.filter(this::hasAtLeastOneParticipatingControlUnit) // only keep voltage controls where there is at list one enabled control unit
.collect(Collectors.toSet());
.collect(Collectors.toCollection(LinkedHashSet::new));
}

private boolean hasAtLeastOneParticipatingControlUnit(GeneratorVoltageControl vc) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,11 +185,15 @@ public Optional<GeneratorVoltageControl> getGeneratorVoltageControl() {

@Override
public void setGeneratorVoltageControl(GeneratorVoltageControl generatorVoltageControl) {
this.generatorVoltageControl = Objects.requireNonNull(generatorVoltageControl);
if (hasGeneratorVoltageControllerCapability()) {
this.generatorVoltageControlEnabled = true;
} else if (!isGeneratorVoltageControlled()) {
throw new PowsyblException("Setting inconsistent voltage control to bus " + getId());
this.generatorVoltageControl = generatorVoltageControl;
if (generatorVoltageControl != null) {
if (hasGeneratorVoltageControllerCapability()) {
this.generatorVoltageControlEnabled = true;
} else if (!isGeneratorVoltageControlled()) {
throw new PowsyblException("Setting inconsistent voltage control to bus " + getId());
}
} else {
this.generatorVoltageControlEnabled = false;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1105,11 +1105,20 @@ private static void createSecondaryVoltageControls(Network network, LfNetworkPar
// filter missing control units and find corresponding primary voltage control, controlled bus
Set<GeneratorVoltageControl> generatorVoltageControls = findControlZoneGeneratorVoltageControl(network, parameters, lfNetwork, controlZone);
if (!generatorVoltageControls.isEmpty()) {
// remove remote control for generators that belongs to a secondary voltage control zone because
// - it does not make sens to mix generator remote control plus pilot point remote control
// - it cannot work in case of generator shared voltage control (no way to align K of generators
// for a given shared voltage control as a unique controlled bus cannot help to align reactive
// power of generators)
Set<GeneratorVoltageControl> splitGeneratorVoltageControls = generatorVoltageControls.stream()
.flatMap(vc -> vc.toLocalVoltageControls().stream())
.collect(Collectors.toCollection(LinkedHashSet::new));

Set<String> participatingControlUnitIds = controlZone.getControlUnits().stream()
.filter(ControlUnit::isParticipate)
.map(ControlUnit::getId).collect(Collectors.toSet());
var lfSvc = new LfSecondaryVoltageControl(controlZone.getName(), lfPilotBus, targetV,
participatingControlUnitIds, generatorVoltageControls);
participatingControlUnitIds, splitGeneratorVoltageControls);
lfNetwork.addSecondaryVoltageControl(lfSvc);
}
}
Expand Down
Loading
Loading