Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 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
18 changes: 15 additions & 3 deletions docs/loadflow/loadflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,19 +237,19 @@ Where:
* "Interchange Target" is the interchange target parameter of the area.
* "Slack Injection" is the active power mismatch of the slack bus(es) present in the area (see [Slack bus mismatch attribution](#slack-bus-mismatch-attribution)).

The outer loop iterates until this mismatch is below the configured [parameter `areaInterchangePMaxMismatch`](parameters.md) for all areas.
The outer loop iterates until the absolute value of this mismatch is below the configured [parameter `areaInterchangePMaxMismatch`](parameters.md) for all areas.

When it is the case, "interchange only" mismatch is computed for all areas:

$$
Interchange Mismatch = Interchange - Interchange Target
$$

If this mismatch for all areas and the slack injection of the buses without area are below the configured [parameter `slackBusPMaxMismatch`](parameters.md), then the outer loop declares a stable status, meaning that the interchanges are correct and the slack bus active power is distributed.
If the absolute value of this mismatch is below the [parameter `areaInterchangePMaxMismatch`](parameters.md) for all areas and the absolute value of slack bus active power mismatch is below the [parameter `slackBusPMaxMismatch`](parameters.md), then the outer loop declares a stable status, meaning that the interchanges are correct and the slack bus active power is distributed.

If not, the remaining slack bus mismatch is first distributed over the buses that have no area.

If some slack bus mismatch still remains, it is distributed over all buses of the network.
If some slack bus mismatch still remains, it is distributed over all the areas (see [Remaining slack bus mismatch distribution](#Remaining-slack-bus-mismatch-distribution)).

### Areas validation
There are some cases where areas are considered invalid and will not be considered for the area interchange control:
Expand Down Expand Up @@ -277,6 +277,18 @@ Indeed, in this case the slack injection can be seen as an interchange to 'the v
- All connected branches are boundaries of those areas: Not attributed to anyone, the mismatch will already be present in the interchange mismatch
- Some connected branches are not declared as boundaries of the areas: Amount of mismatch to distribute is split equally among the areas (added to their "total mismatch")

### Remaining slack bus mismatch distribution
This section covers the case where the "total mismatch" of all areas is in [-[`areaInterchangePMaxMismatch`](parameters.md);[`areaInterchangePMaxMismatch`](parameters.md)], but some slack bus active power mismatch remains (even after trying to distribute on buses with no area).
This remaining slack bus active power mismatch will be distributed by all areas, each one will get a share of this mismatch to distribute.

This distribution will affect each area's interchange and will not necessarily make it closer to its target.
The distribution factor of each area will be computed in a way that minimises chances of having the area increase its interchange mismatch up to more than [`areaInterchangePMaxMismatch`](parameters.md) in absolute value.
So the factor is proportional to the "margin" of active power that the area can distribute while keeping $-areaInterchangePMaxMismatch < Area Total Mismatch < areaInterchangePMaxMismatch$.

It is computed like this:
$Factor = sign(Slack Bus Mismatch) * Area Total Mismatch + areaInterchangePMaxMismatch $
Then factors are normalized to have sum of factors equal to 1.

### Zero impedance boundary branches
The following applies when the [`lowImpedanceBranchMode`](parameters.md) is set to `REPLACE_BY_ZERO_IMPEDANCE_LINE`.
Currently, computations involving zero-impedance branches used as boundary branches are not supported.
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,20 @@ public class AreaInterchangeControlContextData extends DistributedSlackContextDa
/**
* The part of the total slack active power mismatch that should be added to the Area's net interchange mismatch, ie the part of the slack that should be distributed in the Area.
*/
private final Map<String, Double> areaSlackDistributionParticipationFactor;
private final Map<String, Double> slackDistributionFactorByAreaId;

public AreaInterchangeControlContextData(Set<LfBus> busesWithoutArea, Map<String, Double> areaSlackDistributionParticipationFactor) {
super();
this.busesWithoutArea = new HashSet<>(busesWithoutArea);
this.areaSlackDistributionParticipationFactor = new HashMap<>(areaSlackDistributionParticipationFactor);
this.slackDistributionFactorByAreaId = new HashMap<>(areaSlackDistributionParticipationFactor);
}

public Set<LfBus> getBusesWithoutArea() {
return busesWithoutArea;
}

public Map<String, Double> getAreaSlackDistributionParticipationFactor() {
return areaSlackDistributionParticipationFactor;
public Map<String, Double> getSlackDistributionFactorByAreaId() {
return slackDistributionFactorByAreaId;
}

}
10 changes: 6 additions & 4 deletions src/main/java/com/powsybl/openloadflow/util/Reports.java
Original file line number Diff line number Diff line change
Expand Up @@ -217,18 +217,20 @@ public static ReportNode reportAreaInterchangeControlDistributionFailure(ReportN
.add();
}

public static void reportAreaInterchangeControlAreaMismatch(ReportNode reportNode, String area, double mismatch) {
public static void reportAicAreaDistributionMismatch(ReportNode reportNode, String area, double mismatch, boolean isInterchangeDistribution) {
String messageTemplate = isInterchangeDistribution ? "olf.areaInterchangeControlAreaMismatch" : "olf.areaInterchangeControlAreaSlackMismatch";
reportNode.newReportNode()
.withMessageTemplate("olf.areaInterchangeControlAreaMismatch")
.withMessageTemplate(messageTemplate)
.withUntypedValue("area", area)
.withTypedValue(MISMATCH, mismatch, OpenLoadFlowReportConstants.MISMATCH_TYPED_VALUE)
.withSeverity(TypedValue.WARN_SEVERITY)
.add();
}

public static void reportAreaInterchangeControlAreaDistributionSuccess(ReportNode reportNode, String area, double mismatch, int iterationCount) {
public static void reportAicAreaDistributionSuccess(ReportNode reportNode, String area, double mismatch, int iterationCount, boolean isInterchangeDistribution) {
String messageTemplate = isInterchangeDistribution ? "olf.areaInterchangeControlAreaDistributionSuccess" : "olf.areaInterchangeControlSlackDistributionSuccess";
reportNode.newReportNode()
.withMessageTemplate("olf.areaInterchangeControlAreaDistributionSuccess")
.withMessageTemplate(messageTemplate)
.withUntypedValue("area", area)
.withTypedValue(MISMATCH, mismatch, OpenLoadFlowReportConstants.MISMATCH_TYPED_VALUE)
.withUntypedValue(ITERATION_COUNT, iterationCount)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ olf.acSecurityAnalysis = AC security analysis on network '${networkId}'
olf.activePowerControlPstsChangedTaps = ${numOfActivePowerControlPstsThatChangedTap} active power control PST(s) changed taps
olf.angleReferenceBusSelection = Angle reference bus: ${referenceBus}
olf.areaInterchangeControlAreaDistributionSuccess = Area ${area} interchange mismatch (${mismatch} MW) distributed in ${iterationCount} distribution iteration(s)
olf.areaInterchangeControlAreaMismatch = Remaining mismatch for Area ${area}: ${mismatch} MW
olf.areaInterchangeControlAreaMismatch = Remaining interchange mismatch for Area ${area}: ${mismatch} MW
olf.areaInterchangeControlAreaSlackMismatch = Remaining slack distribution mismatch for Area ${area}: ${mismatch} MW
olf.areaInterchangeControlDistributionFailure = Failed to distribute interchange active power mismatch
olf.areaInterchangeControlSlackDistributionSuccess = Area ${area} slack distribution share (${mismatch} MW) distributed in ${iterationCount} distribution iteration(s)
olf.areaNoInterchangeControlMissingBuses = Area ${area} will not be considered in area interchange control, reason: Area does not have all its boundary buses in the same connected component or synchronous component
olf.areaNoInterchangeControlNoBoundary = Area ${area} will not be considered in area interchange control, reason: Area does not have any area boundary
olf.areaNoInterchangeControlNoInterchangeTarget = Area ${area} will not be considered in area interchange control, reason: Area does not have an interchange target
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ olf.activePowerControlPstsChangedTaps = ${numOfActivePowerControlPstsThatChanged
olf.angleReferenceBusSelection = Noeud de r�f�rence des angles : ${referenceBus}
olf.areaInterchangeControlAreaDistributionSuccess = Zone ${area) - �cart de ${mismatch} MW par rapport � la cible des �changes interzones compens� en ${iterationCount} it�ration(s)
olf.areaInterchangeControlAreaMismatch = Zone ${area) - �cart restant par rapport � la cible des �changes interzones: ${mismatch} MW
olf.areaInterchangeControlAreaSlackMismatch = Zone ${area} - �chec de la participation � la compensation de puissance active au noeud bilan, ${mismatch} MW restants.
olf.areaInterchangeControlDistributionFailure = �chec de la compensation d'�change interzones
olf.areaInterchangeControlSlackDistributionSuccess = Zone ${area) - participation de ${mismatch} MW � la compensation de puissance active du noeud bilan r�alis�e en ${iterationCount} it�ration(s)
olf.areaNoInterchangeControlMissingBuses = Zone ${area} - zone non prise en compte dans le contr�le des �changes interzones, raison : La zone n'a pas tous ses noeuds fronti�res sur le m�me composant connect� ou le m�me composant synchrone.
olf.areaNoInterchangeControlNoBoundary = Zone ${area} - zone non prise en compte dans le contr�le des �changes interzones, raison : La zone n'a pas de limite de zone
olf.areaNoInterchangeControlNoInterchangeTarget = Zone ${area} - zone non prise en compte dans le contr�le des �changes interzones, raison : La zone n'a pas de cible d'�change
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,28 @@
*/
package com.powsybl.openloadflow.ac;

import com.powsybl.commons.report.ReportNode;
import com.powsybl.commons.test.PowsyblTestReportResourceBundle;
import com.powsybl.iidm.network.Area;
import com.powsybl.iidm.network.Network;
import com.powsybl.loadflow.LoadFlow;
import com.powsybl.loadflow.LoadFlowParameters;
import com.powsybl.loadflow.LoadFlowResult;
import com.powsybl.loadflow.LoadFlowRunParameters;
import com.powsybl.math.matrix.DenseMatrixFactory;
import com.powsybl.openloadflow.OpenLoadFlowParameters;
import com.powsybl.openloadflow.OpenLoadFlowProvider;
import com.powsybl.openloadflow.network.*;
import com.powsybl.openloadflow.network.impl.Networks;
import com.powsybl.openloadflow.util.LoadFlowAssert;
import com.powsybl.openloadflow.util.report.PowsyblOpenLoadFlowReportResourceBundle;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import java.io.IOException;
import java.util.List;
import java.util.concurrent.CompletionException;
import java.util.stream.Stream;
Expand All @@ -37,12 +43,15 @@ class AreaInterchangeControlTest {
private LoadFlow.Runner loadFlowRunner;
private LoadFlowParameters parameters;

private LoadFlowRunParameters runParameters;

private OpenLoadFlowParameters parametersExt;

@BeforeEach
void setUp() {
loadFlowRunner = new LoadFlow.Runner(new OpenLoadFlowProvider(new DenseMatrixFactory()));
parameters = new LoadFlowParameters();
runParameters = new LoadFlowRunParameters().setParameters(parameters);
parametersExt = OpenLoadFlowParameters.create(parameters)
.setAreaInterchangeControl(true)
.setSlackBusPMaxMismatch(1e-3)
Expand All @@ -67,7 +76,7 @@ void twoAreasWithUnpairedDanglingLine() {
Network network = MultiAreaNetworkFactory.createTwoAreasWithDanglingLine();
double interchangeTarget1 = -60; // area a1 has a boundary that is an unpaired dangling line with P0 = 20MW
double interchangeTarget2 = 40;
runLfTwoAreas(network, interchangeTarget1, interchangeTarget2, -10, 4);
runLfTwoAreas(network, interchangeTarget1, interchangeTarget2, -10, 3);
parameters.setDc(true);
runLfTwoAreas(network, interchangeTarget1, interchangeTarget2, -10, 0);
}
Expand Down Expand Up @@ -151,7 +160,7 @@ private void runLfOneAreaSlackDistributionFailure(OpenLoadFlowParameters.SlackDi
assertEquals(expectedDistributedP, mainComponentResult.getDistributedActivePower(), 1e-3);
} else {
CompletionException thrown = assertThrows(CompletionException.class, () -> loadFlowRunner.run(network, parameters));
assertEquals("Failed to distribute interchange active power mismatch", thrown.getCause().getMessage());
assertEquals("Failed to distribute active power mismatch", thrown.getCause().getMessage());
}
}

Expand Down Expand Up @@ -276,6 +285,89 @@ void tenAreasSlackDistribution() {
assertEquals(0, result.getComponentResults().get(0).getSlackBusResults().get(0).getActivePowerMismatch(), parametersExt.getSlackBusPMaxMismatch());
}

@Test
void remainingSlackDistribution() throws IOException {
Network network = MultiAreaNetworkFactory.createTwoAreasWithXNodeHighZ();
ReportNode node = ReportNode.newRootReportNode()
.withResourceBundles(PowsyblOpenLoadFlowReportResourceBundle.BASE_NAME, PowsyblTestReportResourceBundle.TEST_BASE_NAME)
.withMessageTemplate("test")
.build();
parametersExt.setAreaInterchangePMaxMismatch(0.5);
var result = loadFlowRunner.run(network, runParameters.setReportNode(node));
assertEquals(LoadFlowResult.Status.FULLY_CONVERGED, result.getStatus());
assertEquals(0, result.getComponentResults().get(0).getSlackBusResults().get(0).getActivePowerMismatch(), parametersExt.getSlackBusPMaxMismatch());
String expectedReport = """
+ test
+ Load flow on network 'areas'
+ Network CC0 SC0
+ Network info
Network has 4 buses and 3 branches
Network balance: active generation=140 MW, active load=110 MW, reactive generation=0 MVar, reactive load=15 MVar
Angle reference bus: bx1_vl_0
Slack bus: bx1_vl_0
+ Outer loop AreaInterchangeControl
+ Outer loop iteration 1
Area a1 interchange mismatch (10.168852 MW) distributed in 1 distribution iteration(s)
Area a2 interchange mismatch (-39.924115 MW) distributed in 1 distribution iteration(s)
+ Outer loop iteration 2
Area a1 slack distribution share (0.293861 MW) distributed in 1 distribution iteration(s)
Area a2 slack distribution share (0.331121 MW) distributed in 1 distribution iteration(s)
Outer loop VoltageMonitoring
Outer loop ReactiveLimits
Outer loop AreaInterchangeControl
Outer loop VoltageMonitoring
Outer loop ReactiveLimits
AC load flow completed successfully (solverStatus=CONVERGED, outerloopStatus=STABLE)
""";
LoadFlowAssert.assertTxtReportEquals(expectedReport, node);
}

@Test
void remainingSlackDistributionShares() throws IOException {
Network network = MultiAreaNetworkFactory.createTwoAreasWithXNode();
ReportNode node = ReportNode.newRootReportNode()
.withResourceBundles(PowsyblOpenLoadFlowReportResourceBundle.BASE_NAME, PowsyblTestReportResourceBundle.TEST_BASE_NAME)
.withMessageTemplate("test")
.build();

// slack bus in area a1
parameters.setReadSlackBus(true);
parametersExt.setSlackBusSelectionMode(SlackBusSelectionMode.NAME);
parametersExt.setSlackBusId("vl1_0");

network.getGenerator("g1").setTargetP(70.4);
network.getGenerator("gen3").setTargetP(40);

parametersExt.setAreaInterchangePMaxMismatch(0.5);
network.getArea("a1").setInterchangeTarget(-10); // will have a margin of 0.9 for slack distribution (interchange with slack =-10.4 and max acceptable interchange = -9.5)
network.getArea("a2").setInterchangeTarget(9.51); // will have a margin of 0.01 for slack distribution (interchange with slack = 10 and max acceptable interchange = 10.01)

var result = loadFlowRunner.run(network, runParameters.setReportNode(node));
assertEquals(LoadFlowResult.Status.FULLY_CONVERGED, result.getStatus());
assertEquals(0, result.getComponentResults().get(0).getSlackBusResults().get(0).getActivePowerMismatch(), parametersExt.getSlackBusPMaxMismatch());
String expectedReport = """
+ test
+ Load flow on network 'areas'
+ Network CC0 SC0
+ Network info
Network has 4 buses and 3 branches
Network balance: active generation=110.4 MW, active load=110 MW, reactive generation=0 MVar, reactive load=15 MVar
Angle reference bus: vl1_0
Slack bus: vl1_0
+ Outer loop AreaInterchangeControl
+ Outer loop iteration 1
Area a1 slack distribution share (-0.395604 MW) distributed in 1 distribution iteration(s)
Area a2 slack distribution share (-0.004396 MW) distributed in 1 distribution iteration(s)
Outer loop VoltageMonitoring
Outer loop ReactiveLimits
Outer loop AreaInterchangeControl
Outer loop VoltageMonitoring
Outer loop ReactiveLimits
AC load flow completed successfully (solverStatus=CONVERGED, outerloopStatus=STABLE)
""";
LoadFlowAssert.assertTxtReportEquals(expectedReport, node);
}

private LoadFlowResult runLfTwoAreas(Network network, double interchangeTarget1, double interchangeTarget2, double expectedDistributedP, int expectedIterationCount) {
Area area1 = network.getArea("a1");
Area area2 = network.getArea("a2");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -488,7 +488,7 @@ void outerLoopFailedTest() {
assertFalse(result.isFullyConverged());

assertEquals(LoadFlowResult.ComponentResult.Status.FAILED, result.getComponentResults().get(0).getStatus());
assertEquals("Outer loop failed: Failed to distribute interchange active power mismatch", result.getComponentResults().get(0).getStatusText());
assertEquals("Outer loop failed: Failed to distribute active power mismatch", result.getComponentResults().get(0).getStatusText());
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,13 @@ public static Network createTwoAreasWithXNode() {
return network;
}

public static Network createTwoAreasWithXNodeHighZ() {
Network network = createTwoAreasWithXNode();
network.getLine("l23_A1").setX(7).setR(10);
network.getLine("l23_A2").setX(7).setR(10);
return network;
}

/**
* g1 100 MW gen3 40MW
* | |
Expand Down