Skip to content

Commit 97a0498

Browse files
Merge branch 'main' into hvdc_ac_emulation_outerloop
2 parents e72d1ad + 6f892b0 commit 97a0498

11 files changed

Lines changed: 253 additions & 52 deletions

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: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,10 @@ It is computed like this:
300300
$Factor = sign(Slack Bus Mismatch) * Area Total Mismatch + areaInterchangePMaxMismatch $
301301
Then factors are normalized to have sum of factors equal to 1.
302302

303+
The distribution is iterative (inside the same outer loop iteration).
304+
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.
305+
The distribution iterates until all the mismatch has been distributed and fails if all areas cannot distribute anymore but some mismatch remains.
306+
303307
### Zero impedance boundary branches
304308
The following applies when the [`lowImpedanceBranchMode`](parameters.md) is set to `REPLACE_BY_ZERO_IMPEDANCE_LINE`.
305309
Currently, computations involving zero-impedance branches used as boundary branches are not supported.

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
});

0 commit comments

Comments
 (0)