All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.powsybl.openloadflow.lf.outerloop.AbstractAreaInterchangeControlOuterLoop Maven / Gradle / Ivy

The newest version!
/**
 * Copyright (c) 2024, Coreso SA (https://www.coreso.eu/) and TSCNET Services GmbH (https://www.tscnet.eu/)
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 * SPDX-License-Identifier: MPL-2.0
 */
package com.powsybl.openloadflow.lf.outerloop;

import com.powsybl.commons.PowsyblException;
import com.powsybl.commons.report.ReportNode;
import com.powsybl.openloadflow.equations.Quantity;
import com.powsybl.openloadflow.lf.AbstractLoadFlowParameters;
import com.powsybl.openloadflow.lf.LoadFlowContext;
import com.powsybl.openloadflow.network.LfArea;
import com.powsybl.openloadflow.network.LfBranch;
import com.powsybl.openloadflow.network.LfBus;
import com.powsybl.openloadflow.network.LfNetwork;
import com.powsybl.openloadflow.network.util.ActivePowerDistribution;
import com.powsybl.openloadflow.util.PerUnit;
import com.powsybl.openloadflow.util.Reports;
import org.apache.commons.lang3.tuple.Pair;
import org.slf4j.Logger;

import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * @author Valentin Mouradian {@literal }
 */
public abstract class AbstractAreaInterchangeControlOuterLoop<
            V extends Enum & Quantity,
            E extends Enum & Quantity,
            P extends AbstractLoadFlowParameters

, C extends LoadFlowContext, O extends AbstractOuterLoopContext> extends AbstractActivePowerDistributionOuterLoop implements OuterLoop, ActivePowerDistributionOuterLoop { private final Logger logger; protected static final String FAILED_TO_DISTRIBUTE_INTERCHANGE_ACTIVE_POWER_MISMATCH = "Failed to distribute interchange active power mismatch"; protected static final String DEFAULT_NO_AREA_NAME = "NO_AREA"; protected final double slackBusPMaxMismatch; protected final double areaInterchangePMaxMismatch; protected final ActivePowerDistribution activePowerDistribution; protected final OuterLoop noAreaOuterLoop; protected AbstractAreaInterchangeControlOuterLoop(ActivePowerDistribution activePowerDistribution, OuterLoop noAreaOuterLoop, double slackBusPMaxMismatch, double areaInterchangePMaxMismatch, Logger logger) { this.activePowerDistribution = Objects.requireNonNull(activePowerDistribution); this.slackBusPMaxMismatch = slackBusPMaxMismatch; this.areaInterchangePMaxMismatch = areaInterchangePMaxMismatch; this.logger = logger; this.noAreaOuterLoop = noAreaOuterLoop; } @Override public void initialize(O context) { LfNetwork network = context.getNetwork(); if (!network.hasArea() && noAreaOuterLoop != null) { noAreaOuterLoop.initialize(context); return; } var contextData = new AreaInterchangeControlContextData(listBusesWithoutArea(network), allocateSlackDistributionParticipationFactors(network)); context.setData(contextData); } @Override public OuterLoopResult check(O context, ReportNode reportNode) { LfNetwork network = context.getNetwork(); if (!network.hasArea() && noAreaOuterLoop != null) { return noAreaOuterLoop.check(context, reportNode); } double slackBusActivePowerMismatch = getSlackBusActivePowerMismatch(context); AreaInterchangeControlContextData contextData = (AreaInterchangeControlContextData) context.getData(); Map areaSlackDistributionParticipationFactor = contextData.getAreaSlackDistributionParticipationFactor(); // First, we balance the areas that have a mismatch in their interchange power flow, and take the slack mismatch into account. Map areaInterchangeWithSlackMismatches = network.getAreaStream() .collect(Collectors.toMap(area -> area, area -> getInterchangeMismatchWithSlack(area, slackBusActivePowerMismatch, areaSlackDistributionParticipationFactor))); List areasToBalance = areaInterchangeWithSlackMismatches.entrySet().stream() .filter(entry -> { double areaActivePowerMismatch = entry.getValue(); return !lessThanInterchangeMaxMismatch(areaActivePowerMismatch); }) .map(Map.Entry::getKey) .toList(); if (areasToBalance.isEmpty()) { // Balancing takes the slack mismatch of the Areas into account. Now that the balancing is done, we check only the interchange power flow mismatch. // Doing this we make sure that the Areas' interchange targets have been reached and that the slack is correctly distributed. Map areaInterchangeMismatches = network.getAreaStream() .filter(area -> { double areaInterchangeMismatch = getInterchangeMismatch(area); return !lessThanInterchangeMaxMismatch(areaInterchangeMismatch); }).collect(Collectors.toMap(LfArea::getId, this::getInterchangeMismatch)); if (areaInterchangeMismatches.isEmpty() && lessThanSlackBusMaxMismatch(slackBusActivePowerMismatch)) { logger.debug("Already balanced"); } else { // If some mismatch remains, we distribute the slack bus active power on the buses without area // Corner case: if there is less slack than the slackBusPMaxMismatch, but there are areas with mismatch. // We consider that to still distribute the remaining slack will continue to reduce difference between interchange mismatch with slack and interchange mismatch. // Which should at the end of the day end up by not having interchange mismatches. Set busesWithoutArea = contextData.getBusesWithoutArea(); Map, Double>> mismatchToDistributeOnBusesWithoutArea = new HashMap<>(); mismatchToDistributeOnBusesWithoutArea.put(DEFAULT_NO_AREA_NAME, Pair.of(busesWithoutArea, slackBusActivePowerMismatch)); Map resultNoArea = distributeActivePower(mismatchToDistributeOnBusesWithoutArea); // If some mismatch remains (when there is no buses without area that participate for example), we distribute equally among the areas. double mismatchToSplitAmongAreas = resultNoArea.get(DEFAULT_NO_AREA_NAME).remainingMismatch(); if (lessThanSlackBusMaxMismatch(mismatchToSplitAmongAreas)) { return buildOuterLoopResult(mismatchToDistributeOnBusesWithoutArea, resultNoArea, reportNode, context); } else { int areasCount = (int) network.getAreaStream().count(); Map, Double>> mismatchSplitAmongAreas = network.getAreaStream().collect(Collectors.toMap(LfArea::getId, area -> Pair.of(area.getBuses(), mismatchToSplitAmongAreas / areasCount))); Map resultByArea = distributeActivePower(mismatchSplitAmongAreas); return buildOuterLoopResult(mismatchSplitAmongAreas, resultByArea, reportNode, context); } } return new OuterLoopResult(this, OuterLoopStatus.STABLE); } Map, Double>> areasMap = areasToBalance.stream() .collect(Collectors.toMap(LfArea::getId, area -> Pair.of(area.getBuses(), getInterchangeMismatchWithSlack(area, slackBusActivePowerMismatch, areaSlackDistributionParticipationFactor)))); Map resultByArea = distributeActivePower(areasMap); return buildOuterLoopResult(areasMap, resultByArea, reportNode, context); } protected Map distributeActivePower(Map, Double>> areas) { Map resultByArea = new HashMap<>(); for (Map.Entry, Double>> e : areas.entrySet()) { double areaActivePowerMismatch = e.getValue().getRight(); ActivePowerDistribution.Result result = activePowerDistribution.run(null, e.getValue().getLeft(), areaActivePowerMismatch); resultByArea.put(e.getKey(), result); } return resultByArea; } protected boolean lessThanInterchangeMaxMismatch(double mismatch) { return Math.abs(mismatch) <= this.areaInterchangePMaxMismatch / PerUnit.SB || Math.abs(mismatch) <= ActivePowerDistribution.P_RESIDUE_EPS; } boolean lessThanSlackBusMaxMismatch(double mismatch) { return Math.abs(mismatch) <= this.slackBusPMaxMismatch / PerUnit.SB || Math.abs(mismatch) <= ActivePowerDistribution.P_RESIDUE_EPS; } protected double getInterchangeMismatch(LfArea area) { return area.getInterchange() - area.getInterchangeTarget(); } protected double getInterchangeMismatchWithSlack(LfArea area, double slackBusActivePowerMismatch, Map areaSlackDistributionParticipationFactor) { return area.getInterchange() - area.getInterchangeTarget() + getSlackInjection(area.getId(), slackBusActivePowerMismatch, areaSlackDistributionParticipationFactor); } protected double getSlackInjection(String areaId, double slackBusActivePowerMismatch, Map areaSlackDistributionParticipationFactor) { return areaSlackDistributionParticipationFactor.getOrDefault(areaId, 0.0) * slackBusActivePowerMismatch; } protected OuterLoopResult buildOuterLoopResult(Map, Double>> areas, Map resultByArea, ReportNode reportNode, O context) { Map remainingMismatchByArea = new HashMap<>(); Map iterationsByArea = new HashMap<>(); double totalDistributedActivePower = 0.0; boolean movedBuses = false; for (Map.Entry e : resultByArea.entrySet()) { String area = e.getKey(); ActivePowerDistribution.Result result = e.getValue(); if (!lessThanInterchangeMaxMismatch(result.remainingMismatch())) { remainingMismatchByArea.put(area, result.remainingMismatch()); } totalDistributedActivePower += areas.get(area).getRight() - result.remainingMismatch(); movedBuses |= result.movedBuses(); iterationsByArea.put(area, result.iteration()); } ReportNode iterationReportNode = Reports.createOuterLoopIterationReporter(reportNode, context.getOuterLoopTotalIterations() + 1); AreaInterchangeControlContextData contextData = (AreaInterchangeControlContextData) context.getData(); contextData.addDistributedActivePower(totalDistributedActivePower); if (!remainingMismatchByArea.isEmpty()) { logger.error(FAILED_TO_DISTRIBUTE_INTERCHANGE_ACTIVE_POWER_MISMATCH); ReportNode failureReportNode = Reports.reportAreaInterchangeControlDistributionFailure(iterationReportNode); remainingMismatchByArea.entrySet().stream().sorted(Map.Entry.comparingByKey()).forEach(entry -> { logger.error("Remaining mismatch for Area {}: {} MW", entry.getKey(), entry.getValue() * PerUnit.SB); Reports.reportAreaInterchangeControlAreaMismatch(failureReportNode, entry.getKey(), entry.getValue() * PerUnit.SB); }); switch (context.getLoadFlowContext().getParameters().getSlackDistributionFailureBehavior()) { case THROW -> throw new PowsyblException(FAILED_TO_DISTRIBUTE_INTERCHANGE_ACTIVE_POWER_MISMATCH); case LEAVE_ON_SLACK_BUS -> { return new OuterLoopResult(this, movedBuses ? OuterLoopStatus.UNSTABLE : OuterLoopStatus.STABLE); } case FAIL, DISTRIBUTE_ON_REFERENCE_GENERATOR -> { // Mismatches reported in LoadFlowResult on slack bus(es) are the mismatches of the last solver (DC, NR, ...) run. // Since we will not be re-running the solver, revert distributedActivePower reporting which would otherwise be misleading. // Said differently, we report that we didn't distribute anything, and this is indeed consistent with the network state. contextData.addDistributedActivePower(-totalDistributedActivePower); return new OuterLoopResult(this, OuterLoopStatus.FAILED, FAILED_TO_DISTRIBUTE_INTERCHANGE_ACTIVE_POWER_MISMATCH); } default -> throw new IllegalStateException("Unexpected SlackDistributionFailureBehavior value"); } } else { if (movedBuses) { areas.entrySet().stream().sorted(Map.Entry.comparingByKey()).forEach(entry -> { logger.info("Area {} interchange mismatch ({} MW) distributed in {} distribution iteration(s)", entry.getKey(), entry.getValue().getValue() * PerUnit.SB, iterationsByArea.get(entry.getKey())); Reports.reportAreaInterchangeControlAreaDistributionSuccess(iterationReportNode, entry.getKey(), entry.getValue().getValue() * PerUnit.SB, iterationsByArea.get(entry.getKey())); }); return new OuterLoopResult(this, OuterLoopStatus.UNSTABLE); } else { return new OuterLoopResult(this, OuterLoopStatus.STABLE); } } } protected Set listBusesWithoutArea(LfNetwork network) { return network.getBuses().stream() .filter(b -> b.getArea().isEmpty()) .filter(b -> !b.isFictitious()) .collect(Collectors.toSet()); } protected Map allocateSlackDistributionParticipationFactors(LfNetwork lfNetwork) { Map areaSlackDistributionParticipationFactor = new HashMap<>(); List slackBuses = lfNetwork.getSlackBuses(); int totalSlackBusCount = slackBuses.size(); for (LfBus slackBus : slackBuses) { Optional areaOpt = slackBus.getArea(); if (areaOpt.isPresent()) { areaSlackDistributionParticipationFactor.put(areaOpt.get().getId(), areaSlackDistributionParticipationFactor.getOrDefault(areaOpt.get().getId(), 0.0) + 1.0 / totalSlackBusCount); } else { // When a bus is connected to one or multiple Areas but the flow through the bus is not considered for those areas' interchange power flow, // its slack injection should be considered for the slack of some Areas that it is connected to. Set connectedBranches = new HashSet<>(slackBus.getBranches()); Set connectedAreas = connectedBranches.stream() .flatMap(branch -> Stream.of(branch.getBus1(), branch.getBus2())) .filter(Objects::nonNull) .map(LfBus::getArea) .filter(Optional::isPresent) .map(Optional::get) .collect(Collectors.toSet()); // If the slack bus is on a boundary point considered for net position, // it will resolve naturally because deviations caused by slack are already present on tie line flows // no need to include in any area net position calculation Set areasSharingSlack = connectedAreas.stream() .filter(area -> area.getBoundaries().stream().noneMatch(boundary -> connectedBranches.contains(boundary.getBranch()))) .collect(Collectors.toSet()); if (!areasSharingSlack.isEmpty()) { areasSharingSlack.forEach(area -> areaSlackDistributionParticipationFactor.put(area.getId(), areaSlackDistributionParticipationFactor.getOrDefault(area.getId(), 0.0) + 1.0 / areasSharingSlack.size() / totalSlackBusCount)); logger.warn("Slack bus {} is not in any Area and is connected to Areas: {}. Areas {} are not considering the flow through this bus for their interchange flow. The slack will be distributed between those areas.", slackBus.getId(), connectedAreas.stream().map(LfArea::getId).toList(), areasSharingSlack.stream().map(LfArea::getId).toList()); } else { areaSlackDistributionParticipationFactor.put(DEFAULT_NO_AREA_NAME, areaSlackDistributionParticipationFactor.getOrDefault(DEFAULT_NO_AREA_NAME, 0.0) + 1.0 / totalSlackBusCount); } } } return areaSlackDistributionParticipationFactor; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy