2626import java .util .Comparator ;
2727import java .util .HashMap ;
2828import java .util .HashSet ;
29- import java .util .Iterator ;
3029import java .util .List ;
3130import java .util .Map ;
3231import 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