diff --git a/docs/loadflow/loadflow.md b/docs/loadflow/loadflow.md index 47a6013c52..737a385385 100644 --- a/docs/loadflow/loadflow.md +++ b/docs/loadflow/loadflow.md @@ -201,3 +201,81 @@ Note that the vector $b$ of right-hand sides is linearly computed from the given To solve this system, we follow the classic approach of the LU matrices decomposition $J = LU$. Hence, by solving the system using LU decomposition, you can compute the voltage angles by giving as data the injections and the phase-shifting angles. + +## Area Interchange Control + +Area Interchange Control consists in having the Load Flow finding a solution where area interchanges are solved to match the input target interchange values. + +Currently, Area Interchange Control is only supported for AC load flow, DC load flow support is planned for future release. + +The area interchange control feature is optional, can be activated via the [parameter `areaInterchangeControl`](parameters.md) +and is performed by an outer loop. + +Area Interchange Control is performed using an outer loop, similar in principle to the traditional `SlackDistribution` outer loop. +However unlike the `SlackDistribution` outer loop which distributes imbalance over the entire synchronous component (island), +the Area Interchange Control outer loop performs an active power distribution over areas +(filtered on areas having their type matching the configured [parameter `areaInterchangeControlAreaType`](parameters.md)), +in order to have all areas' active power interchanges matching their target interchanges. + +The Area Interchange Control outer loop can handle networks where part (or even all) of the buses are not in an area. +For networks that have no areas at all, the behaviour is the same as with the distributed slack outer loop - in such case +internally the Area Interchange Control outer loop just triggers the Slack Distribution outer loop logic. + +Just like other outer loops, the Area Interchange Control outer loop checks whether area imbalance must be distributed: +* If no, the outer loop is stable +* If yes, the outer loop is unstable and a new Newton-Raphson is triggered + +### Area Interchange Control - algorithm description + +The active power is distributed separately on injections (as configured in the [parameter `balanceType`](parameters.md)) of each area +to compensate the area "total mismatch" that is given by: + +$$ +Area Total Mismatch = Interchange - Interchange Target + Slack Injection +$$ + +Where: +* "Interchange" is the sum of the power flows at the boundaries of the area (load sign convention i.e. counted positive for imports). +* "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. + +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 outerloop is stable and declares a stable status, meaning that the interchanges are correct and the slack bus active power is distributed. + +If not, the remaining mismatch is first distributed over the buses that have no area. + +If some mismatch still remains, it is distributed equally over all the areas. + +### Areas validation +There are some cases where areas are considered invalid and will not be considered for the area interchange control: +- Areas without interchange target +- Areas without boundaries +- Areas that have boundaries in multiple synchronous/connected components. If all the boundaries are in the same component but some buses are in different components, only the part in the component of the boundaries will be considered. + +In such cases the involved areas are not considered in the Area Interchange Control outer loop, however other valid areas will still be considered. + +### Interchange flow calculation + +In iIDM each area defines the boundary points to be considered in the interchange. iIDM supports two ways of modeling area boundaries: +- either via an equipment terminal, +- or via a DanglingLine boundary. + +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. + +### Slack bus mismatch attribution +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: +- If the slack bus is part of an area: the slack power is attributed to the area (see "total mismatch" calculation in [Algorithm description](#area-interchange-control---algorithm-description)). +Indeed, in this case the slack injection can be seen as an interchange to 'the void' which must be resolved. +- Slack bus has no area: + - Connected to other bus(es) without area: treated as the slack mismatch of the buses without area + - Connected to only buses that have an area: + - 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") diff --git a/docs/loadflow/parameters.md b/docs/loadflow/parameters.md index bda9947d5f..adefc66f00 100644 --- a/docs/loadflow/parameters.md +++ b/docs/loadflow/parameters.md @@ -120,6 +120,20 @@ When slack distribution is enabled (`distributedSlack` set to `true` in LoadFlow is considered to be distributed. The default value is `1 MW` and it must be greater or equal to `0 MW`. +**areaInterchangeControl** +The `areaInterchangeControl` property is an optional property that defines if the [area interchange control](loadflow.md#area-interchange-control) outer loop is enabled. +If set to `true`, the area interchange control outer loop will be used instead of the slack distribution outer loop. +The default value is `false`. + +**areaInterchangeControlAreaType** +Defines the `areaType` of the areas on which the [area interchange control](loadflow.md#area-interchange-control) is applied. +Only the areas of the input network that have this type will be considered. +The default value is `ControlArea`. + +**areaInterchangePMaxMismatch** +Defines the maximum interchange mismatch tolerance for [area interchange control](loadflow.md#area-interchange-control). +The default value is `2 MW` and it must be greater than `0 MW`. + **voltageRemoteControl** The `voltageRemoteControl` property is an optional property that defines if the remote control for voltage controllers has to be modeled. If set to false, any existing voltage remote control is converted to a local control, rescaling the target voltage diff --git a/src/main/java/com/powsybl/openloadflow/AbstractAcOuterLoopConfig.java b/src/main/java/com/powsybl/openloadflow/AbstractAcOuterLoopConfig.java index a18c41551e..1e9f4b6b6a 100644 --- a/src/main/java/com/powsybl/openloadflow/AbstractAcOuterLoopConfig.java +++ b/src/main/java/com/powsybl/openloadflow/AbstractAcOuterLoopConfig.java @@ -11,7 +11,10 @@ import com.powsybl.openloadflow.ac.outerloop.*; import com.powsybl.openloadflow.network.util.ActivePowerDistribution; import com.powsybl.openloadflow.util.PerUnit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.util.List; import java.util.Optional; /** @@ -19,6 +22,8 @@ */ abstract class AbstractAcOuterLoopConfig implements AcOuterLoopConfig { + private static final Logger LOGGER = LoggerFactory.getLogger(AbstractAcOuterLoopConfig.class); + protected AbstractAcOuterLoopConfig() { } @@ -30,6 +35,14 @@ protected static Optional createDistributedSlackOuterLoop(LoadFlowP return Optional.empty(); } + protected static Optional createAreaInterchangeControlOuterLoop(LoadFlowParameters parameters, OpenLoadFlowParameters parametersExt) { + if (parametersExt.isAreaInterchangeControl()) { + ActivePowerDistribution activePowerDistribution = ActivePowerDistribution.create(parameters.getBalanceType(), parametersExt.isLoadPowerFactorConstant(), parametersExt.isUseActiveLimits()); + return Optional.of(new AreaInterchangeControlOuterloop(activePowerDistribution, parametersExt.getSlackBusPMaxMismatch(), parametersExt.getAreaInterchangePMaxMismatch())); + } + return Optional.empty(); + } + protected static Optional createReactiveLimitsOuterLoop(LoadFlowParameters parameters, OpenLoadFlowParameters parametersExt) { if (parameters.isUseReactiveLimits()) { double effectiveMaxReactivePowerMismatch = switch (parametersExt.getNewtonRaphsonStoppingCriteriaType()) { @@ -122,4 +135,18 @@ protected static Optional createAutomationSystemOuterLoop(OpenLoadF } return Optional.empty(); } + + static List filterInconsistentOuterLoops(List outerLoops) { + if (outerLoops.stream().anyMatch(AreaInterchangeControlOuterloop.class::isInstance)) { + return outerLoops.stream().filter(o -> { + if (o instanceof DistributedSlackOuterLoop) { + LOGGER.warn("Distributed slack and area interchange control are both enabled. " + + "Distributed slack outer loop will be disabled, slack will be distributed by the area interchange control."); + return false; + } + return true; + }).toList(); + } + return outerLoops; + } } diff --git a/src/main/java/com/powsybl/openloadflow/DefaultAcOuterLoopConfig.java b/src/main/java/com/powsybl/openloadflow/DefaultAcOuterLoopConfig.java index d100fe82ec..15d020cde4 100644 --- a/src/main/java/com/powsybl/openloadflow/DefaultAcOuterLoopConfig.java +++ b/src/main/java/com/powsybl/openloadflow/DefaultAcOuterLoopConfig.java @@ -23,6 +23,8 @@ public List configure(LoadFlowParameters parameters, OpenLoadFlowPa List outerLoops = new ArrayList<>(5); // primary frequency control createDistributedSlackOuterLoop(parameters, parametersExt).ifPresent(outerLoops::add); + // area interchange control + createAreaInterchangeControlOuterLoop(parameters, parametersExt).ifPresent(outerLoops::add); // secondary voltage control createSecondaryVoltageControlOuterLoop(parametersExt).ifPresent(outerLoops::add); // primary voltage control @@ -38,6 +40,6 @@ public List configure(LoadFlowParameters parameters, OpenLoadFlowPa createShuntVoltageControlOuterLoop(parameters, parametersExt).ifPresent(outerLoops::add); // automation system createAutomationSystemOuterLoop(parametersExt).ifPresent(outerLoops::add); - return outerLoops; + return filterInconsistentOuterLoops(outerLoops); } } diff --git a/src/main/java/com/powsybl/openloadflow/ExplicitAcOuterLoopConfig.java b/src/main/java/com/powsybl/openloadflow/ExplicitAcOuterLoopConfig.java index 294a2227b2..fbc8c18edd 100644 --- a/src/main/java/com/powsybl/openloadflow/ExplicitAcOuterLoopConfig.java +++ b/src/main/java/com/powsybl/openloadflow/ExplicitAcOuterLoopConfig.java @@ -35,7 +35,8 @@ public class ExplicitAcOuterLoopConfig extends AbstractAcOuterLoopConfig { SimpleTransformerVoltageControlOuterLoop.NAME, TransformerVoltageControlOuterLoop.NAME, AutomationSystemOuterLoop.NAME, - IncrementalTransformerReactivePowerControlOuterLoop.NAME); + IncrementalTransformerReactivePowerControlOuterLoop.NAME, + AreaInterchangeControlOuterloop.NAME); private static Optional createOuterLoop(String name, LoadFlowParameters parameters, OpenLoadFlowParameters parametersExt) { return switch (name) { @@ -68,6 +69,7 @@ private static Optional createOuterLoop(String name, LoadFlowParame parametersExt.getGeneratorVoltageControlMinNominalVoltage()); case AutomationSystemOuterLoop.NAME -> createAutomationSystemOuterLoop(parametersExt); case IncrementalTransformerReactivePowerControlOuterLoop.NAME -> createTransformerReactivePowerControlOuterLoop(parametersExt); + case AreaInterchangeControlOuterloop.NAME -> createAreaInterchangeControlOuterLoop(parameters, parametersExt); default -> throw new PowsyblException("Unknown outer loop '" + name + "'"); }; } @@ -89,6 +91,6 @@ public List configure(LoadFlowParameters parameters, OpenLoadFlowPa .flatMap(name -> createOuterLoop(name, parameters, parametersExt).stream()) .toList(); checkTypeUnicity(outerLoops); - return outerLoops; + return filterInconsistentOuterLoops(outerLoops); } } diff --git a/src/main/java/com/powsybl/openloadflow/OpenLoadFlowParameters.java b/src/main/java/com/powsybl/openloadflow/OpenLoadFlowParameters.java index e6c342147d..77f4c9848b 100644 --- a/src/main/java/com/powsybl/openloadflow/OpenLoadFlowParameters.java +++ b/src/main/java/com/powsybl/openloadflow/OpenLoadFlowParameters.java @@ -128,6 +128,10 @@ public enum FictitiousGeneratorVoltageControlCheckMode { protected static final FictitiousGeneratorVoltageControlCheckMode FICTITIOUS_GENERATOR_VOLTAGE_CONTROL_CHECK_MODE_DEFAULT_VALUE = FictitiousGeneratorVoltageControlCheckMode.FORCED; + public static final boolean AREA_INTERCHANGE_CONTROL_DEFAULT_VALUE = false; + + public static final double AREA_INTERCHANGE_P_MAX_MISMATCH_DEFAULT_VALUE = 2.0; + public static final String SLACK_BUS_SELECTION_MODE_PARAM_NAME = "slackBusSelectionMode"; public static final String SLACK_BUSES_IDS_PARAM_NAME = "slackBusesIds"; @@ -264,6 +268,12 @@ public enum FictitiousGeneratorVoltageControlCheckMode { public static final String FICTITIOUS_GENERATOR_VOLTAGE_CONTROL_CHECK_MODE = "fictitiousGeneratorVoltageControlCheckMode"; + public static final String AREA_INTERCHANGE_CONTROL_PARAM_NAME = "areaInterchangeControl"; + + public static final String AREA_INTERCHANGE_CONTROL_AREA_TYPE_PARAM_NAME = "areaInterchangeControlAreaType"; + + public static final String AREA_INTERCHANGE_P_MAX_MISMATCH_PARAM_NAME = "areaInterchangePMaxMismatch"; + public static > List getEnumPossibleValues(Class enumClass) { return EnumSet.allOf(enumClass).stream().map(Enum::name).collect(Collectors.toList()); } @@ -398,7 +408,10 @@ public static > List getEnumPossibleValues(Class en new Parameter(VOLTAGE_TARGET_PRIORITIES_PARAM_NAME, ParameterType.STRING_LIST, "Voltage target priorities for voltage controls", LfNetworkParameters.VOLTAGE_CONTROL_PRIORITIES_DEFAULT_VALUE, getEnumPossibleValues(VoltageControl.Type.class), ParameterScope.FUNCTIONAL, VOLTAGE_CONTROLS_CATEGORY_KEY), new Parameter(TRANSFORMER_VOLTAGE_CONTROL_USE_INITIAL_TAP_POSITION_PARAM_NAME, ParameterType.BOOLEAN, "Maintain initial tap position if possible", LfNetworkParameters.TRANSFORMER_VOLTAGE_CONTROL_USE_INITIAL_TAP_POSITION_DEFAULT_VALUE, ParameterScope.FUNCTIONAL, TRANSFORMER_VOLTAGE_CONTROL_CATEGORY_KEY), new Parameter(GENERATOR_VOLTAGE_CONTROL_MIN_NOMINAL_VOLTAGE_PARAM_NAME, ParameterType.DOUBLE, "Nominal voltage under which generator voltage controls are disabled during transformer voltage control outer loop of mode AFTER_GENERATOR_VOLTAGE_CONTROL, < 0 means automatic detection", OpenLoadFlowParameters.GENERATOR_VOLTAGE_CONTROL_MIN_NOMINAL_VOLTAGE_DEFAULT_VALUE, ParameterScope.FUNCTIONAL, TRANSFORMER_VOLTAGE_CONTROL_CATEGORY_KEY), - new Parameter(FICTITIOUS_GENERATOR_VOLTAGE_CONTROL_CHECK_MODE, ParameterType.STRING, "Specifies fictitious generators active power checks exemption for voltage control", OpenLoadFlowParameters.FICTITIOUS_GENERATOR_VOLTAGE_CONTROL_CHECK_MODE_DEFAULT_VALUE.name(), getEnumPossibleValues(FictitiousGeneratorVoltageControlCheckMode.class), ParameterScope.FUNCTIONAL, GENERATOR_VOLTAGE_CONTROL_CATEGORY_KEY) + new Parameter(FICTITIOUS_GENERATOR_VOLTAGE_CONTROL_CHECK_MODE, ParameterType.STRING, "Specifies fictitious generators active power checks exemption for voltage control", OpenLoadFlowParameters.FICTITIOUS_GENERATOR_VOLTAGE_CONTROL_CHECK_MODE_DEFAULT_VALUE.name(), getEnumPossibleValues(FictitiousGeneratorVoltageControlCheckMode.class), ParameterScope.FUNCTIONAL, GENERATOR_VOLTAGE_CONTROL_CATEGORY_KEY), + new Parameter(AREA_INTERCHANGE_CONTROL_PARAM_NAME, ParameterType.BOOLEAN, "Area interchange control", AREA_INTERCHANGE_CONTROL_DEFAULT_VALUE, ParameterScope.FUNCTIONAL, SLACK_DISTRIBUTION_CATEGORY_KEY), + new Parameter(AREA_INTERCHANGE_CONTROL_AREA_TYPE_PARAM_NAME, ParameterType.STRING, "Area type for area interchange control", LfNetworkParameters.AREA_INTERCHANGE_CONTROL_AREA_TYPE_DEFAULT_VALUE, ParameterScope.FUNCTIONAL, SLACK_DISTRIBUTION_CATEGORY_KEY), + new Parameter(AREA_INTERCHANGE_P_MAX_MISMATCH_PARAM_NAME, ParameterType.DOUBLE, "Area interchange max active power mismatch", AREA_INTERCHANGE_P_MAX_MISMATCH_DEFAULT_VALUE, ParameterScope.FUNCTIONAL, SLACK_DISTRIBUTION_CATEGORY_KEY) ); public enum VoltageInitModeOverride { @@ -576,6 +589,12 @@ public enum ReactiveRangeCheckMode { private FictitiousGeneratorVoltageControlCheckMode fictitiousGeneratorVoltageControlCheckMode = FICTITIOUS_GENERATOR_VOLTAGE_CONTROL_CHECK_MODE_DEFAULT_VALUE; + private boolean areaInterchangeControl = AREA_INTERCHANGE_CONTROL_DEFAULT_VALUE; + + private String areaInterchangeControlAreaType = LfNetworkParameters.AREA_INTERCHANGE_CONTROL_AREA_TYPE_DEFAULT_VALUE; + + private double areaInterchangePMaxMismatch = AREA_INTERCHANGE_P_MAX_MISMATCH_DEFAULT_VALUE; + public static double checkParameterValue(double parameterValue, boolean condition, String parameterName) { if (!condition) { throw new IllegalArgumentException("Invalid value for parameter " + parameterName + ": " + parameterValue); @@ -1272,6 +1291,35 @@ public OpenLoadFlowParameters setFictitiousGeneratorVoltageControlCheckMode(Fict return this; } + public boolean isAreaInterchangeControl() { + return areaInterchangeControl; + } + + public OpenLoadFlowParameters setAreaInterchangeControl(boolean areaInterchangeControl) { + this.areaInterchangeControl = areaInterchangeControl; + return this; + } + + public String getAreaInterchangeControlAreaType() { + return areaInterchangeControlAreaType; + } + + public OpenLoadFlowParameters setAreaInterchangeControlAreaType(String areaInterchangeControlAreaType) { + this.areaInterchangeControlAreaType = Objects.requireNonNull(areaInterchangeControlAreaType); + return this; + } + + public double getAreaInterchangePMaxMismatch() { + return areaInterchangePMaxMismatch; + } + + public OpenLoadFlowParameters setAreaInterchangePMaxMismatch(double areaInterchangePMaxMismatch) { + this.areaInterchangePMaxMismatch = checkParameterValue(areaInterchangePMaxMismatch, + areaInterchangePMaxMismatch >= 0, + AREA_INTERCHANGE_P_MAX_MISMATCH_PARAM_NAME); + return this; + } + public static OpenLoadFlowParameters load() { return load(PlatformConfig.defaultConfig()); } @@ -1347,7 +1395,10 @@ public static OpenLoadFlowParameters load(PlatformConfig platformConfig) { .setWriteReferenceTerminals(config.getBooleanProperty(WRITE_REFERENCE_TERMINALS_PARAM_NAME, WRITE_REFERENCE_TERMINALS_DEFAULT_VALUE)) .setVoltageTargetPriorities(config.getStringListProperty(VOLTAGE_TARGET_PRIORITIES_PARAM_NAME, LfNetworkParameters.VOLTAGE_CONTROL_PRIORITIES_DEFAULT_VALUE)) .setTransformerVoltageControlUseInitialTapPosition(config.getBooleanProperty(TRANSFORMER_VOLTAGE_CONTROL_USE_INITIAL_TAP_POSITION_PARAM_NAME, LfNetworkParameters.TRANSFORMER_VOLTAGE_CONTROL_USE_INITIAL_TAP_POSITION_DEFAULT_VALUE)) - .setGeneratorVoltageControlMinNominalVoltage(config.getDoubleProperty(GENERATOR_VOLTAGE_CONTROL_MIN_NOMINAL_VOLTAGE_PARAM_NAME, GENERATOR_VOLTAGE_CONTROL_MIN_NOMINAL_VOLTAGE_DEFAULT_VALUE))); + .setGeneratorVoltageControlMinNominalVoltage(config.getDoubleProperty(GENERATOR_VOLTAGE_CONTROL_MIN_NOMINAL_VOLTAGE_PARAM_NAME, GENERATOR_VOLTAGE_CONTROL_MIN_NOMINAL_VOLTAGE_DEFAULT_VALUE)) + .setAreaInterchangeControl(config.getBooleanProperty(AREA_INTERCHANGE_CONTROL_PARAM_NAME, AREA_INTERCHANGE_CONTROL_DEFAULT_VALUE)) + .setAreaInterchangeControlAreaType(config.getStringProperty(AREA_INTERCHANGE_CONTROL_AREA_TYPE_PARAM_NAME, LfNetworkParameters.AREA_INTERCHANGE_CONTROL_AREA_TYPE_DEFAULT_VALUE)) + .setAreaInterchangePMaxMismatch(config.getDoubleProperty(AREA_INTERCHANGE_P_MAX_MISMATCH_PARAM_NAME, AREA_INTERCHANGE_P_MAX_MISMATCH_DEFAULT_VALUE))); return parameters; } @@ -1502,11 +1553,17 @@ public OpenLoadFlowParameters update(Map properties) { .ifPresent(prop -> this.setGeneratorVoltageControlMinNominalVoltage(Double.parseDouble(prop))); Optional.ofNullable(properties.get(FICTITIOUS_GENERATOR_VOLTAGE_CONTROL_CHECK_MODE)) .ifPresent(prop -> this.setFictitiousGeneratorVoltageControlCheckMode(FictitiousGeneratorVoltageControlCheckMode.valueOf(prop))); + Optional.ofNullable(properties.get(AREA_INTERCHANGE_CONTROL_PARAM_NAME)) + .ifPresent(prop -> this.setAreaInterchangeControl(Boolean.parseBoolean(prop))); + Optional.ofNullable(properties.get(AREA_INTERCHANGE_CONTROL_AREA_TYPE_PARAM_NAME)) + .ifPresent(this::setAreaInterchangeControlAreaType); + Optional.ofNullable(properties.get(AREA_INTERCHANGE_P_MAX_MISMATCH_PARAM_NAME)) + .ifPresent(prop -> this.setAreaInterchangePMaxMismatch(Double.parseDouble(prop))); return this; } public Map toMap() { - Map map = new LinkedHashMap<>(66); + Map map = new LinkedHashMap<>(71); map.put(SLACK_BUS_SELECTION_MODE_PARAM_NAME, slackBusSelectionMode); map.put(SLACK_BUSES_IDS_PARAM_NAME, slackBusesIds); map.put(SLACK_DISTRIBUTION_FAILURE_BEHAVIOR_PARAM_NAME, slackDistributionFailureBehavior); @@ -1575,6 +1632,9 @@ public Map toMap() { map.put(TRANSFORMER_VOLTAGE_CONTROL_USE_INITIAL_TAP_POSITION_PARAM_NAME, transformerVoltageControlUseInitialTapPosition); map.put(GENERATOR_VOLTAGE_CONTROL_MIN_NOMINAL_VOLTAGE_PARAM_NAME, generatorVoltageControlMinNominalVoltage); map.put(FICTITIOUS_GENERATOR_VOLTAGE_CONTROL_CHECK_MODE, fictitiousGeneratorVoltageControlCheckMode); + map.put(AREA_INTERCHANGE_CONTROL_PARAM_NAME, areaInterchangeControl); + map.put(AREA_INTERCHANGE_CONTROL_AREA_TYPE_PARAM_NAME, areaInterchangeControlAreaType); + map.put(AREA_INTERCHANGE_P_MAX_MISMATCH_PARAM_NAME, areaInterchangePMaxMismatch); return map; } @@ -1658,7 +1718,7 @@ static VoltageInitializer getVoltageInitializer(LoadFlowParameters parameters, O case PREVIOUS_VALUES: return new PreviousValueVoltageInitializer(); case DC_VALUES: - return new DcValueVoltageInitializer(networkParameters, parameters.isDistributedSlack(), parameters.getBalanceType(), parameters.isDcUseTransformerRatio(), parametersExt.getDcApproximationType(), matrixFactory, parametersExt.getMaxOuterLoopIterations()); + return new DcValueVoltageInitializer(networkParameters, parameters.isDistributedSlack() || parametersExt.isAreaInterchangeControl(), parameters.getBalanceType(), parameters.isDcUseTransformerRatio(), parametersExt.getDcApproximationType(), matrixFactory, parametersExt.getMaxOuterLoopIterations()); default: throw new UnsupportedOperationException("Unsupported voltage init mode: " + parameters.getVoltageInitMode()); } @@ -1676,7 +1736,7 @@ static VoltageInitializer getExtendedVoltageInitializer(LoadFlowParameters param case FULL_VOLTAGE: return new FullVoltageInitializer(new VoltageMagnitudeInitializer(parameters.isTransformerVoltageControlOn(), matrixFactory, networkParameters.getLowImpedanceThreshold()), new DcValueVoltageInitializer(networkParameters, - parameters.isDistributedSlack(), + parameters.isDistributedSlack() || parametersExt.isAreaInterchangeControl(), parameters.getBalanceType(), parameters.isDcUseTransformerRatio(), parametersExt.getDcApproximationType(), @@ -1703,7 +1763,7 @@ static LfNetworkParameters getNetworkParameters(LoadFlowParameters parameters, O .setDisableVoltageControlOfGeneratorsOutsideActivePowerLimits(parametersExt.isDisableVoltageControlOfGeneratorsOutsideActivePowerLimits()) .setComputeMainConnectedComponentOnly(parameters.getConnectedComponentMode() == LoadFlowParameters.ConnectedComponentMode.MAIN) .setCountriesToBalance(parameters.getCountriesToBalance()) - .setDistributedOnConformLoad(parameters.isDistributedSlack() && parameters.getBalanceType() == LoadFlowParameters.BalanceType.PROPORTIONAL_TO_CONFORM_LOAD) + .setDistributedOnConformLoad((parameters.isDistributedSlack() || parametersExt.isAreaInterchangeControl()) && parameters.getBalanceType() == LoadFlowParameters.BalanceType.PROPORTIONAL_TO_CONFORM_LOAD) .setPhaseControl(parameters.isPhaseShifterRegulationOn()) .setTransformerVoltageControl(parameters.isTransformerVoltageControlOn()) .setVoltagePerReactivePowerControl(parametersExt.isVoltagePerReactivePowerControl()) @@ -1729,7 +1789,9 @@ static LfNetworkParameters getNetworkParameters(LoadFlowParameters parameters, O .setSimulateAutomationSystems(parametersExt.isSimulateAutomationSystems()) .setReferenceBusSelector(ReferenceBusSelector.fromMode(parametersExt.getReferenceBusSelectionMode())) .setVoltageTargetPriorities(parametersExt.getVoltageTargetPriorities()) - .setFictitiousGeneratorVoltageControlCheckMode(parametersExt.getFictitiousGeneratorVoltageControlCheckMode()); + .setFictitiousGeneratorVoltageControlCheckMode(parametersExt.getFictitiousGeneratorVoltageControlCheckMode()) + .setAreaInterchangeControl(parametersExt.isAreaInterchangeControl()) + .setAreaInterchangeControlAreaType(parametersExt.getAreaInterchangeControlAreaType()); } public static AcLoadFlowParameters createAcParameters(Network network, LoadFlowParameters parameters, OpenLoadFlowParameters parametersExt, @@ -1845,7 +1907,7 @@ public static DcLoadFlowParameters createDcParameters(LoadFlowParameters paramet .setDisableVoltageControlOfGeneratorsOutsideActivePowerLimits(parametersExt.isDisableVoltageControlOfGeneratorsOutsideActivePowerLimits()) .setComputeMainConnectedComponentOnly(parameters.getConnectedComponentMode() == LoadFlowParameters.ConnectedComponentMode.MAIN) .setCountriesToBalance(parameters.getCountriesToBalance()) - .setDistributedOnConformLoad(parameters.isDistributedSlack() && parameters.getBalanceType() == LoadFlowParameters.BalanceType.PROPORTIONAL_TO_CONFORM_LOAD) + .setDistributedOnConformLoad((parameters.isDistributedSlack() || parametersExt.isAreaInterchangeControl()) && parameters.getBalanceType() == LoadFlowParameters.BalanceType.PROPORTIONAL_TO_CONFORM_LOAD) .setPhaseControl(parameters.isPhaseShifterRegulationOn()) .setTransformerVoltageControl(false) .setVoltagePerReactivePowerControl(false) @@ -1979,7 +2041,10 @@ public static boolean equals(LoadFlowParameters parameters1, LoadFlowParameters Objects.equals(extension1.getVoltageTargetPriorities(), extension2.getVoltageTargetPriorities()) && extension1.isTransformerVoltageControlUseInitialTapPosition() == extension2.isTransformerVoltageControlUseInitialTapPosition() && extension1.getGeneratorVoltageControlMinNominalVoltage() == extension2.getGeneratorVoltageControlMinNominalVoltage() && - extension1.getFictitiousGeneratorVoltageControlCheckMode() == extension2.getFictitiousGeneratorVoltageControlCheckMode(); + extension1.getFictitiousGeneratorVoltageControlCheckMode() == extension2.getFictitiousGeneratorVoltageControlCheckMode() && + extension1.isAreaInterchangeControl() == extension2.isAreaInterchangeControl() && + Objects.equals(extension1.getAreaInterchangeControlAreaType(), extension2.getAreaInterchangeControlAreaType()) && + extension1.getAreaInterchangePMaxMismatch() == extension2.getAreaInterchangePMaxMismatch(); } public static LoadFlowParameters clone(LoadFlowParameters parameters) { @@ -2072,7 +2137,10 @@ public static LoadFlowParameters clone(LoadFlowParameters parameters) { .setVoltageTargetPriorities(extension.getVoltageTargetPriorities()) .setTransformerVoltageControlUseInitialTapPosition(extension.isTransformerVoltageControlUseInitialTapPosition()) .setGeneratorVoltageControlMinNominalVoltage(extension.getGeneratorVoltageControlMinNominalVoltage()) - .setFictitiousGeneratorVoltageControlCheckMode(extension.getFictitiousGeneratorVoltageControlCheckMode()); + .setFictitiousGeneratorVoltageControlCheckMode(extension.getFictitiousGeneratorVoltageControlCheckMode()) + .setAreaInterchangeControl(extension.isAreaInterchangeControl()) + .setAreaInterchangeControlAreaType(extension.getAreaInterchangeControlAreaType()) + .setAreaInterchangePMaxMismatch(extension.getAreaInterchangePMaxMismatch()); if (extension2 != null) { parameters2.addExtension(OpenLoadFlowParameters.class, extension2); diff --git a/src/main/java/com/powsybl/openloadflow/OpenLoadFlowProvider.java b/src/main/java/com/powsybl/openloadflow/OpenLoadFlowProvider.java index b0fb8234d5..8b92a03da3 100644 --- a/src/main/java/com/powsybl/openloadflow/OpenLoadFlowProvider.java +++ b/src/main/java/com/powsybl/openloadflow/OpenLoadFlowProvider.java @@ -111,7 +111,7 @@ private void updateAcState(Network network, LoadFlowParameters parameters, OpenL parameters.isPhaseShifterRegulationOn(), parameters.isTransformerVoltageControlOn(), parametersExt.isTransformerReactivePowerControl(), - parameters.isDistributedSlack() && (parameters.getBalanceType() == LoadFlowParameters.BalanceType.PROPORTIONAL_TO_LOAD || parameters.getBalanceType() == LoadFlowParameters.BalanceType.PROPORTIONAL_TO_CONFORM_LOAD) && parametersExt.isLoadPowerFactorConstant(), + (parameters.isDistributedSlack() || parametersExt.isAreaInterchangeControl()) && (parameters.getBalanceType() == LoadFlowParameters.BalanceType.PROPORTIONAL_TO_LOAD || parameters.getBalanceType() == LoadFlowParameters.BalanceType.PROPORTIONAL_TO_CONFORM_LOAD) && parametersExt.isLoadPowerFactorConstant(), parameters.isDc(), acParameters.getNetworkParameters().isBreakers(), parametersExt.getReactivePowerDispatchMode(), diff --git a/src/main/java/com/powsybl/openloadflow/ac/AcloadFlowEngine.java b/src/main/java/com/powsybl/openloadflow/ac/AcloadFlowEngine.java index 6331fb5f26..c41528229d 100644 --- a/src/main/java/com/powsybl/openloadflow/ac/AcloadFlowEngine.java +++ b/src/main/java/com/powsybl/openloadflow/ac/AcloadFlowEngine.java @@ -12,6 +12,7 @@ import com.powsybl.openloadflow.ac.equations.AcEquationType; import com.powsybl.openloadflow.ac.equations.AcVariableType; import com.powsybl.openloadflow.ac.outerloop.AcOuterLoop; +import com.powsybl.openloadflow.ac.outerloop.AreaInterchangeControlOuterloop; import com.powsybl.openloadflow.ac.outerloop.DistributedSlackOuterLoop; import com.powsybl.openloadflow.ac.solver.*; import com.powsybl.openloadflow.lf.LoadFlowEngine; @@ -191,7 +192,7 @@ public AcLoadFlowResult run() { for (var outerLoopAndContext : Lists.reverse(outerLoopsAndContexts)) { var outerLoop = outerLoopAndContext.getLeft(); var outerLoopContext = outerLoopAndContext.getRight(); - if (outerLoop instanceof DistributedSlackOuterLoop) { + if (outerLoop instanceof DistributedSlackOuterLoop || outerLoop instanceof AreaInterchangeControlOuterloop) { distributedActivePower = ((DistributedSlackContextData) outerLoopContext.getData()).getDistributedActivePower(); } outerLoop.cleanup(outerLoopContext); diff --git a/src/main/java/com/powsybl/openloadflow/ac/outerloop/AreaInterchangeControlOuterloop.java b/src/main/java/com/powsybl/openloadflow/ac/outerloop/AreaInterchangeControlOuterloop.java new file mode 100644 index 0000000000..bf47175c39 --- /dev/null +++ b/src/main/java/com/powsybl/openloadflow/ac/outerloop/AreaInterchangeControlOuterloop.java @@ -0,0 +1,270 @@ +/** + * 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.ac.outerloop; + +import com.powsybl.commons.PowsyblException; +import com.powsybl.commons.report.ReportNode; +import com.powsybl.openloadflow.OpenLoadFlowParameters; +import com.powsybl.openloadflow.ac.AcOuterLoopContext; +import com.powsybl.openloadflow.lf.outerloop.AreaInterchangeControlContextData; +import com.powsybl.openloadflow.lf.outerloop.OuterLoopResult; +import com.powsybl.openloadflow.lf.outerloop.OuterLoopStatus; +import com.powsybl.openloadflow.network.*; +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 org.slf4j.LoggerFactory; + +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * @author Valentin Mouradian {@literal } + */ +public class AreaInterchangeControlOuterloop implements AcOuterLoop { + + private static final Logger LOGGER = LoggerFactory.getLogger(AreaInterchangeControlOuterloop.class); + + public static final String NAME = "AreaInterchangeControl"; + + private static final String DEFAULT_NO_AREA_NAME = "NO_AREA"; + private static final String FAILED_TO_DISTRIBUTE_INTERCHANGE_ACTIVE_POWER_MISMATCH = "Failed to distribute interchange active power mismatch"; + + private final double areaInterchangePMaxMismatch; + + private final double slackBusPMaxMismatch; + + private final ActivePowerDistribution activePowerDistribution; + + private final AcOuterLoop noAreaOuterLoop; + + public AreaInterchangeControlOuterloop(ActivePowerDistribution activePowerDistribution, double slackBusPMaxMismatch, double areaInterchangePMaxMismatch) { + this.activePowerDistribution = Objects.requireNonNull(activePowerDistribution); + this.areaInterchangePMaxMismatch = areaInterchangePMaxMismatch; + this.slackBusPMaxMismatch = slackBusPMaxMismatch; + this.noAreaOuterLoop = new DistributedSlackOuterLoop(activePowerDistribution, this.slackBusPMaxMismatch); + } + + @Override + public String getName() { + return NAME; + } + + @Override + public void initialize(AcOuterLoopContext context) { + LfNetwork network = context.getNetwork(); + if (!network.hasArea()) { + noAreaOuterLoop.initialize(context); + return; + } + var contextData = new AreaInterchangeControlContextData(listBusesWithoutArea(network), allocateSlackDistributionParticipationFactors(network)); + context.setData(contextData); + } + + @Override + public OuterLoopResult check(AcOuterLoopContext context, ReportNode reportNode) { + LfNetwork network = context.getNetwork(); + if (!network.hasArea()) { + return noAreaOuterLoop.check(context, reportNode); + } + double slackBusActivePowerMismatch = context.getLastSolverResult().getSlackBusActivePowerMismatch(); + 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>> remainingMismatchMap = new HashMap<>(); + remainingMismatchMap.put(DEFAULT_NO_AREA_NAME, Pair.of(busesWithoutArea, slackBusActivePowerMismatch)); + Map resultNoArea = distributeActivePower(remainingMismatchMap); + + // 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(remainingMismatchMap, resultNoArea, reportNode, context); + } else { + int areasCount = (int) network.getAreaStream().count(); + remainingMismatchMap = network.getAreaStream().collect(Collectors.toMap(LfArea::getId, area -> Pair.of(area.getBuses(), mismatchToSplitAmongAreas / areasCount))); + Map resultByArea = distributeActivePower(remainingMismatchMap); + return buildOuterLoopResult(remainingMismatchMap, 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); + } + + private OuterLoopResult buildOuterLoopResult(Map, Double>> areas, Map resultByArea, ReportNode reportNode, AcOuterLoopContext context) { + Map remainingMismatchByArea = resultByArea.entrySet().stream() + .filter(e -> !lessThanInterchangeMaxMismatch(e.getValue().remainingMismatch())) + .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().remainingMismatch())); + double totalDistributedActivePower = resultByArea.entrySet().stream().mapToDouble(e -> areas.get(e.getKey()).getRight() - e.getValue().remainingMismatch()).sum(); + boolean movedBuses = resultByArea.values().stream().map(ActivePowerDistribution.Result::movedBuses).reduce(false, (a, b) -> a || b); + Map iterationsByArea = resultByArea.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().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); + } + ); + return distributionFailureResult(context, movedBuses, contextData, totalDistributedActivePower); + } 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); + } + } + } + + private 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; + } + + boolean lessThanInterchangeMaxMismatch(double mismatch) { + return Math.abs(mismatch) <= this.areaInterchangePMaxMismatch / PerUnit.SB || lessThanSlackBusMaxMismatch(mismatch); + } + + boolean lessThanSlackBusMaxMismatch(double mismatch) { + return Math.abs(mismatch) <= this.slackBusPMaxMismatch / PerUnit.SB || Math.abs(mismatch) <= ActivePowerDistribution.P_RESIDUE_EPS; + } + + private OuterLoopResult distributionFailureResult(AcOuterLoopContext context, boolean movedBuses, AreaInterchangeControlContextData contextData, double totalDistributedActivePower) { + OpenLoadFlowParameters.SlackDistributionFailureBehavior slackDistributionFailureBehavior = context.getLoadFlowContext().getParameters().getSlackDistributionFailureBehavior(); + if (OpenLoadFlowParameters.SlackDistributionFailureBehavior.DISTRIBUTE_ON_REFERENCE_GENERATOR == slackDistributionFailureBehavior) { + LOGGER.error("Distribute on reference generator is not supported in AreaInterchangeControlOuterloop, falling back to FAIL mode"); + slackDistributionFailureBehavior = OpenLoadFlowParameters.SlackDistributionFailureBehavior.FAIL; + } + + switch (slackDistributionFailureBehavior) { + 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 -> { + // Mismatches reported in LoadFlowResult on slack bus(es) are the mismatches of the last NR run. + // Since we will not be re-running an NR, 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 IllegalArgumentException("Unknown slackDistributionFailureBehavior"); + } + } + + private double getInterchangeMismatch(LfArea area) { + return area.getInterchange() - area.getInterchangeTarget(); + } + + private double getInterchangeMismatchWithSlack(LfArea area, double slackBusActivePowerMismatch, Map areaSlackDistributionParticipationFactor) { + return area.getInterchange() - area.getInterchangeTarget() + getSlackInjection(area.getId(), slackBusActivePowerMismatch, areaSlackDistributionParticipationFactor); + } + + private double getSlackInjection(String areaId, double slackBusActivePowerMismatch, Map areaSlackDistributionParticipationFactor) { + return areaSlackDistributionParticipationFactor.getOrDefault(areaId, 0.0) * slackBusActivePowerMismatch; + } + + private Set listBusesWithoutArea(LfNetwork network) { + return network.getBuses().stream() + .filter(b -> b.getArea().isEmpty()) + .filter(b -> !b.isFictitious()) + .collect(Collectors.toSet()); + } + + private 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; + } + +} diff --git a/src/main/java/com/powsybl/openloadflow/lf/outerloop/AreaInterchangeControlContextData.java b/src/main/java/com/powsybl/openloadflow/lf/outerloop/AreaInterchangeControlContextData.java new file mode 100644 index 0000000000..f07c8008d2 --- /dev/null +++ b/src/main/java/com/powsybl/openloadflow/lf/outerloop/AreaInterchangeControlContextData.java @@ -0,0 +1,43 @@ +/** + * 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.openloadflow.network.LfBus; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * @author Valentin Mouradian {@literal } + */ +public class AreaInterchangeControlContextData extends DistributedSlackContextData { + + private final Set busesWithoutArea; + + /** + * 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 areaSlackDistributionParticipationFactor; + + public AreaInterchangeControlContextData(Set busesWithoutArea, Map areaSlackDistributionParticipationFactor) { + super(); + this.busesWithoutArea = new HashSet<>(busesWithoutArea); + this.areaSlackDistributionParticipationFactor = new HashMap<>(areaSlackDistributionParticipationFactor); + } + + public Set getBusesWithoutArea() { + return busesWithoutArea; + } + + public Map getAreaSlackDistributionParticipationFactor() { + return areaSlackDistributionParticipationFactor; + } + +} diff --git a/src/main/java/com/powsybl/openloadflow/network/AbstractLfNetworkLoaderPostProcessor.java b/src/main/java/com/powsybl/openloadflow/network/AbstractLfNetworkLoaderPostProcessor.java index 3bd5762803..8cde79b8bd 100644 --- a/src/main/java/com/powsybl/openloadflow/network/AbstractLfNetworkLoaderPostProcessor.java +++ b/src/main/java/com/powsybl/openloadflow/network/AbstractLfNetworkLoaderPostProcessor.java @@ -31,4 +31,9 @@ public void onBranchAdded(Object element, LfBranch lfBranch) { public void onInjectionAdded(Object element, LfBus lfBus) { // to implement } + + @Override + public void onAreaAdded(Object element, LfArea lfArea) { + // to implement + } } diff --git a/src/main/java/com/powsybl/openloadflow/network/LfArea.java b/src/main/java/com/powsybl/openloadflow/network/LfArea.java new file mode 100644 index 0000000000..cd80220393 --- /dev/null +++ b/src/main/java/com/powsybl/openloadflow/network/LfArea.java @@ -0,0 +1,37 @@ +/** + * 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.network; + +import java.util.Set; + +/** + * @author Valentin Mouradian {@literal } + */ +public interface LfArea extends PropertyBag { + String getId(); + + double getInterchangeTarget(); + + void setInterchangeTarget(double interchangeTarget); + + double getInterchange(); + + Set getBuses(); + + Set getBoundaries(); + + LfNetwork getNetwork(); + + public interface Boundary { + LfBranch getBranch(); + + double getP(); + + } + +} diff --git a/src/main/java/com/powsybl/openloadflow/network/LfBus.java b/src/main/java/com/powsybl/openloadflow/network/LfBus.java index 6f96340a71..60d6355728 100644 --- a/src/main/java/com/powsybl/openloadflow/network/LfBus.java +++ b/src/main/java/com/powsybl/openloadflow/network/LfBus.java @@ -215,4 +215,8 @@ default Optional getCountry() { LfAsymBus getAsym(); void setAsym(LfAsymBus asym); + + Optional getArea(); + + void setArea(LfArea area); } diff --git a/src/main/java/com/powsybl/openloadflow/network/LfNetwork.java b/src/main/java/com/powsybl/openloadflow/network/LfNetwork.java index c9ce489c2b..47670e25ed 100644 --- a/src/main/java/com/powsybl/openloadflow/network/LfNetwork.java +++ b/src/main/java/com/powsybl/openloadflow/network/LfNetwork.java @@ -28,6 +28,7 @@ import java.util.*; import java.util.concurrent.TimeUnit; import java.util.function.Predicate; +import java.util.stream.Stream; import static com.powsybl.openloadflow.util.Markers.PERFORMANCE_MARKER; @@ -76,6 +77,8 @@ public class LfNetwork extends AbstractPropertyBag implements PropertyBag { private final Map loadsById = new HashMap<>(); + private final Map areasById = new HashMap<>(); + private final List hvdcs = new ArrayList<>(); private final Map hvdcsById = new HashMap<>(); @@ -298,6 +301,11 @@ public void addBus(LfBus bus) { bus.getLoads().forEach(load -> load.getOriginalIds().forEach(id -> loadsById.put(id, load))); } + public void addArea(LfArea area) { + Objects.requireNonNull(area); + areasById.put(area.getId(), area); + } + public List getBuses() { return busesByIndex; } @@ -365,6 +373,19 @@ public LfLoad getLoadById(String id) { return loadsById.get(id); } + public Stream getAreaStream() { + return areasById.values().stream(); + } + + public boolean hasArea() { + return !areasById.isEmpty(); + } + + public LfArea getAreaById(String id) { + Objects.requireNonNull(id); + return areasById.get(id); + } + public void addHvdc(LfHvdc hvdc) { Objects.requireNonNull(hvdc); hvdc.setNum(hvdcs.size()); diff --git a/src/main/java/com/powsybl/openloadflow/network/LfNetworkLoaderPostProcessor.java b/src/main/java/com/powsybl/openloadflow/network/LfNetworkLoaderPostProcessor.java index 376fdb6819..434415fb2b 100644 --- a/src/main/java/com/powsybl/openloadflow/network/LfNetworkLoaderPostProcessor.java +++ b/src/main/java/com/powsybl/openloadflow/network/LfNetworkLoaderPostProcessor.java @@ -35,4 +35,6 @@ static List findAll() { void onBranchAdded(Object element, LfBranch lfBranch); void onInjectionAdded(Object element, LfBus lfBus); + + void onAreaAdded(Object element, LfArea lfArea); } diff --git a/src/main/java/com/powsybl/openloadflow/network/LfNetworkParameters.java b/src/main/java/com/powsybl/openloadflow/network/LfNetworkParameters.java index b7de3a51f2..6d80e48fe3 100644 --- a/src/main/java/com/powsybl/openloadflow/network/LfNetworkParameters.java +++ b/src/main/java/com/powsybl/openloadflow/network/LfNetworkParameters.java @@ -56,6 +56,8 @@ public class LfNetworkParameters { public static final boolean SIMULATE_AUTOMATION_SYSTEMS_DEFAULT_VALUE = false; + public static final String AREA_INTERCHANGE_CONTROL_AREA_TYPE_DEFAULT_VALUE = "ControlArea"; + private SlackBusSelector slackBusSelector = new FirstSlackBusSelector(SLACK_BUS_COUNTRY_FILTER_DEFAULT_VALUE); private GraphConnectivityFactory connectivityFactory = new EvenShiloachGraphDecrementalConnectivityFactory<>(); @@ -142,6 +144,10 @@ public class LfNetworkParameters { private OpenLoadFlowParameters.FictitiousGeneratorVoltageControlCheckMode fictitiousGeneratorVoltageControlCheckMode = OpenLoadFlowParameters.FictitiousGeneratorVoltageControlCheckMode.FORCED; + private boolean areaInterchangeControl = OpenLoadFlowParameters.AREA_INTERCHANGE_CONTROL_DEFAULT_VALUE; + + private String areaInterchangeControlAreaType = AREA_INTERCHANGE_CONTROL_AREA_TYPE_DEFAULT_VALUE; + public LfNetworkParameters() { } @@ -186,6 +192,8 @@ public LfNetworkParameters(LfNetworkParameters other) { this.referenceBusSelector = other.referenceBusSelector; this.voltageTargetPriorities = new ArrayList<>(other.voltageTargetPriorities); this.fictitiousGeneratorVoltageControlCheckMode = other.fictitiousGeneratorVoltageControlCheckMode; + this.areaInterchangeControl = other.areaInterchangeControl; + this.areaInterchangeControlAreaType = other.areaInterchangeControlAreaType; } public SlackBusSelector getSlackBusSelector() { @@ -571,6 +579,24 @@ public int getVoltageTargetPriority(VoltageControl.Type voltageControlType) { return priority; } + public boolean isAreaInterchangeControl() { + return areaInterchangeControl; + } + + public LfNetworkParameters setAreaInterchangeControl(boolean areaInterchangeControl) { + this.areaInterchangeControl = areaInterchangeControl; + return this; + } + + public String getAreaInterchangeControlAreaType() { + return areaInterchangeControlAreaType; + } + + public LfNetworkParameters setAreaInterchangeControlAreaType(String areaInterchangeControlAreaType) { + this.areaInterchangeControlAreaType = areaInterchangeControlAreaType; + return this; + } + @Override public String toString() { return "LfNetworkParameters(" + @@ -610,6 +636,8 @@ public String toString() { ", referenceBusSelector=" + referenceBusSelector.getClass().getSimpleName() + ", voltageTargetPriorities=" + voltageTargetPriorities + ", fictitiousGeneratorVoltageControlCheckMode=" + fictitiousGeneratorVoltageControlCheckMode + + ", areaInterchangeControl=" + areaInterchangeControl + + ", areaInterchangeControlAreaType=" + areaInterchangeControlAreaType + ')'; } } diff --git a/src/main/java/com/powsybl/openloadflow/network/impl/AbstractLfBus.java b/src/main/java/com/powsybl/openloadflow/network/impl/AbstractLfBus.java index 721bd06828..5300c52fd0 100644 --- a/src/main/java/com/powsybl/openloadflow/network/impl/AbstractLfBus.java +++ b/src/main/java/com/powsybl/openloadflow/network/impl/AbstractLfBus.java @@ -87,6 +87,8 @@ public abstract class AbstractLfBus extends AbstractElement implements LfBus { protected LfAsymBus asym; + private LfArea area = null; + protected AbstractLfBus(LfNetwork network, double v, double angle, boolean distributedOnConformLoad) { super(network); this.v = v; @@ -820,4 +822,14 @@ public void setAsym(LfAsymBus asym) { this.asym = asym; asym.setBus(this); } + + @Override + public Optional getArea() { + return Optional.ofNullable(area); + } + + @Override + public void setArea(LfArea area) { + this.area = area; + } } diff --git a/src/main/java/com/powsybl/openloadflow/network/impl/LfAreaImpl.java b/src/main/java/com/powsybl/openloadflow/network/impl/LfAreaImpl.java new file mode 100644 index 0000000000..c58757c61f --- /dev/null +++ b/src/main/java/com/powsybl/openloadflow/network/impl/LfAreaImpl.java @@ -0,0 +1,122 @@ +/** + * 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.network.impl; + +import com.powsybl.iidm.network.*; +import com.powsybl.iidm.network.util.SV; +import com.powsybl.openloadflow.network.*; +import com.powsybl.openloadflow.util.PerUnit; + +import java.util.*; + +/** + * @author Valentin Mouradian {@literal } + */ +public class LfAreaImpl extends AbstractPropertyBag implements LfArea { + + private final LfNetwork network; + private final Ref areaRef; + + private double interchangeTarget; + + private final Set buses; + + private final Set boundaries; + + protected LfAreaImpl(Area area, Set buses, Set boundaries, LfNetwork network, LfNetworkParameters parameters) { + this.network = network; + this.areaRef = Ref.create(area, parameters.isCacheEnabled()); + this.interchangeTarget = area.getInterchangeTarget().orElse(0.0) / PerUnit.SB; + this.buses = buses; + this.boundaries = boundaries; + } + + public static LfAreaImpl create(Area area, Set buses, Set boundaries, LfNetwork network, LfNetworkParameters parameters) { + LfAreaImpl lfArea = new LfAreaImpl(area, buses, boundaries, network, parameters); + lfArea.getBuses().forEach(bus -> bus.setArea(lfArea)); + return lfArea; + } + + private Area getArea() { + return areaRef.get(); + } + + @Override + public String getId() { + return getArea().getId(); + } + + @Override + public double getInterchangeTarget() { + return interchangeTarget; + } + + @Override + public void setInterchangeTarget(double interchangeTarget) { + this.interchangeTarget = interchangeTarget; + } + + @Override + public double getInterchange() { + return boundaries.stream().mapToDouble(Boundary::getP).sum(); + } + + @Override + public Set getBuses() { + return buses; + } + + @Override + public Set getBoundaries() { + return boundaries; + } + + @Override + public LfNetwork getNetwork() { + return network; + } + + public static class BoundaryImpl implements Boundary { + private final LfBranch branch; + private final TwoSides side; + + public BoundaryImpl(LfBranch branch, TwoSides side) { + this.branch = Objects.requireNonNull(branch); + this.side = Objects.requireNonNull(side); + } + + @Override + public LfBranch getBranch() { + return branch; + } + + @Override + public double getP() { + if (branch.isDisabled()) { + return 0.0; + } + if (branch instanceof LfTieLineBranch lfTieLineBranch) { + if (side == TwoSides.ONE) { + DanglingLine danglingLine1 = lfTieLineBranch.getHalf1(); + double nominalV1 = danglingLine1.getTerminal().getVoltageLevel().getNominalV(); + return new SV(lfTieLineBranch.getP1().eval() * PerUnit.SB, lfTieLineBranch.getQ1().eval() * PerUnit.SB, lfTieLineBranch.getV1() * nominalV1, Math.toDegrees(lfTieLineBranch.getAngle1()), side) + .otherSideP(danglingLine1, false) / PerUnit.SB; + } else if (side == TwoSides.TWO) { + DanglingLine danglingLine = lfTieLineBranch.getHalf2(); + double nominalV2 = danglingLine.getTerminal().getVoltageLevel().getNominalV(); + return new SV(lfTieLineBranch.getP2().eval() * PerUnit.SB, lfTieLineBranch.getQ2().eval() * PerUnit.SB, lfTieLineBranch.getV2() * nominalV2, Math.toDegrees(lfTieLineBranch.getAngle2()), side) + .otherSideP(danglingLine, false) / PerUnit.SB; + } + } + return switch (side) { + case ONE -> branch.getP1().eval(); + case TWO -> branch.getP2().eval(); + }; + } + } +} diff --git a/src/main/java/com/powsybl/openloadflow/network/impl/LfNetworkLoaderImpl.java b/src/main/java/com/powsybl/openloadflow/network/impl/LfNetworkLoaderImpl.java index e704a879cc..6cb3d30502 100644 --- a/src/main/java/com/powsybl/openloadflow/network/impl/LfNetworkLoaderImpl.java +++ b/src/main/java/com/powsybl/openloadflow/network/impl/LfNetworkLoaderImpl.java @@ -55,6 +55,12 @@ private static class LoadingContext { private final Set shuntSet = new LinkedHashSet<>(); private final Set hvdcLineSet = new LinkedHashSet<>(); + + private final Map areaTerminalMap = new HashMap<>(); + + private final Map> areaBusMap = new HashMap<>(); + + private final Map> areaBoundaries = new HashMap<>(); } private final Supplier> postProcessorsSupplier; @@ -316,6 +322,8 @@ private static LfBusImpl createBus(Bus bus, LfNetworkParameters parameters, LfNe List shuntCompensators = new ArrayList<>(); + updateArea(bus, lfBus, parameters, loadingContext); + bus.visitConnectedEquipments(new DefaultTopologyVisitor() { private void visitBranch(Branch branch) { @@ -428,6 +436,7 @@ private static void createBranches(List lfBuses, LfNetwork lfNetwork, LfT LfBus lfBus2 = getLfBus(branch.getTerminal2(), lfNetwork, parameters.isBreakers()); LfBranchImpl lfBranch = LfBranchImpl.create(branch, lfNetwork, lfBus1, lfBus2, topoConfig, parameters); addBranch(lfNetwork, lfBranch, report); + addBranchAreaBoundaries(branch, lfBranch, loadingContext); postProcessors.forEach(pp -> pp.onBranchAdded(branch, lfBranch)); } @@ -439,6 +448,7 @@ private static void createBranches(List lfBuses, LfNetwork lfNetwork, LfT LfBus lfBus2 = getLfBus(tieLine.getDanglingLine2().getTerminal(), lfNetwork, parameters.isBreakers()); LfBranch lfBranch = LfTieLineBranch.create(tieLine, lfNetwork, lfBus1, lfBus2, parameters); addBranch(lfNetwork, lfBranch, report); + addBranchAreaBoundaries(tieLine, lfBranch, loadingContext); postProcessors.forEach(pp -> pp.onBranchAdded(tieLine, lfBranch)); visitedDanglingLinesIds.add(tieLine.getDanglingLine1().getId()); visitedDanglingLinesIds.add(tieLine.getDanglingLine2().getId()); @@ -450,6 +460,7 @@ private static void createBranches(List lfBuses, LfNetwork lfNetwork, LfT LfBus lfBus1 = getLfBus(danglingLine.getTerminal(), lfNetwork, parameters.isBreakers()); LfBranch lfBranch = LfDanglingLineBranch.create(danglingLine, lfNetwork, lfBus1, lfBus2, parameters); addBranch(lfNetwork, lfBranch, report); + addDanglingLineAreaBoundary(danglingLine, lfBranch, loadingContext); postProcessors.forEach(pp -> { pp.onBusAdded(danglingLine, lfBus2); pp.onBranchAdded(danglingLine, lfBranch); @@ -503,6 +514,121 @@ private static void createBranches(List lfBuses, LfNetwork lfNetwork, LfT } } + private static void updateArea(Bus bus, LfBus lfBus, LfNetworkParameters parameters, LoadingContext loadingContext) { + if (parameters.isAreaInterchangeControl()) { + // Consider only the area type that should be used for area interchange control + Optional areaOpt = bus.getVoltageLevel().getArea(parameters.getAreaInterchangeControlAreaType()); + areaOpt.ifPresent(area -> + loadingContext.areaBusMap.computeIfAbsent(area, k -> { + area.getAreaBoundaryStream().forEach(boundary -> { + boundary.getTerminal().ifPresent(t -> loadingContext.areaTerminalMap.put(t, area)); + boundary.getBoundary().ifPresent(b -> loadingContext.areaTerminalMap.put(b.getDanglingLine().getTerminal(), area)); + }); + return new HashSet<>(); + }).add(lfBus) + ); + } + } + + /** + * Add the terminals active power to the calculation of their Area's interchange (load convention) if they are boundaries. + * For simple branches (lines, transformers, switches), the side declared as the boundary must be the one which active power is used. + * For tie lines, the side declared as the boundary must be the one on the side of the concerned area. + */ + private static void addBranchAreaBoundaries(Branch branch, LfBranch lfBranch, LoadingContext loadingContext) { + addAreaBoundary(branch.getTerminal1(), lfBranch, TwoSides.ONE, loadingContext); + addAreaBoundary(branch.getTerminal2(), lfBranch, TwoSides.TWO, loadingContext); + } + + /** + * Adds the active power of the terminal of the dangling line to the calculation of the Area's interchange (load convention) if it is a boundary. + * The equivalent injection of the dangling line lfBranch model is P2; + */ + private static void addDanglingLineAreaBoundary(DanglingLine danglingLine, LfBranch lfDanglingLineBranch, LoadingContext loadingContext) { + addAreaBoundary(danglingLine.getTerminal(), lfDanglingLineBranch, TwoSides.TWO, loadingContext); + } + + private static void addAreaBoundary(Terminal terminal, LfBranch branch, TwoSides side, LoadingContext loadingContext) { + if (loadingContext.areaTerminalMap.containsKey(terminal)) { + Area area = loadingContext.areaTerminalMap.get(terminal); + loadingContext.areaBoundaries.computeIfAbsent(area, k -> new HashSet<>()).add(new LfAreaImpl.BoundaryImpl(branch, side)); + } + } + + private static void createAreas(LfNetwork network, LoadingContext loadingContext, List postProcessors, LfNetworkParameters parameters) { + if (parameters.isAreaInterchangeControl()) { + loadingContext.areaBusMap + .entrySet() + .stream() + .filter(e -> { + if (e.getKey().getAreaBoundaryStream().findAny().isEmpty()) { + Reports.reportAreaNoInterchangeControl(network.getReportNode(), e.getKey().getId(), "Area does not have any area boundary"); + LOGGER.warn("Network {}: Area {} does not have any area boundary. The area will not be considered for area interchange control", network, e.getKey().getId()); + return false; + } + return true; + }) + .filter(e -> { + if (e.getKey().getInterchangeTarget().isEmpty()) { + Reports.reportAreaNoInterchangeControl(network.getReportNode(), e.getKey().getId(), "Area does not have an interchange target"); + LOGGER.warn("Network {}: Area {} does not have an interchange target. The area will not be considered for area interchange control", network, e.getKey().getId()); + return false; + } + return true; + }) + .filter(e -> checkBoundariesComponent(network, e.getKey())) + .forEach(e -> { + Area area = e.getKey(); + Set lfBuses = e.getValue(); + Set boundaries = loadingContext.areaBoundaries.getOrDefault(area, new HashSet<>()); + LfArea lfArea = LfAreaImpl.create(area, lfBuses, boundaries, network, parameters); + network.addArea(lfArea); + postProcessors.forEach(pp -> pp.onAreaAdded(area, lfArea)); + }); + } + } + + private static boolean checkBoundariesComponent(LfNetwork network, Area area) { + final int numCC = network.getNumCC(); + final int numSC = network.getNumSC(); + List boundaryBuses = area.getAreaBoundaryStream() + .map(areaBoundary -> areaBoundary.getTerminal() + .orElseGet(() -> areaBoundary.getBoundary().orElseThrow().getDanglingLine().getTerminal())) + .map(t -> t.getBusView().getBus()) + .filter(Objects::nonNull) + .toList(); + + List connectedComponents = boundaryBuses.stream() + .map(Bus::getConnectedComponent) + .filter(Objects::nonNull) + .map(Component::getNum) + .distinct() + .sorted() + .toList(); + + List synchronousComponents = boundaryBuses.stream() + .map(Bus::getSynchronousComponent) + .filter(Objects::nonNull) + .map(Component::getNum) + .distinct() + .sorted() + .toList(); + + if (connectedComponents.size() > 1 && synchronousComponents.size() > 1) { + if (connectedComponents.get(0) == numCC && synchronousComponents.get(0) == numSC) { + // to avoid logging the same warn multiple times + Reports.reportAreaNoInterchangeControl(network.getReportNode(), area.getId(), "Area does not have all its boundary buses in the same connected component or synchronous component"); + LOGGER.warn("Network {}: Area {} does not have all its boundary buses in the same connected component or synchronous component. The area will not be considered for area interchange control", network, area.getId()); + } + return false; + } else if (!connectedComponents.contains(numCC) || !synchronousComponents.contains(numSC)) { + // only debug level and do report here, this is very common on real networks and would just clutter the logs/reports + LOGGER.debug("Network {}: Area {} has buses in component ({}, {}) but has no boundary in it, this part of the area will not be considered for area interchange control", network, area.getId(), numCC, numSC); + return false; + } + return true; + } + private static void createTransformersVoltageControls(LfNetwork lfNetwork, LfNetworkParameters parameters, LoadingContext loadingContext, LfNetworkLoadingReport report) { // Create discrete voltage controls which link controller -> controlled @@ -788,6 +914,7 @@ private LfNetwork create(int numCC, int numSC, Network network, List buses, List lfBuses = new ArrayList<>(); createBuses(buses, parameters, lfNetwork, lfBuses, topoConfig, loadingContext, report, postProcessors); createBranches(lfBuses, lfNetwork, topoConfig, loadingContext, report, parameters, postProcessors); + createAreas(lfNetwork, loadingContext, postProcessors, parameters); if (parameters.getLoadFlowModel() == LoadFlowModel.AC) { createVoltageControls(lfBuses, parameters); diff --git a/src/main/java/com/powsybl/openloadflow/sensi/AcSensitivityAnalysis.java b/src/main/java/com/powsybl/openloadflow/sensi/AcSensitivityAnalysis.java index d199ac82e6..d9f296955c 100644 --- a/src/main/java/com/powsybl/openloadflow/sensi/AcSensitivityAnalysis.java +++ b/src/main/java/com/powsybl/openloadflow/sensi/AcSensitivityAnalysis.java @@ -223,7 +223,9 @@ public void analyse(Network network, List contingencies, .setMinNominalVoltageTargetVoltageCheck(lfParametersExt.getMinNominalVoltageTargetVoltageCheck()) .setCacheEnabled(false) // force not caching as not supported in sensi analysis .setSimulateAutomationSystems(false) - .setReferenceBusSelector(ReferenceBusSelector.DEFAULT_SELECTOR); // not supported yet + .setReferenceBusSelector(ReferenceBusSelector.DEFAULT_SELECTOR) // not supported yet + .setAreaInterchangeControl(lfParametersExt.isAreaInterchangeControl()) + .setAreaInterchangeControlAreaType(lfParametersExt.getAreaInterchangeControlAreaType()); // create networks including all necessary switches try (LfNetworkList lfNetworks = Networks.load(network, lfNetworkParameters, topoConfig, reportNode)) { diff --git a/src/main/java/com/powsybl/openloadflow/util/Reports.java b/src/main/java/com/powsybl/openloadflow/util/Reports.java index a5f0e86635..b35dcdf485 100644 --- a/src/main/java/com/powsybl/openloadflow/util/Reports.java +++ b/src/main/java/com/powsybl/openloadflow/util/Reports.java @@ -22,6 +22,7 @@ public final class Reports { private static final String NETWORK_NUM_CC = "networkNumCc"; private static final String NETWORK_NUM_SC = "networkNumSc"; private static final String ITERATION = "iteration"; + private static final String ITERATION_COUNT = "iterationCount"; private static final String NETWORK_ID = "networkId"; private static final String IMPACTED_GENERATOR_COUNT = "impactedGeneratorCount"; @@ -31,6 +32,7 @@ public final class Reports { private static final String BUS_ID = "busId"; private static final String CONTROLLER_BUS_ID = "controllerBusId"; private static final String CONTROLLED_BUS_ID = "controlledBusId"; + public static final String MISMATCH = "mismatch"; public record BusReport(String busId, double mismatch, double nominalV, double v, double phi, double p, double q) { } @@ -129,7 +131,7 @@ public static void reportComponentsWithoutGenerators(ReportNode reportNode, int public static void reportMismatchDistributionFailure(ReportNode reportNode, double remainingMismatch) { reportNode.newReportNode() .withMessageTemplate("mismatchDistributionFailure", "Failed to distribute slack bus active power mismatch, ${mismatch} MW remains") - .withTypedValue("mismatch", remainingMismatch, OpenLoadFlowReportConstants.MISMATCH_TYPED_VALUE) + .withTypedValue(MISMATCH, remainingMismatch, OpenLoadFlowReportConstants.MISMATCH_TYPED_VALUE) .withSeverity(TypedValue.ERROR_SEVERITY) .add(); } @@ -138,7 +140,42 @@ public static void reportMismatchDistributionSuccess(ReportNode reportNode, doub reportNode.newReportNode() .withMessageTemplate("mismatchDistributionSuccess", "Slack bus active power (${initialMismatch} MW) distributed in ${iterationCount} distribution iteration(s)") .withTypedValue("initialMismatch", slackBusActivePowerMismatch, OpenLoadFlowReportConstants.MISMATCH_TYPED_VALUE) - .withUntypedValue("iterationCount", iterationCount) + .withUntypedValue(ITERATION_COUNT, iterationCount) + .withSeverity(TypedValue.INFO_SEVERITY) + .add(); + } + + public static void reportAreaNoInterchangeControl(ReportNode reportNode, String area, String reason) { + reportNode.newReportNode() + .withMessageTemplate("areaNoInterchangeControl", "Area ${area} will not be considered in area interchange control, reason: ${reason}") + .withUntypedValue("area", area) + .withUntypedValue("reason", reason) + .withSeverity(TypedValue.WARN_SEVERITY) + .add(); + } + + public static ReportNode reportAreaInterchangeControlDistributionFailure(ReportNode reportNode) { + return reportNode.newReportNode() + .withMessageTemplate("areaInterchangeControlDistributionFailure", "Failed to distribute interchange active power mismatch") + .withSeverity(TypedValue.ERROR_SEVERITY) + .add(); + } + + public static void reportAreaInterchangeControlAreaMismatch(ReportNode reportNode, String area, double mismatch) { + reportNode.newReportNode() + .withMessageTemplate("areaInterchangeControlAreaMismatch", "Remaining mismatch for Area ${area}: ${mismatch} MW") + .withUntypedValue("area", area) + .withTypedValue(MISMATCH, mismatch, OpenLoadFlowReportConstants.MISMATCH_TYPED_VALUE) + .withSeverity(TypedValue.ERROR_SEVERITY) + .add(); + } + + public static void reportAreaInterchangeControlAreaDistributionSuccess(ReportNode reportNode, String area, double mismatch, int iterationCount) { + reportNode.newReportNode() + .withMessageTemplate("areaInterchangeControlAreaDistributionSuccess", "Area ${area} interchange mismatch (${mismatch} MW) distributed in ${iterationCount} distribution iteration(s)") + .withUntypedValue("area", area) + .withTypedValue(MISMATCH, mismatch, OpenLoadFlowReportConstants.MISMATCH_TYPED_VALUE) + .withUntypedValue(ITERATION_COUNT, iterationCount) .withSeverity(TypedValue.INFO_SEVERITY) .add(); } @@ -524,7 +561,7 @@ public static void reportNewtonRaphsonLargestMismatches(ReportNode reportNode, S ReportNode subReportNode = reportNode.newReportNode() .withMessageTemplate("NRMismatch", "Largest ${equationType} mismatch: ${mismatch} ${mismatchUnit}") .withUntypedValue("equationType", acEquationType) - .withTypedValue("mismatch", mismatchUnitConverter * busReport.mismatch(), OpenLoadFlowReportConstants.MISMATCH_TYPED_VALUE) + .withTypedValue(MISMATCH, mismatchUnitConverter * busReport.mismatch(), OpenLoadFlowReportConstants.MISMATCH_TYPED_VALUE) .withUntypedValue("mismatchUnit", mismatchUnit) .add(); diff --git a/src/test/java/com/powsybl/openloadflow/OpenLoadFlowParametersTest.java b/src/test/java/com/powsybl/openloadflow/OpenLoadFlowParametersTest.java index c79f42720d..826ae259ac 100644 --- a/src/test/java/com/powsybl/openloadflow/OpenLoadFlowParametersTest.java +++ b/src/test/java/com/powsybl/openloadflow/OpenLoadFlowParametersTest.java @@ -384,7 +384,7 @@ void testCloneParameters() { @Test void testToString() { OpenLoadFlowParameters parameters = new OpenLoadFlowParameters(); - assertEquals("OpenLoadFlowParameters(slackBusSelectionMode=MOST_MESHED, slackBusesIds=[], slackDistributionFailureBehavior=LEAVE_ON_SLACK_BUS, voltageRemoteControl=true, lowImpedanceBranchMode=REPLACE_BY_ZERO_IMPEDANCE_LINE, loadPowerFactorConstant=false, plausibleActivePowerLimit=5000.0, newtonRaphsonStoppingCriteriaType=UNIFORM_CRITERIA, slackBusPMaxMismatch=1.0, maxActivePowerMismatch=0.01, maxReactivePowerMismatch=0.01, maxVoltageMismatch=1.0E-4, maxAngleMismatch=1.0E-5, maxRatioMismatch=1.0E-5, maxSusceptanceMismatch=1.0E-4, voltagePerReactivePowerControl=false, generatorReactivePowerRemoteControl=false, transformerReactivePowerControl=false, maxNewtonRaphsonIterations=15, maxOuterLoopIterations=20, newtonRaphsonConvEpsPerEq=1.0E-4, voltageInitModeOverride=NONE, transformerVoltageControlMode=WITH_GENERATOR_VOLTAGE_CONTROL, shuntVoltageControlMode=WITH_GENERATOR_VOLTAGE_CONTROL, minPlausibleTargetVoltage=0.8, maxPlausibleTargetVoltage=1.2, minRealisticVoltage=0.5, maxRealisticVoltage=2.0, reactiveRangeCheckMode=MAX, lowImpedanceThreshold=1.0E-8, networkCacheEnabled=false, svcVoltageMonitoring=true, stateVectorScalingMode=NONE, maxSlackBusCount=1, debugDir=null, incrementalTransformerRatioTapControlOuterLoopMaxTapShift=3, secondaryVoltageControl=false, reactiveLimitsMaxPqPvSwitch=3, phaseShifterControlMode=CONTINUOUS_WITH_DISCRETISATION, alwaysUpdateNetwork=false, mostMeshedSlackBusSelectorMaxNominalVoltagePercentile=95.0, reportedFeatures=[], slackBusCountryFilter=[], actionableSwitchesIds=[], actionableTransformersIds=[], asymmetrical=false, minNominalVoltageTargetVoltageCheck=20.0, reactivePowerDispatchMode=Q_EQUAL_PROPORTION, outerLoopNames=null, useActiveLimits=true, disableVoltageControlOfGeneratorsOutsideActivePowerLimits=false, lineSearchStateVectorScalingMaxIteration=10, lineSearchStateVectorScalingStepFold=1.3333333333333333, maxVoltageChangeStateVectorScalingMaxDv=0.1, maxVoltageChangeStateVectorScalingMaxDphi=0.17453292519943295, linePerUnitMode=IMPEDANCE, useLoadModel=false, dcApproximationType=IGNORE_R, simulateAutomationSystems=false, acSolverType=NEWTON_RAPHSON, maxNewtonKrylovIterations=100, newtonKrylovLineSearch=false, referenceBusSelectionMode=FIRST_SLACK, writeReferenceTerminals=true, voltageTargetPriorities=[GENERATOR, TRANSFORMER, SHUNT], transformerVoltageControlUseInitialTapPosition=false, generatorVoltageControlMinNominalVoltage=-1.0, fictitiousGeneratorVoltageControlCheckMode=FORCED)", + assertEquals("OpenLoadFlowParameters(slackBusSelectionMode=MOST_MESHED, slackBusesIds=[], slackDistributionFailureBehavior=LEAVE_ON_SLACK_BUS, voltageRemoteControl=true, lowImpedanceBranchMode=REPLACE_BY_ZERO_IMPEDANCE_LINE, loadPowerFactorConstant=false, plausibleActivePowerLimit=5000.0, newtonRaphsonStoppingCriteriaType=UNIFORM_CRITERIA, slackBusPMaxMismatch=1.0, maxActivePowerMismatch=0.01, maxReactivePowerMismatch=0.01, maxVoltageMismatch=1.0E-4, maxAngleMismatch=1.0E-5, maxRatioMismatch=1.0E-5, maxSusceptanceMismatch=1.0E-4, voltagePerReactivePowerControl=false, generatorReactivePowerRemoteControl=false, transformerReactivePowerControl=false, maxNewtonRaphsonIterations=15, maxOuterLoopIterations=20, newtonRaphsonConvEpsPerEq=1.0E-4, voltageInitModeOverride=NONE, transformerVoltageControlMode=WITH_GENERATOR_VOLTAGE_CONTROL, shuntVoltageControlMode=WITH_GENERATOR_VOLTAGE_CONTROL, minPlausibleTargetVoltage=0.8, maxPlausibleTargetVoltage=1.2, minRealisticVoltage=0.5, maxRealisticVoltage=2.0, reactiveRangeCheckMode=MAX, lowImpedanceThreshold=1.0E-8, networkCacheEnabled=false, svcVoltageMonitoring=true, stateVectorScalingMode=NONE, maxSlackBusCount=1, debugDir=null, incrementalTransformerRatioTapControlOuterLoopMaxTapShift=3, secondaryVoltageControl=false, reactiveLimitsMaxPqPvSwitch=3, phaseShifterControlMode=CONTINUOUS_WITH_DISCRETISATION, alwaysUpdateNetwork=false, mostMeshedSlackBusSelectorMaxNominalVoltagePercentile=95.0, reportedFeatures=[], slackBusCountryFilter=[], actionableSwitchesIds=[], actionableTransformersIds=[], asymmetrical=false, minNominalVoltageTargetVoltageCheck=20.0, reactivePowerDispatchMode=Q_EQUAL_PROPORTION, outerLoopNames=null, useActiveLimits=true, disableVoltageControlOfGeneratorsOutsideActivePowerLimits=false, lineSearchStateVectorScalingMaxIteration=10, lineSearchStateVectorScalingStepFold=1.3333333333333333, maxVoltageChangeStateVectorScalingMaxDv=0.1, maxVoltageChangeStateVectorScalingMaxDphi=0.17453292519943295, linePerUnitMode=IMPEDANCE, useLoadModel=false, dcApproximationType=IGNORE_R, simulateAutomationSystems=false, acSolverType=NEWTON_RAPHSON, maxNewtonKrylovIterations=100, newtonKrylovLineSearch=false, referenceBusSelectionMode=FIRST_SLACK, writeReferenceTerminals=true, voltageTargetPriorities=[GENERATOR, TRANSFORMER, SHUNT], transformerVoltageControlUseInitialTapPosition=false, generatorVoltageControlMinNominalVoltage=-1.0, fictitiousGeneratorVoltageControlCheckMode=FORCED, areaInterchangeControl=false, areaInterchangeControlAreaType=ControlArea, areaInterchangePMaxMismatch=2.0)", parameters.toString()); } @@ -408,10 +408,29 @@ void testExplicitOuterLoopsParameter() { e = assertThrows(PowsyblException.class, () -> OpenLoadFlowParameters.createOuterLoops(parameters, parametersExt)); assertEquals("Unknown outer loop 'Foo'", e.getMessage()); - assertEquals("Ordered explicit list of outer loop names, supported outer loops are IncrementalPhaseControl, DistributedSlack, IncrementalShuntVoltageControl, IncrementalTransformerVoltageControl, VoltageMonitoring, PhaseControl, ReactiveLimits, SecondaryVoltageControl, ShuntVoltageControl, SimpleTransformerVoltageControl, TransformerVoltageControl, AutomationSystem, IncrementalTransformerReactivePowerControl", + assertEquals("Ordered explicit list of outer loop names, supported outer loops are IncrementalPhaseControl, DistributedSlack, IncrementalShuntVoltageControl, IncrementalTransformerVoltageControl, VoltageMonitoring, PhaseControl, ReactiveLimits, SecondaryVoltageControl, ShuntVoltageControl, SimpleTransformerVoltageControl, TransformerVoltageControl, AutomationSystem, IncrementalTransformerReactivePowerControl, AreaInterchangeControl", OpenLoadFlowParameters.SPECIFIC_PARAMETERS.stream().filter(p -> p.getName().equals(OpenLoadFlowParameters.OUTER_LOOP_NAMES_PARAM_NAME)).findFirst().orElseThrow().getDescription()); } + @Test + void testSlackDistributionOuterLoops() { + LoadFlowParameters parameters = new LoadFlowParameters() + .setDistributedSlack(true); + OpenLoadFlowParameters parametersExt = new OpenLoadFlowParameters(); + + assertEquals(List.of("DistributedSlack", "VoltageMonitoring", "ReactiveLimits"), OpenLoadFlowParameters.createOuterLoops(parameters, parametersExt).stream().map(OuterLoop::getType).toList()); + + parametersExt.setAreaInterchangeControl(true); + assertEquals(List.of("AreaInterchangeControl", "VoltageMonitoring", "ReactiveLimits"), OpenLoadFlowParameters.createOuterLoops(parameters, parametersExt).stream().map(OuterLoop::getType).toList()); + + parametersExt.setOuterLoopNames(List.of("DistributedSlack", "AreaInterchangeControl")); + assertEquals(List.of("AreaInterchangeControl"), OpenLoadFlowParameters.createOuterLoops(parameters, parametersExt).stream().map(OuterLoop::getType).toList()); + + parametersExt.setOuterLoopNames(List.of("DistributedSlack")); + assertEquals(List.of("DistributedSlack"), OpenLoadFlowParameters.createOuterLoops(parameters, parametersExt).stream().map(OuterLoop::getType).toList()); + + } + @Test void testVoltageTargetPrioritiesParameter() { LoadFlowParameters parameters = new LoadFlowParameters(); diff --git a/src/test/java/com/powsybl/openloadflow/OpenLoadFlowProviderTest.java b/src/test/java/com/powsybl/openloadflow/OpenLoadFlowProviderTest.java index b5eb3cc21d..0b2c1a31b4 100644 --- a/src/test/java/com/powsybl/openloadflow/OpenLoadFlowProviderTest.java +++ b/src/test/java/com/powsybl/openloadflow/OpenLoadFlowProviderTest.java @@ -51,7 +51,7 @@ void test() { void testDcParameters() { Network network = Mockito.mock(Network.class); DcLoadFlowParameters dcParameters = OpenLoadFlowParameters.createDcParameters(network, new LoadFlowParameters().setReadSlackBus(true), new OpenLoadFlowParameters(), new DenseMatrixFactory(), new EvenShiloachGraphDecrementalConnectivityFactory<>(), true); - assertEquals("DcLoadFlowParameters(networkParameters=LfNetworkParameters(slackBusSelector=NetworkSlackBusSelector, connectivityFactory=EvenShiloachGraphDecrementalConnectivityFactory, generatorVoltageRemoteControl=false, minImpedance=false, twtSplitShuntAdmittance=false, breakers=false, plausibleActivePowerLimit=5000.0, computeMainConnectedComponentOnly=true, countriesToBalance=[], distributedOnConformLoad=false, phaseControl=false, transformerVoltageControl=false, voltagePerReactivePowerControl=false, generatorReactivePowerRemoteControl=false, transformerReactivePowerControl=false, loadFlowModel=DC, reactiveLimits=false, hvdcAcEmulation=true, minPlausibleTargetVoltage=0.8, maxPlausibleTargetVoltage=1.2, loaderPostProcessorSelection=[], reactiveRangeCheckMode=MAX, lowImpedanceThreshold=1.0E-8, svcVoltageMonitoring=false, maxSlackBusCount=1, debugDir=null, secondaryVoltageControl=false, cacheEnabled=false, asymmetrical=false, minNominalVoltageTargetVoltageCheck=20.0, linePerUnitMode=IMPEDANCE, useLoadModel=false, simulateAutomationSystems=false, referenceBusSelector=ReferenceBusFirstSlackSelector, voltageTargetPriorities=[GENERATOR, TRANSFORMER, SHUNT], fictitiousGeneratorVoltageControlCheckMode=FORCED), equationSystemCreationParameters=DcEquationSystemCreationParameters(updateFlows=true, forcePhaseControlOffAndAddAngle1Var=true, useTransformerRatio=true, dcApproximationType=IGNORE_R), matrixFactory=DenseMatrixFactory, distributedSlack=true, balanceType=PROPORTIONAL_TO_GENERATION_P_MAX, setVToNan=true, maxOuterLoopIterations=20)", + assertEquals("DcLoadFlowParameters(networkParameters=LfNetworkParameters(slackBusSelector=NetworkSlackBusSelector, connectivityFactory=EvenShiloachGraphDecrementalConnectivityFactory, generatorVoltageRemoteControl=false, minImpedance=false, twtSplitShuntAdmittance=false, breakers=false, plausibleActivePowerLimit=5000.0, computeMainConnectedComponentOnly=true, countriesToBalance=[], distributedOnConformLoad=false, phaseControl=false, transformerVoltageControl=false, voltagePerReactivePowerControl=false, generatorReactivePowerRemoteControl=false, transformerReactivePowerControl=false, loadFlowModel=DC, reactiveLimits=false, hvdcAcEmulation=true, minPlausibleTargetVoltage=0.8, maxPlausibleTargetVoltage=1.2, loaderPostProcessorSelection=[], reactiveRangeCheckMode=MAX, lowImpedanceThreshold=1.0E-8, svcVoltageMonitoring=false, maxSlackBusCount=1, debugDir=null, secondaryVoltageControl=false, cacheEnabled=false, asymmetrical=false, minNominalVoltageTargetVoltageCheck=20.0, linePerUnitMode=IMPEDANCE, useLoadModel=false, simulateAutomationSystems=false, referenceBusSelector=ReferenceBusFirstSlackSelector, voltageTargetPriorities=[GENERATOR, TRANSFORMER, SHUNT], fictitiousGeneratorVoltageControlCheckMode=FORCED, areaInterchangeControl=false, areaInterchangeControlAreaType=ControlArea), equationSystemCreationParameters=DcEquationSystemCreationParameters(updateFlows=true, forcePhaseControlOffAndAddAngle1Var=true, useTransformerRatio=true, dcApproximationType=IGNORE_R), matrixFactory=DenseMatrixFactory, distributedSlack=true, balanceType=PROPORTIONAL_TO_GENERATION_P_MAX, setVToNan=true, maxOuterLoopIterations=20)", dcParameters.toString()); } @@ -59,7 +59,7 @@ void testDcParameters() { void testAcParameters() { Network network = Mockito.mock(Network.class); AcLoadFlowParameters acParameters = OpenLoadFlowParameters.createAcParameters(network, new LoadFlowParameters().setReadSlackBus(true), new OpenLoadFlowParameters(), new DenseMatrixFactory(), new EvenShiloachGraphDecrementalConnectivityFactory<>(), false, false); - assertEquals("AcLoadFlowParameters(networkParameters=LfNetworkParameters(slackBusSelector=NetworkSlackBusSelector, connectivityFactory=EvenShiloachGraphDecrementalConnectivityFactory, generatorVoltageRemoteControl=true, minImpedance=false, twtSplitShuntAdmittance=false, breakers=false, plausibleActivePowerLimit=5000.0, computeMainConnectedComponentOnly=true, countriesToBalance=[], distributedOnConformLoad=false, phaseControl=false, transformerVoltageControl=false, voltagePerReactivePowerControl=false, generatorReactivePowerRemoteControl=false, transformerReactivePowerControl=false, loadFlowModel=AC, reactiveLimits=true, hvdcAcEmulation=true, minPlausibleTargetVoltage=0.8, maxPlausibleTargetVoltage=1.2, loaderPostProcessorSelection=[], reactiveRangeCheckMode=MAX, lowImpedanceThreshold=1.0E-8, svcVoltageMonitoring=true, maxSlackBusCount=1, debugDir=null, secondaryVoltageControl=false, cacheEnabled=false, asymmetrical=false, minNominalVoltageTargetVoltageCheck=20.0, linePerUnitMode=IMPEDANCE, useLoadModel=false, simulateAutomationSystems=false, referenceBusSelector=ReferenceBusFirstSlackSelector, voltageTargetPriorities=[GENERATOR, TRANSFORMER, SHUNT], fictitiousGeneratorVoltageControlCheckMode=FORCED), equationSystemCreationParameters=AcEquationSystemCreationParameters(forceA1Var=false), newtonRaphsonParameters=NewtonRaphsonParameters(maxIterations=15, minRealisticVoltage=0.5, maxRealisticVoltage=2.0, stoppingCriteria=DefaultNewtonRaphsonStoppingCriteria, stateVectorScalingMode=NONE, alwaysUpdateNetwork=false, lineSearchStateVectorScalingMaxIteration=10, lineSearchStateVectorScalingStepFold=1.3333333333333333, maxVoltageChangeStateVectorScalingMaxDv=0.1, maxVoltageChangeStateVectorScalingMaxDphi=0.17453292519943295), newtonKrylovParameters=NewtonKrylovParameters(maxIterations=100, lineSearch=false), outerLoops=[DistributedSlackOuterLoop, MonitoringVoltageOuterLoop, ReactiveLimitsOuterLoop], maxOuterLoopIterations=20, matrixFactory=DenseMatrixFactory, voltageInitializer=UniformValueVoltageInitializer, asymmetrical=false, slackDistributionFailureBehavior=LEAVE_ON_SLACK_BUS, solverFactory=NewtonRaphsonFactory, detailedReport=false)", + assertEquals("AcLoadFlowParameters(networkParameters=LfNetworkParameters(slackBusSelector=NetworkSlackBusSelector, connectivityFactory=EvenShiloachGraphDecrementalConnectivityFactory, generatorVoltageRemoteControl=true, minImpedance=false, twtSplitShuntAdmittance=false, breakers=false, plausibleActivePowerLimit=5000.0, computeMainConnectedComponentOnly=true, countriesToBalance=[], distributedOnConformLoad=false, phaseControl=false, transformerVoltageControl=false, voltagePerReactivePowerControl=false, generatorReactivePowerRemoteControl=false, transformerReactivePowerControl=false, loadFlowModel=AC, reactiveLimits=true, hvdcAcEmulation=true, minPlausibleTargetVoltage=0.8, maxPlausibleTargetVoltage=1.2, loaderPostProcessorSelection=[], reactiveRangeCheckMode=MAX, lowImpedanceThreshold=1.0E-8, svcVoltageMonitoring=true, maxSlackBusCount=1, debugDir=null, secondaryVoltageControl=false, cacheEnabled=false, asymmetrical=false, minNominalVoltageTargetVoltageCheck=20.0, linePerUnitMode=IMPEDANCE, useLoadModel=false, simulateAutomationSystems=false, referenceBusSelector=ReferenceBusFirstSlackSelector, voltageTargetPriorities=[GENERATOR, TRANSFORMER, SHUNT], fictitiousGeneratorVoltageControlCheckMode=FORCED, areaInterchangeControl=false, areaInterchangeControlAreaType=ControlArea), equationSystemCreationParameters=AcEquationSystemCreationParameters(forceA1Var=false), newtonRaphsonParameters=NewtonRaphsonParameters(maxIterations=15, minRealisticVoltage=0.5, maxRealisticVoltage=2.0, stoppingCriteria=DefaultNewtonRaphsonStoppingCriteria, stateVectorScalingMode=NONE, alwaysUpdateNetwork=false, lineSearchStateVectorScalingMaxIteration=10, lineSearchStateVectorScalingStepFold=1.3333333333333333, maxVoltageChangeStateVectorScalingMaxDv=0.1, maxVoltageChangeStateVectorScalingMaxDphi=0.17453292519943295), newtonKrylovParameters=NewtonKrylovParameters(maxIterations=100, lineSearch=false), outerLoops=[DistributedSlackOuterLoop, MonitoringVoltageOuterLoop, ReactiveLimitsOuterLoop], maxOuterLoopIterations=20, matrixFactory=DenseMatrixFactory, voltageInitializer=UniformValueVoltageInitializer, asymmetrical=false, slackDistributionFailureBehavior=LEAVE_ON_SLACK_BUS, solverFactory=NewtonRaphsonFactory, detailedReport=false)", acParameters.toString()); } @@ -87,7 +87,7 @@ void testGetExtendedVoltageInitializer() { @Test void specificParametersTest() { OpenLoadFlowProvider provider = new OpenLoadFlowProvider(); - assertEquals(68, provider.getSpecificParameters().size()); + assertEquals(71, provider.getSpecificParameters().size()); LoadFlowParameters parameters = new LoadFlowParameters(); provider.loadSpecificParameters(Collections.emptyMap()) @@ -110,7 +110,7 @@ void testCreateMapFromSpecificParameters() { OpenLoadFlowParameters parametersExt = new OpenLoadFlowParameters(); OpenLoadFlowProvider provider = new OpenLoadFlowProvider(); Map map = provider.createMapFromSpecificParameters(parametersExt); - assertEquals(68, map.size()); + assertEquals(71, map.size()); assertEquals(provider.getSpecificParameters().size(), map.size()); } diff --git a/src/test/java/com/powsybl/openloadflow/ac/AcLoadFlowBoundaryTest.java b/src/test/java/com/powsybl/openloadflow/ac/AcLoadFlowBoundaryTest.java index 9c1cd21f0a..6dac29b671 100644 --- a/src/test/java/com/powsybl/openloadflow/ac/AcLoadFlowBoundaryTest.java +++ b/src/test/java/com/powsybl/openloadflow/ac/AcLoadFlowBoundaryTest.java @@ -103,10 +103,22 @@ void testWithVoltageRegulationOn() { } @Test - void testWithXnode() { - network = BoundaryFactory.createWithXnode(); + void testWithXnodeDistributedSlack() { parameters.setUseReactiveLimits(true); parameters.setDistributedSlack(true); + testWithXnode(); + } + + @Test + void testWithXnodeAreaInterchangeControl() { + parameters.setUseReactiveLimits(true); + parametersExt.setAreaInterchangeControl(true); + testWithXnode(); + } + + void testWithXnode() { + network = BoundaryFactory.createWithXnode(); + LoadFlowResult result = loadFlowRunner.run(network, parameters); assertTrue(result.isFullyConverged()); @@ -117,10 +129,21 @@ void testWithXnode() { } @Test - void testWithTieLine() { - network = BoundaryFactory.createWithTieLine(); + void testWithTieLineDistributedSlack() { parameters.setUseReactiveLimits(true); parameters.setDistributedSlack(true); + testWithTieLine(); + } + + @Test + void testWithTieLineAreaInterchangeControl() { + parameters.setUseReactiveLimits(true); + parametersExt.setAreaInterchangeControl(true); + testWithTieLine(); + } + + void testWithTieLine() { + network = BoundaryFactory.createWithTieLine(); LoadFlowResult result = loadFlowRunner.run(network, parameters); assertTrue(result.isFullyConverged()); diff --git a/src/test/java/com/powsybl/openloadflow/ac/AcLoadFlowReportTest.java b/src/test/java/com/powsybl/openloadflow/ac/AcLoadFlowReportTest.java index aa8b02e7a8..c887fb15b8 100644 --- a/src/test/java/com/powsybl/openloadflow/ac/AcLoadFlowReportTest.java +++ b/src/test/java/com/powsybl/openloadflow/ac/AcLoadFlowReportTest.java @@ -238,4 +238,22 @@ void testTransformerControlAlreadyExistsWithDifferentTargetV() throws IOExceptio assertEquals(LoadFlowResult.ComponentResult.Status.CONVERGED, result.getComponentResults().get(0).getStatus()); LoadFlowAssert.assertReportEquals("/transformerControlAlreadyExistsWithDifferentTargetVReport.txt", reportNode); } + + @Test + void areaInterchangeControl() throws IOException { + Network network = MultiAreaNetworkFactory.createTwoAreasWithXNode(); + ReportNode reportNode = ReportNode.newRootReportNode() + .withMessageTemplate("testReport", "Test Report") + .build(); + var lfParameters = new LoadFlowParameters(); + OpenLoadFlowParameters.create(lfParameters) + .setAreaInterchangeControl(true); + + LoadFlowProvider provider = new OpenLoadFlowProvider(new DenseMatrixFactory(), new NaiveGraphConnectivityFactory<>(LfBus::getNum)); + LoadFlow.Runner runner = new LoadFlow.Runner(provider); + LoadFlowResult result = runner.run(network, network.getVariantManager().getWorkingVariantId(), LocalComputationManager.getDefault(), lfParameters, reportNode); + + assertEquals(LoadFlowResult.ComponentResult.Status.CONVERGED, result.getComponentResults().get(0).getStatus()); + LoadFlowAssert.assertReportEquals("/areaInterchangeControlOuterloop.txt", reportNode); + } } diff --git a/src/test/java/com/powsybl/openloadflow/ac/AreaInterchangeControlTest.java b/src/test/java/com/powsybl/openloadflow/ac/AreaInterchangeControlTest.java new file mode 100644 index 0000000000..98350ac0e9 --- /dev/null +++ b/src/test/java/com/powsybl/openloadflow/ac/AreaInterchangeControlTest.java @@ -0,0 +1,229 @@ +/** + * 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.ac; + +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.math.matrix.DenseMatrixFactory; +import com.powsybl.openloadflow.OpenLoadFlowParameters; +import com.powsybl.openloadflow.OpenLoadFlowProvider; +import com.powsybl.openloadflow.network.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.concurrent.CompletionException; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * @author Valentin Mouradian {@literal } + */ +class AreaInterchangeControlTest { + + private LoadFlow.Runner loadFlowRunner; + private LoadFlowParameters parameters; + + private OpenLoadFlowParameters parametersExt; + + @BeforeEach + void setUp() { + loadFlowRunner = new LoadFlow.Runner(new OpenLoadFlowProvider(new DenseMatrixFactory())); + parameters = new LoadFlowParameters(); + parametersExt = OpenLoadFlowParameters.create(parameters) + .setAreaInterchangeControl(true) + .setSlackBusPMaxMismatch(1e-3) + .setAreaInterchangePMaxMismatch(1e-1); + } + + @Test + void twoAreasWithXnodeTest() { + Network network = MultiAreaNetworkFactory.createTwoAreasWithXNode(); + runLfTwoAreas(network, -40, 40, -30, 2); + } + + @Test + 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, 3); + } + + @Test + void twoAreasWithTieLineTest() { + Network network = MultiAreaNetworkFactory.createTwoAreasWithTieLine(); + runLfTwoAreas(network, -40, 40, -30, 2); + } + + @Test + void twoAreasWithUnconsideredTlTest() { + Network network = MultiAreaNetworkFactory.createTwoAreasWithUnconsideredTieLine(); + int expectedIterationCount = 3; + runLfTwoAreas(network, -40, 40, -35, expectedIterationCount); + } + + @Test + void remainingMismatchLeaveOneSlackBus() { + parametersExt.setSlackDistributionFailureBehavior(OpenLoadFlowParameters.SlackDistributionFailureBehavior.LEAVE_ON_SLACK_BUS); + Network network = MultiAreaNetworkFactory.createOneAreaBase(); + network.getGenerator("g1").setMinP(90); // the generator should go down to 70MW to meet the interchange target + var result = loadFlowRunner.run(network, parameters); + var mainComponentResult = result.getComponentResults().get(0); + + assertEquals(-90, network.getGenerator("g1").getTerminal().getP(), 1e-3); + assertEquals(-10, mainComponentResult.getDistributedActivePower(), 1e-3); + assertEquals(-20, mainComponentResult.getSlackBusResults().get(0).getActivePowerMismatch(), 1e-3); + } + + @Test + void remainingMismatchFail() { + parametersExt.setSlackDistributionFailureBehavior(OpenLoadFlowParameters.SlackDistributionFailureBehavior.FAIL); + Network network = MultiAreaNetworkFactory.createOneAreaBase(); + network.getGenerator("g1").setMinP(90); // the generator should go down to 70MW to meet the interchange target + var result = loadFlowRunner.run(network, parameters); + var mainComponentResult = result.getComponentResults().get(0); + + assertEquals(Double.NaN, network.getGenerator("g1").getTerminal().getP(), 1e-3); + assertEquals(0, mainComponentResult.getDistributedActivePower(), 1e-3); + assertEquals(-30, mainComponentResult.getSlackBusResults().get(0).getActivePowerMismatch(), 1e-3); + } + + @Test + void remainingMismatchDistributeOnReferenceGenerator() { + parametersExt.setSlackDistributionFailureBehavior(OpenLoadFlowParameters.SlackDistributionFailureBehavior.DISTRIBUTE_ON_REFERENCE_GENERATOR); + Network network = MultiAreaNetworkFactory.createOneAreaBase(); + network.getGenerator("g1").setMinP(90); // the generator should go down to 70MW to meet the interchange target + var result = loadFlowRunner.run(network, parameters); + var mainComponentResult = result.getComponentResults().get(0); + + // falls back to FAIL + assertEquals(Double.NaN, network.getGenerator("g1").getTerminal().getP(), 1e-3); + assertEquals(0, mainComponentResult.getDistributedActivePower(), 1e-3); + assertEquals(-30, mainComponentResult.getSlackBusResults().get(0).getActivePowerMismatch(), 1e-3); + } + + @Test + void remainingMismatchThrow() { + parametersExt.setSlackDistributionFailureBehavior(OpenLoadFlowParameters.SlackDistributionFailureBehavior.THROW); + Network network = MultiAreaNetworkFactory.createOneAreaBase(); + network.getGenerator("g1").setMinP(90); // the generator should go down to 70MW to meet the interchange target + CompletionException thrown = assertThrows(CompletionException.class, () -> loadFlowRunner.run(network, parameters)); + assertEquals("Failed to distribute interchange active power mismatch", thrown.getCause().getMessage()); + } + + @Test + void slackBusOnBoundaryBus() { + // The slack bus is on a boundary bus, and the flow though this boundary bus is considered in the area interchange + Network network = MultiAreaNetworkFactory.createTwoAreasWithTwoXNodes(); + parametersExt.setSlackBusSelectionMode(SlackBusSelectionMode.NAME) + .setSlackBusId("bx1_vl_0"); + var result = runLfTwoAreas(network, -15, 15, -30, 6); + List slackBusResults = result.getComponentResults().get(0).getSlackBusResults(); + assertEquals(1, slackBusResults.size()); + assertEquals("bx1_vl_0", slackBusResults.get(0).getId()); + } + + @Test + void slackBusOnIgnoredBoundaryBus() { + // The slack bus is on a boundary bus, but the flow on this boundary bus is not considered in the area interchange + Network network = MultiAreaNetworkFactory.createTwoAreasWithTwoXNodes(); + parametersExt.setSlackBusSelectionMode(SlackBusSelectionMode.NAME) + .setSlackBusId("bx2_vl_0"); + var result = runLfTwoAreas(network, -15, 15, -30, 6); + List slackBusResults = result.getComponentResults().get(0).getSlackBusResults(); + assertEquals(1, slackBusResults.size()); + assertEquals("bx2_vl_0", slackBusResults.get(0).getId()); + } + + @Test + void networkWithoutAreas() { + Network network = FourBusNetworkFactory.createBaseNetwork(); + parameters.setDistributedSlack(false); + parametersExt.setAreaInterchangeControl(true); + var result = loadFlowRunner.run(network, parameters); + var componentResult = result.getComponentResults().get(0); + assertEquals(1.998, componentResult.getDistributedActivePower(), 1e-3); + assertEquals(0, componentResult.getSlackBusResults().get(0).getActivePowerMismatch(), 1e-3); + } + + @Test + void halfNetworkWithoutArea() { + Network network = MultiAreaNetworkFactory.createTwoAreasWithTieLine(); + network.getArea("a2").remove(); + + Area area1 = network.getArea("a1"); + var result = loadFlowRunner.run(network, parameters); + + var componentResult = result.getComponentResults().get(0); + assertEquals(area1.getInterchangeTarget().orElseThrow(), area1.getInterchange(), 1e-3); + assertEquals(-30, componentResult.getDistributedActivePower(), 1e-3); + assertEquals(3, componentResult.getIterationCount()); + assertEquals(0, componentResult.getSlackBusResults().get(0).getActivePowerMismatch(), 1e-3); + } + + @Test + void areaFragmentedBuses() { + // Network has an area that has buses in two different components but all boundaries are in the same component + // This Area is considered by area interchange control only in the component where all boundaries are + Network network = MultiAreaNetworkFactory.createAreaTwoComponents(); + + Area area1 = network.getArea("a1"); + Area area2 = network.getArea("a2"); + + parameters.setConnectedComponentMode(LoadFlowParameters.ConnectedComponentMode.ALL); + var result = loadFlowRunner.run(network, parameters); + + var componentResult = result.getComponentResults().get(0); + assertEquals(area1.getInterchangeTarget().orElseThrow(), area1.getInterchange(), 1e-3); + assertEquals(area2.getInterchangeTarget().orElseThrow(), area2.getInterchange(), 1e-3); + assertEquals(-30, componentResult.getDistributedActivePower(), 1e-3); + assertEquals(0, componentResult.getSlackBusResults().get(0).getActivePowerMismatch(), 1e-3); + } + + @Test + void areaFragmentedBoundaries() { + // Network has an area that has buses and boundaries in two different components, this area is ignored by area interchange control + Network network = MultiAreaNetworkFactory.createAreaTwoComponentsWithBoundaries(); + Area area1 = network.getArea("a1"); + Area area2 = network.getArea("a2"); + + parameters.setConnectedComponentMode(LoadFlowParameters.ConnectedComponentMode.ALL); + var result = loadFlowRunner.run(network, parameters); + + var componentResult = result.getComponentResults().get(0); + assertEquals(area1.getInterchangeTarget().orElseThrow(), area1.getInterchange(), 1e-3); + assertEquals(51.1, area2.getInterchange(), 1e-3); // has been ignored by area interchange control because all boundaries are not in the same component + assertEquals(-30, componentResult.getDistributedActivePower(), 1e-3); + assertEquals(3, componentResult.getIterationCount()); + assertEquals(0, componentResult.getSlackBusResults().get(0).getActivePowerMismatch(), 1e-3); + } + + private LoadFlowResult runLfTwoAreas(Network network, double interchangeTarget1, double interchangeTarget2, double expectedDistributedP, int expectedIterationCount) { + Area area1 = network.getArea("a1"); + Area area2 = network.getArea("a2"); + area1.setInterchangeTarget(interchangeTarget1); + area2.setInterchangeTarget(interchangeTarget2); + + var result = loadFlowRunner.run(network, parameters); + assertTrue(result.isFullyConverged()); + + var componentResult = result.getComponentResults().get(0); + assertEquals(interchangeTarget1, area1.getInterchange(), parametersExt.getAreaInterchangePMaxMismatch()); + assertEquals(interchangeTarget2, area2.getInterchange(), parametersExt.getAreaInterchangePMaxMismatch()); + assertEquals(expectedDistributedP, componentResult.getDistributedActivePower(), 1e-3); + assertEquals(expectedIterationCount, componentResult.getIterationCount()); + assertEquals(0, componentResult.getSlackBusResults().get(0).getActivePowerMismatch(), parametersExt.getSlackBusPMaxMismatch()); + return result; + } + +} + diff --git a/src/test/java/com/powsybl/openloadflow/network/MultiAreaNetworkFactory.java b/src/test/java/com/powsybl/openloadflow/network/MultiAreaNetworkFactory.java new file mode 100644 index 0000000000..53da7a41f0 --- /dev/null +++ b/src/test/java/com/powsybl/openloadflow/network/MultiAreaNetworkFactory.java @@ -0,0 +1,455 @@ +/** + * 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.network; + +import com.powsybl.iidm.network.*; + +/** + * @author Valentin Mouradian {@literal } + */ +public class MultiAreaNetworkFactory extends AbstractLoadFlowNetworkFactory { + + /** + * g1 100 MW + * | + * b1 ---(l12)--- b2 + * | + * load1 60MW + */ + public static Network create() { + Network network = Network.create("areas", "test"); + Substation s1 = network.newSubstation() + .setId("S1") + .add(); + Substation s2 = network.newSubstation() + .setId("S2") + .add(); + VoltageLevel vl1 = s1.newVoltageLevel() + .setId("vl1") + .setNominalV(400) + .setTopologyKind(TopologyKind.BUS_BREAKER) + .add(); + vl1.getBusBreakerView().newBus() + .setId("b1") + .add(); + vl1.newGenerator() + .setId("g1") + .setConnectableBus("b1") + .setBus("b1") + .setTargetP(100) + .setTargetV(400) + .setMinP(0) + .setMaxP(150) + .setVoltageRegulatorOn(true) + .add(); + vl1.newLoad() + .setId("load1") + .setBus("b1") + .setP0(60.0) + .setQ0(10.0) + .add(); + VoltageLevel vl2 = s2.newVoltageLevel() + .setId("vl2") + .setNominalV(400) + .setTopologyKind(TopologyKind.BUS_BREAKER) + .add(); + vl2.getBusBreakerView().newBus() + .setId("b2") + .add(); + network.newLine() + .setId("l12") + .setBus1("b1") + .setBus2("b2") + .setR(0) + .setX(1) + .add(); + return network; + } + + /** + * g1 100 MW + * | + * b1 ---(l12)--- b2 ---(l23)--- b3 + * | | + * load1 60MW load3 10MW + * <----------------------> + * Area 1 + * + */ + public static Network createOneAreaBase() { + Network network = create(); + Substation s3 = network.newSubstation() + .setId("S3") + .add(); + VoltageLevel vl3 = s3.newVoltageLevel() + .setId("vl3") + .setNominalV(400) + .setTopologyKind(TopologyKind.BUS_BREAKER) + .add(); + vl3.getBusBreakerView().newBus() + .setId("b3") + .add(); + vl3.newLoad() + .setId("load3") + .setBus("b3") + .setP0(10.0) + .setQ0(5.0) + .add(); + network.newLine() + .setId("l23") + .setBus1("b2") + .setBus2("b3") + .setR(0) + .setX(1) + .add(); + network.newArea() + .setId("a1") + .setName("Area 1") + .setAreaType("ControlArea") + .setInterchangeTarget(-10) + .addVoltageLevel(network.getVoltageLevel("vl1")) + .addVoltageLevel(network.getVoltageLevel("vl2")) + .addAreaBoundary(network.getLine("l23").getTerminal2(), true) + .add(); + return network; + } + + /** + * g1 100 MW gen3 40MW + * | | + * b1 ---(l12)--- b2 b3 + * | | + * load1 60MW load3 50MW + * <--------------------------------> <-------------------> + * Area 1 Area 2 + */ + public static Network createTwoAreasBase() { + Network network = create(); + Substation s3 = network.newSubstation() + .setId("S3") + .add(); + VoltageLevel vl3 = s3.newVoltageLevel() + .setId("vl3") + .setNominalV(400) + .setTopologyKind(TopologyKind.BUS_BREAKER) + .add(); + vl3.getBusBreakerView().newBus() + .setId("b3") + .add(); + vl3.newLoad() + .setId("load3") + .setBus("b3") + .setP0(50.0) + .setQ0(5.0) + .add(); + vl3.newGenerator() + .setId("gen3") + .setBus("b3") + .setTargetP(40) + .setTargetQ(0) + .setTargetV(400) + .setMinP(0) + .setMaxP(150) + .setVoltageRegulatorOn(true) + .add(); + network.newArea() + .setId("a1") + .setName("Area 1") + .setAreaType("ControlArea") + .setInterchangeTarget(-50) + .addVoltageLevel(network.getVoltageLevel("vl1")) + .addVoltageLevel(network.getVoltageLevel("vl2")) + .add(); + network.newArea() + .setId("a2") + .setName("Area 2") + .setAreaType("ControlArea") + .setInterchangeTarget(50) + .addVoltageLevel(network.getVoltageLevel("vl3")) + .add(); + return network; + } + + /** + * g1 100 MW gen3 40MW + * | | + * b1 ---(l12)--- b2 ---(l23_A1)--- bx1 ---(l23_A2)--- b3 + * | | + * load1 60MW load3 50MW + * <--------------------------------> <-------------------> + * Area 1 Area 2 + */ + public static Network createTwoAreasWithXNode() { + Network network = createTwoAreasBase(); + Bus bx1 = createBus(network, "bx1", 400); + createLine(network, network.getBusBreakerView().getBus("b2"), bx1, "l23_A1", 1); + createLine(network, bx1, network.getBusBreakerView().getBus("b3"), "l23_A2", 1); + network.getArea("a1") + .newAreaBoundary() + .setTerminal(network.getLine("l23_A1").getTerminal2()) + .setAc(true) + .add(); + network.getArea("a2") + .newAreaBoundary() + .setTerminal(network.getLine("l23_A2").getTerminal1()) + .setAc(true) + .add(); + return network; + } + + /** + * g1 100 MW gen3 40MW + * | | + * b1 ---(l12)--- b2 ---(l23_A1)--- bx1 ---(l23_A2)--- b3 - load3 50MW + * | | | + * load1 60MW | | + * + --(l23_A1_1)--- bx2 ---(l23_A2_1)-- + + * <--------------------------------> <-------------------> + * Area 1 Area 2 + * The second xnode is not considered in Areas' boundaries. + */ + + public static Network createTwoAreasWithTwoXNodes() { + Network network = createTwoAreasWithXNode(); + Bus bx2 = createBus(network, "bx2", 400); + createLine(network, network.getBusBreakerView().getBus("b2"), bx2, "l23_A1_1", 1); + createLine(network, bx2, network.getBusBreakerView().getBus("b3"), "l23_A2_1", 1); + return network; + } + + /** + * g1 100 MW dl1 30MW gen3 40MW + * | | | + * b1 ---(l12)--- b2 ---(l23_A1)--- bx1 ---(l23_A2)--- b3 + * | | + * load1 60MW load3 50MW + * <--------------------------------> <-------------------> + * Area 1 Area 2 + */ + public static Network createTwoAreasWithDanglingLine() { + Network network = createTwoAreasWithXNode(); + VoltageLevel vl2 = network.getVoltageLevel("vl2"); + vl2.newDanglingLine() + .setId("dl1") + .setConnectableBus("b2") + .setBus("b2") + .setR(0) + .setX(1) + .setG(0) + .setB(0) + .setP0(20) + .setQ0(20) + .newGeneration() + .setTargetP(0) + .setTargetQ(0) + .setTargetV(400) + .setVoltageRegulationOn(false) + .add() + .add(); + Area a1 = network.getArea("a1"); + a1.newAreaBoundary() + .setBoundary(network.getDanglingLine("dl1").getBoundary()) + .setAc(true) + .add(); + return network; + } + + /** + * g1 100 MW gen3 40MW + * | | + * b1 ---(l12)--- b2 ---(dlA1)----< >----(dlA2)--- b3 + * | | + * load1 60MW load3 50MW + * <-------------------------------> <---------------------> + * Area 1 Area 2 + */ + public static Network createTwoAreasWithTieLine() { + Network network = createTwoAreasBase(); + VoltageLevel vl2 = network.getVoltageLevel("vl2"); + DanglingLine dl1 = vl2.newDanglingLine() + .setId("dl1") + .setConnectableBus("b2") + .setBus("b2") + .setR(0) + .setX(1) + .setG(0) + .setB(0) + .setP0(0) + .setQ0(0) + .setPairingKey("tlA1A2") + .add(); + VoltageLevel vl3 = network.getVoltageLevel("vl3"); + DanglingLine dl2 = vl3.newDanglingLine() + .setId("dl2") + .setConnectableBus("b3") + .setBus("b3") + .setR(0) + .setX(1) + .setG(0) + .setB(0) + .setP0(0) + .setQ0(0) + .setPairingKey("tlA1A2") + .add(); + network.newTieLine() + .setId("tl1") + .setName("Tie Line A1-A2") + .setDanglingLine1("dl1") + .setDanglingLine2("dl2") + .add(); + network.getArea("a1") + .newAreaBoundary() + .setBoundary(dl1.getBoundary()) + .setAc(true) + .add(); + network.getArea("a2") + .newAreaBoundary() + .setBoundary(dl2.getBoundary()) + .setAc(true) + .add(); + return network; + } + + /** + * g1 100 MW gen3 40MW + * | | + * b1 ---(l12)--- b2 ---(dlA1)----< >----(dlA2)--- b3 + * | | | + * load1 60MW | load3 50MW + * | + * + ---(dlA1_1)---< >---(dlA1_2)--- b4 -- gen4 5 MW + * <-------------------------------> <---------------------> + * Area 1 Area 2 + * The second tie line is not considered in Areas' boundaries. + */ + public static Network createTwoAreasWithUnconsideredTieLine() { + Network network = createTwoAreasWithTieLine(); + VoltageLevel vl2 = network.getVoltageLevel("vl2"); + vl2.newDanglingLine() + .setId("dlA1_1") + .setConnectableBus("b2") + .setBus("b2") + .setR(0) + .setX(1) + .setG(0) + .setB(0) + .setP0(0) + .setQ0(0) + .setPairingKey("tlA1A2_2") + .add(); + Substation s4 = network.newSubstation() + .setId("S4") + .add(); + VoltageLevel vl4 = s4.newVoltageLevel() + .setId("vl4") + .setNominalV(400) + .setTopologyKind(TopologyKind.BUS_BREAKER) + .add(); + vl4.getBusBreakerView().newBus() + .setId("b4") + .add(); + vl4.newGenerator() + .setId("gen4") + .setConnectableBus("b4") + .setBus("b4") + .setTargetP(5) + .setTargetQ(0) + .setTargetV(400) + .setMinP(0) + .setMaxP(30) + .setVoltageRegulatorOn(true) + .add(); + vl4.newDanglingLine() + .setId("dlA1_2") + .setConnectableBus("b4") + .setBus("b4") + .setR(0) + .setX(1) + .setG(0) + .setB(0) + .setP0(0) + .setQ0(0) + .setPairingKey("tlA1A2_2") + .add(); + network.newTieLine() + .setId("tl2") + .setName("Tie Line A1-A2 2") + .setDanglingLine1("dlA1_1") + .setDanglingLine2("dlA1_2") + .add(); + network.getArea("a2") + .addVoltageLevel(vl4); + network.newLine() + .setId("l24") + .setBus1("b2") + .setBus2("b4") + .setR(0) + .setX(1) + .add(); + return network; + } + + /** + * same as createTwoAreasWithTieLine but with a small dummy island and a2 has a boundary in it. + */ + + public static Network createAreaTwoComponents() { + Network network = createTwoAreasWithTieLine(); + // create dummy bus in another island + Bus dummy = createBus(network, "dummy"); + Bus dummy2 = createBus(network, "dummy2"); + createGenerator(dummy, "dummyGen", 1); + createLoad(dummy2, "dummyLoad", 1.1); + createLine(network, dummy, dummy2, "dummyLine", 0); + network.getArea("a2").addVoltageLevel(dummy.getVoltageLevel()).addVoltageLevel(dummy2.getVoltageLevel()); + return network; + } + + public static Network createAreaTwoComponentsWithBoundaries() { + Network network = createAreaTwoComponents(); + Line dummyLine = network.getLine("dummyLine"); + network.getArea("a2").newAreaBoundary() + .setTerminal(dummyLine.getTerminal1()) + .setAc(true) + .add(); + return network; + } + + public static Network createWithAreaWithoutBoundariesOrTarget() { + Network network = createTwoAreasBase(); + // Area a1 has no boundaries + + // Area a2 has boundaries and an interchange target + network.getArea("a2").newAreaBoundary() + .setTerminal(network.getLine("l12").getTerminal2()) + .setAc(true) + .add(); + createBus(network, "b4"); + createLine(network, network.getBusBreakerView().getBus("b2"), network.getBusBreakerView().getBus("b3"), "l23_A2", 1); + createLine(network, network.getBusBreakerView().getBus("b3"), network.getBusBreakerView().getBus("b4"), "l34", 1); + + // Area a3 has boundaries but no target + network.newArea() + .setId("a3") + .setName("Area 3") + .setAreaType("ControlArea") + .addVoltageLevel(network.getVoltageLevel("b4_vl")) + .addAreaBoundary(network.getLine("l34").getTerminal2(), true) + .add(); + + // Area a4 has boundaries and a target but no voltage levels + network.newArea() + .setId("a4") + .setName("Area 4") + .setAreaType("ControlArea") + .addAreaBoundary(network.getLine("l34").getTerminal2(), true) + .add(); + return network; + } + +} diff --git a/src/test/java/com/powsybl/openloadflow/network/impl/LfNetworkLoaderImplTest.java b/src/test/java/com/powsybl/openloadflow/network/impl/LfNetworkLoaderImplTest.java index 847063e7ba..c5e0c6b762 100644 --- a/src/test/java/com/powsybl/openloadflow/network/impl/LfNetworkLoaderImplTest.java +++ b/src/test/java/com/powsybl/openloadflow/network/impl/LfNetworkLoaderImplTest.java @@ -14,6 +14,7 @@ import com.powsybl.iidm.network.test.EurostagTutorialExample1Factory; import com.powsybl.iidm.network.test.ThreeWindingsTransformerNetworkFactory; import com.powsybl.openloadflow.network.*; +import com.powsybl.openloadflow.util.PerUnit; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -114,6 +115,58 @@ void networkWithDanglingLineTest() { assertEquals("VL", lfDanglingLineBus.getVoltageLevelId()); } + @Test + void networkWithControlAreasTest() { + network = EurostagTutorialExample1Factory.createWithTieLinesAndAreas(); + LfNetworkParameters parameters = new LfNetworkParameters(); + + List lfNetworks = Networks.load(network, parameters); + assertEquals(1, lfNetworks.size()); + LfNetwork mainNetwork = lfNetworks.get(0); + assertFalse(mainNetwork.hasArea()); + + parameters.setAreaInterchangeControl(true); + + lfNetworks = Networks.load(network, parameters); + assertEquals(1, lfNetworks.size()); + mainNetwork = lfNetworks.get(0); + LfArea lfArea = mainNetwork.getAreaById("ControlArea_A"); + // The area is not of 'ControlArea' type, so it is not created + assertNull(mainNetwork.getAreaById("Region_AB")); + assertEquals(-602.6 / PerUnit.SB, lfArea.getInterchangeTarget()); + } + + @Test + void networkInvalidAreasTest() { + // The areas have no boundaries, so they are not created + network = MultiAreaNetworkFactory.createWithAreaWithoutBoundariesOrTarget(); + LfNetworkParameters parameters = new LfNetworkParameters(); + parameters.setAreaInterchangeControl(true); + + List lfNetworks = Networks.load(network, parameters); + assertEquals(1, lfNetworks.size()); + assertEquals(4, lfNetworks.get(0).getBuses().size()); + LfNetwork mainNetwork = lfNetworks.get(0); + assertNull(mainNetwork.getAreaById("a1")); // no boundaries + assertNotNull(mainNetwork.getAreaById("a2")); // ok + assertNull(mainNetwork.getAreaById("a3")); // no interchange target + assertNull(mainNetwork.getAreaById("a4")); // no voltage levels + } + + @Test + void networkWithInvalidAreasTest2() { + network = MultiAreaNetworkFactory.createAreaTwoComponentsWithBoundaries(); + LfNetworkParameters parameters = new LfNetworkParameters(); + parameters.setAreaInterchangeControl(true); + + List lfNetworks = Networks.load(network, parameters); + assertEquals(1, lfNetworks.size()); + + LfNetwork mainNetwork = lfNetworks.get(0); + assertNotNull(mainNetwork.getAreaById("a1")); + assertNull(mainNetwork.getAreaById("a2")); // fragmented area (boundaries in different components) + } + @Test void networkWith3wtTest() { network = ThreeWindingsTransformerNetworkFactory.create(); diff --git a/src/test/java/com/powsybl/openloadflow/sa/OpenSecurityAnalysisTest.java b/src/test/java/com/powsybl/openloadflow/sa/OpenSecurityAnalysisTest.java index 39c2d6c876..cc5918ec75 100644 --- a/src/test/java/com/powsybl/openloadflow/sa/OpenSecurityAnalysisTest.java +++ b/src/test/java/com/powsybl/openloadflow/sa/OpenSecurityAnalysisTest.java @@ -2396,11 +2396,24 @@ void testUpdateReactiveKeysAfterGeneratorContingency() { @Test void testWithTieLineContingency() { + SecurityAnalysisParameters securityAnalysisParameters = new SecurityAnalysisParameters(); + securityAnalysisParameters.addExtension(OpenSecurityAnalysisParameters.class, new OpenSecurityAnalysisParameters().setCreateResultExtension(true)); + testWithTieLineContingency(securityAnalysisParameters); + } + + @Test + void testWithTieLineContingencyAreaInterchangeControl() { + SecurityAnalysisParameters securityAnalysisParameters = new SecurityAnalysisParameters(); + securityAnalysisParameters.addExtension(OpenSecurityAnalysisParameters.class, new OpenSecurityAnalysisParameters().setCreateResultExtension(true)); + OpenLoadFlowParameters.create(securityAnalysisParameters.getLoadFlowParameters()) + .setAreaInterchangeControl(true); + testWithTieLineContingency(securityAnalysisParameters); + } + + void testWithTieLineContingency(SecurityAnalysisParameters securityAnalysisParameters) { Network network = BoundaryFactory.createWithTieLine(); List contingencies = List.of(new Contingency("contingency", List.of(new TieLineContingency("t12")))); List monitors = createNetworkMonitors(network); - SecurityAnalysisParameters securityAnalysisParameters = new SecurityAnalysisParameters(); - securityAnalysisParameters.addExtension(OpenSecurityAnalysisParameters.class, new OpenSecurityAnalysisParameters().setCreateResultExtension(true)); SecurityAnalysisResult result = runSecurityAnalysis(network, contingencies, monitors, securityAnalysisParameters); assertEquals(PostContingencyComputationStatus.CONVERGED, result.getPostContingencyResults().get(0).getStatus()); @@ -2488,6 +2501,18 @@ void testWithTieLineContingency4(boolean dcFastMode) { @Test void testAcceptableDurations() { + testAcceptableDurations(new SecurityAnalysisParameters()); + } + + @Test + void testAcceptableDurationsAreaInterchangeControl() { + SecurityAnalysisParameters securityAnalysisParameters = new SecurityAnalysisParameters(); + OpenLoadFlowParameters.create(securityAnalysisParameters.getLoadFlowParameters()) + .setAreaInterchangeControl(true); + testAcceptableDurations(securityAnalysisParameters); + } + + void testAcceptableDurations(SecurityAnalysisParameters securityAnalysisParameters) { Network network = EurostagTutorialExample1Factory.createWithTieLine(); network.getGenerator("GEN").setMaxP(4000).setMinP(-4000); @@ -2519,8 +2544,6 @@ void testAcceptableDurations() { .setValue(1.7976931348623157E308D) .endTemporaryLimit() .add(); - - SecurityAnalysisParameters securityAnalysisParameters = new SecurityAnalysisParameters(); ContingenciesProvider contingencies = n -> ImmutableList.of( new Contingency("contingency1", new BranchContingency("NHV1_NHV2_1")), new Contingency("contingency2", new TieLineContingency("NHV1_NHV2_2")), diff --git a/src/test/java/com/powsybl/openloadflow/sensi/AcSensitivityAnalysisTest.java b/src/test/java/com/powsybl/openloadflow/sensi/AcSensitivityAnalysisTest.java index 90c874a907..f9a6b44945 100644 --- a/src/test/java/com/powsybl/openloadflow/sensi/AcSensitivityAnalysisTest.java +++ b/src/test/java/com/powsybl/openloadflow/sensi/AcSensitivityAnalysisTest.java @@ -1398,12 +1398,24 @@ void testWithTieLines() { SensitivityAnalysisParameters sensiParameters = createParameters(false, "b1_vl_0", true); sensiParameters.getLoadFlowParameters().setBalanceType(LoadFlowParameters.BalanceType.PROPORTIONAL_TO_GENERATION_P_MAX); Network network = BoundaryFactory.createWithTieLine(); - List factors = network.getTieLineStream().map(line -> createBranchFlowPerInjectionIncrease(line.getId(), "g1")).collect(Collectors.toList()); + List factors = network.getTieLineStream().map(line -> createBranchFlowPerInjectionIncrease(line.getId(), "g1")).toList(); SensitivityAnalysisResult result = sensiRunner.run(network, factors, Collections.emptyList(), Collections.emptyList(), sensiParameters); assertEquals(1, result.getValues().size()); assertEquals(35.000, result.getBranchFlow1FunctionReferenceValue("t12"), LoadFlowAssert.DELTA_POWER); } + @Test + void testWithTieLinesAreaInterchangeControl() { + SensitivityAnalysisParameters sensiParameters = createParameters(false, "b1_vl_0", true); + LoadFlowParameters parameters = sensiParameters.getLoadFlowParameters().setBalanceType(LoadFlowParameters.BalanceType.PROPORTIONAL_TO_GENERATION_P_MAX); + OpenLoadFlowParameters.create(parameters).setAreaInterchangeControl(true); + Network network = BoundaryFactory.createWithTieLine(); + List factors = network.getTieLineStream().map(line -> createBranchFlowPerInjectionIncrease(line.getId(), "g1", null, TwoSides.TWO)).toList(); + SensitivityAnalysisResult result = sensiRunner.run(network, factors, Collections.emptyList(), Collections.emptyList(), sensiParameters); + assertEquals(1, result.getValues().size()); + assertEquals(-35.000, result.getBranchFlow2FunctionReferenceValue("t12"), LoadFlowAssert.DELTA_POWER); + } + @Test void testReactivePowerAndCurrentPerTargetVSensi() { Network network = EurostagFactory.fix(EurostagTutorialExample1Factory.create()); diff --git a/src/test/resources/areaInterchangeControlOuterloop.txt b/src/test/resources/areaInterchangeControlOuterloop.txt new file mode 100644 index 0000000000..06ad0c8b7e --- /dev/null +++ b/src/test/resources/areaInterchangeControlOuterloop.txt @@ -0,0 +1,18 @@ ++ Test Report + + Load flow on network 'areas' + + Network CC0 SC0 + + Network info + Network has 4 buses and 3 branches + Network balance: active generation=140.0 MW, active load=110.0 MW, reactive generation=0.0 MVar, reactive load=15.0 MVar + Angle reference bus: bx1_vl_0 + Slack bus: bx1_vl_0 + + Outer loop AreaInterchangeControl + + Outer loop iteration 1 + Area a1 interchange mismatch (10.000000416666666 MW) distributed in 1 distribution iteration(s) + Area a2 interchange mismatch (-40.00000000651042 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) diff --git a/src/test/resources/debug-parameters.json b/src/test/resources/debug-parameters.json index 3dbd07f73c..4c83e814d3 100644 --- a/src/test/resources/debug-parameters.json +++ b/src/test/resources/debug-parameters.json @@ -87,7 +87,10 @@ "voltageTargetPriorities" : [ "GENERATOR", "TRANSFORMER", "SHUNT" ], "transformerVoltageControlUseInitialTapPosition" : false, "generatorVoltageControlMinNominalVoltage" : -1.0, - "fictitiousGeneratorVoltageControlCheckMode" : "FORCED" + "fictitiousGeneratorVoltageControlCheckMode" : "FORCED", + "areaInterchangeControl" : false, + "areaInterchangeControlAreaType" : "ControlArea", + "areaInterchangePMaxMismatch" : 2.0 } } },