Skip to content

Commit d514ec1

Browse files
Merge branch 'main' into fix-slack-ac-dc
2 parents 80c97a9 + 6f892b0 commit d514ec1

63 files changed

Lines changed: 825 additions & 565 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/advanced_programming/contingency_active_power_loss.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ To add another plugin, you will need to code (in Java) an implementation of the
1414
interface and make this implementation available to the Java ServiceLoader (e.g. using Google's AutoService):
1515
- the `getName()` method should provide the plugin name - which can then be used in the `contingencyActivePowerLossDistribution` security analysis parameter
1616
- the `run(...)` method will be called by the security analysis engine for each contingency and should provide the logic.
17-
This method has access to:
17+
This method must return the amount of distributed active power (in per-unit) and has access to:
1818
- the network
1919
- the contingency in open-loadflow representation, including among others information about disconnected network elements, and how much active power has been lost.
2020
- the contingency definition

docs/loadflow/loadflow.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
## Grid modeling
44

55
Open Load Flow computes power flows from IIDM grid model in bus/view topology. From the view, a very simple network, composed
6-
of only buses and branches is created. In the graph vision, we rely on a $\Pi$ model for branches (lines, transformers, dangling lines, etc.):
6+
of only buses and branches is created. In the graph vision, we rely on a $\Pi$ model for branches (lines, transformers, boundary lines, etc.):
77

88
- $R$ and $X$ are respectively the real part (resistance) and the imaginary part (reactance) of the complex impedance ;
99
- $G_1$ and $G_2$ are the real parts (conductance) on respectively side 1 and side 2 of the branch ;
@@ -272,9 +272,9 @@ In such cases the involved areas are not considered in the Area Interchange Cont
272272

273273
In iIDM each area defines the boundary points to be considered in the interchange. iIDM supports two ways of modeling area boundaries:
274274
- either via an equipment terminal,
275-
- or via a DanglingLine boundary.
275+
- or via a BoundaryLine boundary.
276276

277-
In the DanglingLine case, the flow at the boundary side is considered as it should be, for both unpaired DanglingLines and DanglingLines paired in a TieLine.
277+
In the BoundaryLine case, the flow at the boundary side is considered as it should be, for both unpaired BoundaryLines and BoundaryLines paired in a TieLine.
278278

279279
### Slack bus mismatch attribution
280280
Depending on the location of the slack bus(es), the role of distributing the active power mismatch will be attributed based on the following logic:
@@ -298,6 +298,10 @@ It is computed like this:
298298
$Factor = sign(Slack Bus Mismatch) * Area Total Mismatch + areaInterchangePMaxMismatch $
299299
Then factors are normalized to have sum of factors equal to 1.
300300

301+
The distribution is iterative (inside the same outer loop iteration).
302+
Each area distributes its share, if some areas cannot fully distribute it, they are excluded from this distribution and the remaining slack is distributed among other areas at next iteration.
303+
The distribution iterates until all the mismatch has been distributed and fails if all areas cannot distribute anymore but some mismatch remains.
304+
301305
### Zero impedance boundary branches
302306
The following applies when the [`lowImpedanceBranchMode`](parameters.md) is set to `REPLACE_BY_ZERO_IMPEDANCE_LINE`.
303307
Currently, computations involving zero-impedance branches used as boundary branches are not supported.

docs/loadflow/parameters.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -383,7 +383,7 @@ small reactive power ranger means limited to zero voltage control capability. Th
383383
- Generators
384384
- Batteries
385385
- Voltage Source Converters
386-
- The optional generation part of a Dangling Line
386+
- The optional generation part of a Boundary Line
387387
- Static VAR compensators
388388

389389
For a given active power output, the reactive power range is defined as $MaxQ - MinQ$ (always a positive value).

docs/sensitivity/getting_started.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,10 @@ Here is a table to summarize supported use cases:
4747
## Input function types
4848

4949
### Branch sensitivity functions :
50-
BRANCH_ACTIVE_POWER_#, BRANCH_REACTIVE_POWER_# or BRANCH_CURRENT_# are associated to branch objects (lines, transformers, dangling lines, tie lines). The # index corresponding to the side of the studied branch.
50+
BRANCH_ACTIVE_POWER_#, BRANCH_REACTIVE_POWER_# or BRANCH_CURRENT_# are associated to branch objects (lines, transformers, boundary lines, tie lines). The # index corresponding to the side of the studied branch.
5151
- If it is a line, a tie line, or a two windings transformer : The side is 1 or 2.
5252
- If it is a three windings transformer : The side is 1, 2 or 3 corresponding to the studied leg of the transformer.
53-
- If it is a dangling line : The side is 1 (network side) or 2 (boundary side). Note that if the dangling line is paired, only side 1 (network side) can be specified, and the sensitivity function is computed at the corresponding tie line side.
53+
- If it is a boundary line : The side is 1 (network side) or 2 (boundary side). Note that if the boundary line is paired, only side 1 (network side) can be specified, and the sensitivity function is computed at the corresponding tie line side.
5454

5555
### Bus sensitivity functions :
5656
BUS_VOLTAGE or BUS_REACTIVE_POWER are associated to the bus of the given network element.

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353
<slf4jtoys.version>1.6.3</slf4jtoys.version>
5454
<asciitable.version>0.3.2</asciitable.version>
5555

56-
<powsybl-core.version>7.1.1</powsybl-core.version>
56+
<powsybl-core.version>7.2.0-RC1</powsybl-core.version>
5757

5858
<!-- This is required for later correct replacement of argline -->
5959
<argLine/>

src/main/java/com/powsybl/openloadflow/lf/AbstractLoadFlowResult.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ public OuterLoopResult getOuterLoopResult() {
5050
return outerLoopResult;
5151
}
5252

53+
@Override
5354
public double getDistributedActivePower() {
5455
return distributedActivePower;
5556
}

src/main/java/com/powsybl/openloadflow/lf/LoadFlowResult.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,6 @@ record Status(com.powsybl.loadflow.LoadFlowResult.ComponentResult.Status status,
2424
double getSlackBusActivePowerMismatch();
2525

2626
Status toComponentResultStatus();
27+
28+
double getDistributedActivePower();
2729
}

src/main/java/com/powsybl/openloadflow/lf/outerloop/AbstractAreaInterchangeControlOuterLoop.java

Lines changed: 63 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
import java.util.Comparator;
2727
import java.util.HashMap;
2828
import java.util.HashSet;
29-
import java.util.Iterator;
3029
import java.util.List;
3130
import java.util.Map;
3231
import java.util.Objects;
@@ -63,7 +62,17 @@ private enum ActivePowerDistributionType {
6362
AREA_INTERCHANGE, SLACK
6463
}
6564

66-
private record AreaActivePowerDistributionResult(String areaId, ActivePowerDistributionType type, double initialMismatch, ActivePowerDistribution.Result distributionResult) { }
65+
private record AreaActivePowerDistributionResult(String areaId, ActivePowerDistributionType type, double distributedMismatch, int iteration, boolean movedBuses, double remainingMismatch) { }
66+
67+
private AreaActivePowerDistributionResult updateResult(AreaActivePowerDistributionResult previousResult, double addedMismatch, ActivePowerDistribution.Result lastDistributionResult) {
68+
return new AreaActivePowerDistributionResult(
69+
previousResult.areaId(),
70+
previousResult.type(),
71+
previousResult.distributedMismatch() + addedMismatch - lastDistributionResult.remainingMismatch(),
72+
previousResult.iteration() + lastDistributionResult.iteration(),
73+
previousResult.movedBuses() || lastDistributionResult.movedBuses(),
74+
lastDistributionResult.remainingMismatch());
75+
}
6776

6877
protected AbstractAreaInterchangeControlOuterLoop(ActivePowerDistribution activePowerDistribution, OuterLoop<V, E, P, C, O> noAreaOuterLoop, double slackBusPMaxMismatch, double areaInterchangePMaxMismatch, Logger logger) {
6978
this.activePowerDistribution = Objects.requireNonNull(activePowerDistribution);
@@ -124,7 +133,7 @@ public OuterLoopResult check(O context, ReportNode reportNode) {
124133
Set<LfBus> busesWithoutArea = contextData.getBusesWithoutArea();
125134
Map<String, Pair<Set<LfBus>, Double>> busesNoAreaDistributionMap = Map.of(DEFAULT_NO_AREA_NAME, Pair.of(busesWithoutArea, slackBusActivePowerMismatch));
126135
List<AreaActivePowerDistributionResult> busesNoAreaDistributionResult = distributeActivePower(busesNoAreaDistributionMap);
127-
double remainingSlackBusMismatch = busesNoAreaDistributionResult.get(0).distributionResult.remainingMismatch();
136+
double remainingSlackBusMismatch = busesNoAreaDistributionResult.get(0).remainingMismatch();
128137
if (lessThanSlackBusMaxMismatch(remainingSlackBusMismatch)) {
129138
return buildOuterLoopResult(busesNoAreaDistributionResult, reportNode, context);
130139
} else {
@@ -148,21 +157,32 @@ private List<AreaActivePowerDistributionResult> distributeActivePower(Map<String
148157
for (Map.Entry<String, Pair<Set<LfBus>, Double>> e : areas.entrySet()) {
149158
double areaActivePowerMismatch = e.getValue().getRight();
150159
ActivePowerDistribution.Result distributionResult = activePowerDistribution.run(null, e.getValue().getLeft(), areaActivePowerMismatch);
151-
areaResults.add(new AreaActivePowerDistributionResult(e.getKey(), ActivePowerDistributionType.AREA_INTERCHANGE, areaActivePowerMismatch, distributionResult));
160+
areaResults.add(new AreaActivePowerDistributionResult(e.getKey(), ActivePowerDistributionType.AREA_INTERCHANGE, areaActivePowerMismatch - distributionResult.remainingMismatch(), distributionResult.iteration(), distributionResult.movedBuses(), distributionResult.remainingMismatch()));
152161
}
153162
return areaResults;
154163
}
155164

156165
private List<AreaActivePowerDistributionResult> distributeRemainingSlackMismatch(double mismatch, LfNetwork network, Map<String, Double> slackDistributionFactorByAreaId) {
157-
List<AreaActivePowerDistributionResult> resultByArea = new ArrayList<>();
158-
159-
Map<LfArea, Double> distributionFactorByArea = getSlackDistributionFactorByArea(mismatch, network.getAreas(), slackDistributionFactorByAreaId);
166+
Map<LfArea, AreaActivePowerDistributionResult> resultByArea = new HashMap<>();
167+
Map<LfArea, Double> interchangeMarginByArea = getSlackDistributionFactorByArea(mismatch, network.getAreas(), slackDistributionFactorByAreaId);
168+
Map<LfArea, Double> distributionFactorByArea = normalizeSlackParticipationFactors(interchangeMarginByArea);
169+
double remainingMismatch = mismatch;
170+
while (distributionFactorByArea.values().stream().mapToDouble(f -> f).sum() > 0 && Math.abs(remainingMismatch) > ActivePowerDistribution.P_RESIDUE_EPS) {
171+
remainingMismatch = distributeOnAreas(remainingMismatch, distributionFactorByArea, resultByArea);
172+
distributionFactorByArea = normalizeSlackParticipationFactors(distributionFactorByArea);
173+
}
174+
return resultByArea.values().stream().toList();
175+
}
160176

177+
private double distributeOnAreas(double mismatch, Map<LfArea, Double> distributionFactorByArea, Map<LfArea, AreaActivePowerDistributionResult> resultByArea) {
161178
Comparator<Map.Entry<LfArea, Double>> mismatchComparator = Comparator.comparingDouble(Map.Entry::getValue);
162-
Iterator<LfArea> areaIteratorSortedByFactor = distributionFactorByArea.entrySet().stream().
163-
sorted(mismatchComparator.reversed())
179+
// create an iterator of all areas, even for those with 0 factor in order to have the last distribution result for each area
180+
List<LfArea> areasSortedByFactor = distributionFactorByArea.entrySet().stream()
181+
.sorted(mismatchComparator.reversed())
164182
.map(Map.Entry::getKey)
165-
.iterator();
183+
.toList();
184+
185+
var areaIteratorSortedByFactor = areasSortedByFactor.iterator();
166186

167187
double remainingMismatch = mismatch;
168188
while (areaIteratorSortedByFactor.hasNext() && Math.abs(remainingMismatch) > ActivePowerDistribution.P_RESIDUE_EPS) {
@@ -177,23 +197,45 @@ private List<AreaActivePowerDistributionResult> distributeRemainingSlackMismatch
177197
areaActivePowerMismatch = Math.signum(mismatch) * 1.01 * ActivePowerDistribution.P_RESIDUE_EPS;
178198
}
179199
ActivePowerDistribution.Result distributionResult = activePowerDistribution.run(null, area.getBuses(), areaActivePowerMismatch);
180-
resultByArea.add(new AreaActivePowerDistributionResult(area.getId(), ActivePowerDistributionType.SLACK, areaActivePowerMismatch, distributionResult));
200+
201+
if (Math.abs(distributionResult.remainingMismatch()) > ActivePowerDistribution.P_RESIDUE_EPS) {
202+
// The area cannot distribute anymore, its factor is set to 0
203+
distributionFactorByArea.put(area, 0.);
204+
}
205+
var previousResult = resultByArea.getOrDefault(area, new AreaActivePowerDistributionResult(area.getId(), ActivePowerDistributionType.SLACK, 0, 0, false, 0));
206+
resultByArea.put(area, updateResult(previousResult, areaActivePowerMismatch, distributionResult));
207+
181208
remainingMismatch = remainingMismatch - areaActivePowerMismatch + distributionResult.remainingMismatch();
182209
}
183-
return resultByArea;
210+
211+
while (areaIteratorSortedByFactor.hasNext()) {
212+
// Set remaining mismatch to 0 for areas that were not used for last distribution
213+
LfArea area = areaIteratorSortedByFactor.next();
214+
resultByArea.computeIfPresent(area, (a, previousResult) ->
215+
new AreaActivePowerDistributionResult(
216+
previousResult.areaId(),
217+
previousResult.type(),
218+
previousResult.distributedMismatch(),
219+
previousResult.iteration(),
220+
previousResult.movedBuses(),
221+
0.));
222+
}
223+
224+
return remainingMismatch;
184225
}
185226

186227
private Map<LfArea, Double> getSlackDistributionFactorByArea(double mismatch, List<LfArea> areas, Map<String, Double> slackDistributionFactorByAreaId) {
187228
// Compute the "margin" that has the area = the amount of power it can distribute and still have target - maxMismatch < interchange < target + maxMismatch
188229
// We use the interchangeMismatchWithSlack here because:
189230
// For areas without slack bus it changes nothing compared to use interchangeMismatch
190231
// For areas with slack bus, the interchangeMismatchWithSlack is the interchange it would have if all the slack was distributed.
191-
Map<LfArea, Double> interchangeMarginByArea = areas.stream()
232+
return areas.stream()
192233
.collect(Collectors.toMap(
193234
a -> a,
194235
a -> Math.signum(mismatch) * getInterchangeMismatchWithSlack(a, mismatch, slackDistributionFactorByAreaId) + this.areaInterchangePMaxMismatch / PerUnit.SB));
236+
}
195237

196-
// normalize factors
238+
private Map<LfArea, Double> normalizeSlackParticipationFactors(Map<LfArea, Double> interchangeMarginByArea) {
197239
double sumMargin = interchangeMarginByArea.values().stream().mapToDouble(aDouble -> aDouble).sum();
198240
return interchangeMarginByArea.entrySet().stream()
199241
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue() / sumMargin));
@@ -221,8 +263,8 @@ protected double getSlackInjection(String areaId, double slackBusActivePowerMism
221263

222264
private boolean hasRemainingMismatch(AreaActivePowerDistributionResult areaResult) {
223265
return switch (areaResult.type) {
224-
case SLACK -> !lessThanSlackBusMaxMismatch(areaResult.distributionResult.remainingMismatch());
225-
case AREA_INTERCHANGE -> !lessThanInterchangeMaxMismatch(areaResult.distributionResult.remainingMismatch());
266+
case SLACK -> !lessThanSlackBusMaxMismatch(areaResult.remainingMismatch());
267+
case AREA_INTERCHANGE -> !lessThanInterchangeMaxMismatch(areaResult.remainingMismatch());
226268
};
227269
}
228270

@@ -234,9 +276,8 @@ private OuterLoopResult buildOuterLoopResult(List<AreaActivePowerDistributionRes
234276
if (hasRemainingMismatch(areaResult)) {
235277
remainingMismatches.add(areaResult);
236278
}
237-
ActivePowerDistribution.Result distributionResult = areaResult.distributionResult;
238-
totalDistributedActivePower += areaResult.initialMismatch - distributionResult.remainingMismatch();
239-
movedBuses |= distributionResult.movedBuses();
279+
totalDistributedActivePower += areaResult.distributedMismatch;
280+
movedBuses |= areaResult.movedBuses();
240281
}
241282

242283
ReportNode iterationReportNode = Reports.createOuterLoopIterationReporter(reportNode, context.getOuterLoopTotalIterations() + 1);
@@ -273,8 +314,8 @@ private void reportAndLogAreaActivePowerDistributionSuccess(List<AreaActivePower
273314
areaResults.stream().sorted(Comparator.comparing(areaResult -> areaResult.areaId)).forEach(areaResult -> {
274315
boolean isInterchangeDistribution = ActivePowerDistributionType.AREA_INTERCHANGE.equals(areaResult.type);
275316
String distributionType = isInterchangeDistribution ? "interchange mismatch" : "slack distribution share";
276-
logger.info("Area {} {} ({} MW) distributed in {} distribution iteration(s)", areaResult.areaId, distributionType, areaResult.initialMismatch * PerUnit.SB, areaResult.distributionResult.iteration());
277-
Reports.reportAicAreaDistributionSuccess(iterationReportNode, areaResult.areaId, areaResult.initialMismatch * PerUnit.SB, areaResult.distributionResult.iteration(), isInterchangeDistribution);
317+
logger.info("Area {} {} ({} MW) distributed in {} distribution iteration(s)", areaResult.areaId, distributionType, areaResult.distributedMismatch * PerUnit.SB, areaResult.iteration());
318+
Reports.reportAicAreaDistributionSuccess(iterationReportNode, areaResult.areaId, areaResult.distributedMismatch * PerUnit.SB, areaResult.iteration(), isInterchangeDistribution);
278319
});
279320
}
280321

@@ -286,7 +327,7 @@ private void reportAndLogAreaActivePowerDistributionFailure(ReportNode iteration
286327
.forEach(areaResult -> {
287328
boolean isInterchangeDistribution = ActivePowerDistributionType.AREA_INTERCHANGE.equals(areaResult.type);
288329
String mismatchType = isInterchangeDistribution ? "interchange" : "slack distribution";
289-
double remainingMismatch = areaResult.distributionResult.remainingMismatch() * PerUnit.SB;
330+
double remainingMismatch = areaResult.remainingMismatch() * PerUnit.SB;
290331
logger.error("Remaining {} mismatch for Area {}: {} MW", mismatchType, areaResult.areaId, remainingMismatch);
291332
Reports.reportAicAreaDistributionMismatch(failureReportNode, areaResult.areaId, remainingMismatch, isInterchangeDistribution);
292333
});

src/main/java/com/powsybl/openloadflow/network/LfBranch.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ enum BranchType {
3030
TRANSFO_3_LEG_1,
3131
TRANSFO_3_LEG_2,
3232
TRANSFO_3_LEG_3,
33-
DANGLING_LINE,
33+
BOUNDARY_LINE,
3434
SWITCH,
3535
TIE_LINE
3636
}

src/main/java/com/powsybl/openloadflow/network/LfBus.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
*/
88
package com.powsybl.openloadflow.network;
99

10+
import com.powsybl.contingency.violations.ViolationLocation;
1011
import com.powsybl.iidm.network.Country;
1112
import com.powsybl.openloadflow.util.Evaluable;
12-
import com.powsybl.security.ViolationLocation;
1313
import com.powsybl.security.results.BusResult;
1414

1515
import java.util.*;

0 commit comments

Comments
 (0)