GlskPointScalableConverter.java
/*
* Copyright (c) 2020, RTE (http://www.rte-france.com)
* 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/.
*/
package com.powsybl.glsk.api.util.converters;
import com.powsybl.glsk.api.GlskPoint;
import com.powsybl.glsk.api.GlskRegisteredResource;
import com.powsybl.glsk.api.GlskShiftKey;
import com.powsybl.glsk.commons.CountryEICode;
import com.powsybl.glsk.commons.GlskException;
import com.powsybl.iidm.modification.scalable.Scalable;
import com.powsybl.iidm.network.Country;
import com.powsybl.iidm.network.DanglingLine;
import com.powsybl.iidm.network.Generator;
import com.powsybl.iidm.network.Load;
import com.powsybl.iidm.network.Network;
import com.powsybl.iidm.network.Substation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.BiFunction;
import java.util.function.Function;
/**
* Convert a single GlskPoint to Scalable
*
* @author Pengbo Wang {@literal <pengbo.wang@rte-international.com>}
*/
public final class GlskPointScalableConverter {
private static final Logger LOGGER = LoggerFactory.getLogger(GlskPointScalableConverter.class);
private GlskPointScalableConverter() {
throw new AssertionError("Utility class should not be instantiated");
}
/**
* @param network IIDM network
* @param glskPoint GLSK Point
* @return powsybl-core Scalable
*/
public static Scalable convert(Network network, GlskPoint glskPoint) {
Objects.requireNonNull(glskPoint.getGlskShiftKeys());
if (!glskPoint.getGlskShiftKeys().get(0).getBusinessType().equals("B45")) {
return convert(network, glskPoint.getGlskShiftKeys());
} else {
//B45 merit order
return convertMeritOrder(network, glskPoint);
}
}
public static Scalable convert(Network network, List<GlskShiftKey> shiftKeys) {
Objects.requireNonNull(shiftKeys);
List<Scalable> shiftKeyScalables = new ArrayList<>();
List<Double> shiftKeyPercentages = new ArrayList<>();
for (GlskShiftKey glskShiftKey : shiftKeys) {
List<Double> percentages = new ArrayList<>();
List<Scalable> scalables = new ArrayList<>();
if (glskShiftKey.getBusinessType().equals("B42") && glskShiftKey.getRegisteredResourceArrayList().isEmpty()) {
//B42 country
convertCountryProportional(network, glskShiftKey, percentages, scalables);
} else if (glskShiftKey.getBusinessType().equals("B42") && !glskShiftKey.getRegisteredResourceArrayList().isEmpty()) {
//B42 explicit
convertExplicitProportional(network, glskShiftKey, percentages, scalables);
} else if (glskShiftKey.getBusinessType().equals("B43") && !glskShiftKey.getRegisteredResourceArrayList().isEmpty()) {
//B43 participation factor
convertParticipationFactor(network, glskShiftKey, percentages, scalables);
} else if (glskShiftKey.getBusinessType().equals("B44") && !glskShiftKey.getRegisteredResourceArrayList().isEmpty()) {
//B44 remaining capacity
convertRemainingCapacity(network, glskShiftKey, percentages, scalables);
} else {
throw new GlskException("In convert glskShiftKey business type not supported");
}
Double percentageSum = percentages.stream().mapToDouble(Double::doubleValue).sum();
// each scalable needs to have a sum of percentages equal to 100%
List<Double> normalizedPercentages;
if (Math.abs(percentageSum) >= 1e-6) {
Double finalPercentageSum = percentageSum;
normalizedPercentages = percentages.stream().map(p -> p * 100. / finalPercentageSum).toList();
} else {
normalizedPercentages = percentages.stream().map(p -> 100. / percentages.size()).toList();
percentageSum = 0.;
}
shiftKeyPercentages.add(percentageSum);
// the limit of the scalables is the power they can reach, not how much they can be scaled by
Double currentPower = scalables.stream().mapToDouble(scalable -> scalable.getSteadyStatePower(network, 1, Scalable.ScalingConvention.GENERATOR)).sum();
shiftKeyScalables.add(Scalable.proportional(normalizedPercentages, scalables, -Double.MAX_VALUE, currentPower + glskShiftKey.getMaximumShift()));
}
return Scalable.proportional(shiftKeyPercentages, shiftKeyScalables); // iterative must be set in scalingParameters during scale
}
private static void convertRemainingCapacity(Network network, GlskShiftKey glskShiftKey, List<Double> percentages, List<Scalable> scalables) {
LOGGER.debug("GLSK Type B44, not empty registered resources list --> remaining capacity proportional GSK");
// Remaining capacity algorithm is supposed to put all generators at Pmin at the same time when decreasing
// generation, and to put all generators at Pmax at the same time when increasing generation.
// Though the scaling is not symmetrical.
List<GlskRegisteredResource> generatorResources = glskShiftKey.getRegisteredResourceArrayList().stream()
.filter(generatorResource -> NetworkUtil.isCorrect(network.getGenerator(generatorResource.getGeneratorId())))
.toList();
Scalable upScalable = createRemainingCapacityScalable(network, glskShiftKey, generatorResources, GlskPointScalableConverter::getRemainingCapacityUp);
Scalable downScalable = createRemainingCapacityScalable(network, glskShiftKey, generatorResources, GlskPointScalableConverter::getRemainingCapacityDown);
percentages.add(100.0);
scalables.add(Scalable.upDown(upScalable, downScalable));
}
private static Scalable createRemainingCapacityScalable(Network network, GlskShiftKey glskShiftKey, List<GlskRegisteredResource> generatorResources, BiFunction<GlskRegisteredResource, Network, Double> remainingCapacityFunction) {
List<Double> percentages = new ArrayList<>();
List<Scalable> scalables = new ArrayList<>();
double totalFactor = generatorResources.stream().mapToDouble(resource -> remainingCapacityFunction.apply(resource, network)).sum();
generatorResources.forEach(generatorResource -> {
double generatorPercentage = 100.0 * glskShiftKey.getQuantity() * remainingCapacityFunction.apply(generatorResource, network) / totalFactor;
if (!Double.isNaN(generatorPercentage)) {
percentages.add(generatorPercentage);
scalables.add(getGeneratorScalableWithLimits(network, generatorResource));
}
});
return Scalable.proportional(percentages, scalables); // iterative must be set in scalingParameters during scale
}
private static double getRemainingCapacityUp(GlskRegisteredResource resource, Network network) {
Generator generator = network.getGenerator(resource.getGeneratorId());
double maxP = Math.min(resource.getMaximumCapacity().orElse(generator.getMaxP()), generator.getMaxP());
return Math.max(0., maxP - NetworkUtil.pseudoTargetP(generator));
}
private static double getRemainingCapacityDown(GlskRegisteredResource resource, Network network) {
Generator generator = network.getGenerator(resource.getGeneratorId());
double minP = Math.max(resource.getMinimumCapacity().orElse(generator.getMinP()), generator.getMinP());
return Math.max(0, NetworkUtil.pseudoTargetP(generator) - minP);
}
/**
* convert merit order glsk point to scalable
* @param network iidm network
* @param glskPoint glsk point merit order
* @return stack scalable
*/
private static Scalable convertMeritOrder(Network network, GlskPoint glskPoint) {
Objects.requireNonNull(network);
Scalable upScalable = Scalable.stack(glskPoint.getGlskShiftKeys().stream()
.filter(glskShiftKey -> glskShiftKey.getMeritOrderPosition() > 0)
.sorted(Comparator.comparingInt(GlskShiftKey::getMeritOrderPosition))
.map(getGlskShiftKeyScalableFunction(network)).toArray(Scalable[]::new));
Scalable downScalable = Scalable.stack(glskPoint.getGlskShiftKeys().stream()
.filter(glskShiftKey -> glskShiftKey.getMeritOrderPosition() < 0)
.sorted(Comparator.comparingInt(GlskShiftKey::getMeritOrderPosition).reversed())
.map(getGlskShiftKeyScalableFunction(network)).toArray(Scalable[]::new));
return Scalable.upDown(upScalable, downScalable);
}
private static Function<GlskShiftKey, Scalable> getGlskShiftKeyScalableFunction(Network network) {
return glskShiftKey -> {
GlskRegisteredResource resource = Objects.requireNonNull(glskShiftKey.getRegisteredResourceArrayList()).get(0);
if (isGenerator(network, resource)) {
return getGeneratorScalableWithLimits(network, resource);
} else if (isLoad(network, resource)) {
return getLoadScalableWithLimits(network, resource);
} else {
return getDanglingLineScalableWithLimits(network, resource);
}
};
}
/**
* convert country proportional glsk point to scalable
* @param network iidm network
* @param glskShiftKey shift key
* @param percentages list of percentage factor of scalable
* @param scalables list of scalable
*/
private static void convertCountryProportional(Network network, GlskShiftKey glskShiftKey, List<Double> percentages, List<Scalable> scalables) {
Country country = new CountryEICode(glskShiftKey.getSubjectDomainmRID()).getCountry();
if (glskShiftKey.getPsrType().equals("A04")) {
LOGGER.debug("GLSK Type B42, empty registered resources list --> country (proportional) GSK");
List<Generator> generators = network.getGeneratorStream()
.filter(generator -> country.equals(getSubstationNullableCountry(generator.getTerminal().getVoltageLevel().getSubstation())))
.filter(NetworkUtil::isCorrect)
.toList();
//calculate sum P of country's generators
double totalCountryP = generators.stream().mapToDouble(NetworkUtil::pseudoTargetP).sum();
//calculate factor of each generator
generators.forEach(generator -> {
double generatorPercentage = 100.0 * glskShiftKey.getQuantity() * NetworkUtil.pseudoTargetP(generator) / totalCountryP;
percentages.add(generatorPercentage);
scalables.add(Scalable.onGenerator(generator.getId()));
});
} else if (glskShiftKey.getPsrType().equals("A05")) {
LOGGER.debug("GLSK Type B42, empty registered resources list --> country (proportional) LSK");
List<Load> loads = network.getLoadStream()
.filter(load -> country.equals(getSubstationNullableCountry(load.getTerminal().getVoltageLevel().getSubstation())))
.filter(NetworkUtil::isCorrect)
.toList();
//calculate sum P of country's loads
double totalCountryP = loads.stream().mapToDouble(NetworkUtil::pseudoP0).sum();
loads.forEach(load -> {
double loadPercentage = 100.0 * glskShiftKey.getQuantity() * NetworkUtil.pseudoP0(load) / totalCountryP;
percentages.add(loadPercentage);
scalables.add(Scalable.onLoad(load.getId(), -Double.MAX_VALUE, Double.MAX_VALUE));
});
}
}
/**
* convert explicit glsk point to scalable
* @param network iidm network
* @param glskShiftKey shift key
* @param percentages list of percentage factor of scalable
* @param scalables list of scalable
*/
private static void convertExplicitProportional(Network network, GlskShiftKey glskShiftKey, List<Double> percentages, List<Scalable> scalables) {
List<DanglingLine> danglingLines = glskShiftKey.getRegisteredResourceArrayList().stream()
.map(rr -> rr.getDanglingLineId(network))
.filter(Objects::nonNull)
.map(network::getDanglingLine)
.filter(NetworkUtil::isCorrect)
.toList();
double totalP = danglingLines.stream().mapToDouble(NetworkUtil::pseudoP0).sum();
if (glskShiftKey.getPsrType().equals("A04")) {
LOGGER.debug("GLSK Type B42, not empty registered resources list --> (explicit/manual) proportional GSK");
List<Generator> generators = glskShiftKey.getRegisteredResourceArrayList().stream()
.map(GlskRegisteredResource::getGeneratorId)
.map(network::getGenerator)
.filter(NetworkUtil::isCorrect)
.toList();
totalP += generators.stream().mapToDouble(NetworkUtil::pseudoTargetP).sum();
for (Generator generator : generators) {
// Calculate factor of each generator
double factor = glskShiftKey.getQuantity() * NetworkUtil.pseudoTargetP(generator) / totalP;
percentages.add(100.0 * factor);
// In case of global shift key limitation we will limit the generator proportionally to
// its participation in the global proportional scalable
scalables.add(Scalable.onGenerator(generator.getId()));
}
} else if (glskShiftKey.getPsrType().equals("A05")) {
LOGGER.debug("GLSK Type B42, not empty registered resources list --> (explicit/manual) proportional LSK");
List<Load> loads = glskShiftKey.getRegisteredResourceArrayList().stream()
.map(GlskRegisteredResource::getLoadId)
.map(network::getLoad)
.filter(NetworkUtil::isCorrect)
.toList();
totalP += loads.stream().mapToDouble(NetworkUtil::pseudoP0).sum();
for (Load load : loads) {
double loadPercentage = 100.0 * glskShiftKey.getQuantity() * NetworkUtil.pseudoP0(load) / totalP;
// For now glsk shift key maximum shift is not handled for loads by lack of specification
percentages.add(loadPercentage);
scalables.add(Scalable.onLoad(load.getId(), -Double.MAX_VALUE, Double.MAX_VALUE));
}
}
for (DanglingLine danglingLine : danglingLines) {
double danglingLinePercentage = 100.0 * glskShiftKey.getQuantity() * NetworkUtil.pseudoP0(danglingLine) / totalP;
// For now glsk shift key maximum shift is not handled for dangling lines by lack of specification
percentages.add(danglingLinePercentage);
scalables.add(Scalable.onDanglingLine(danglingLine.getId(), -Double.MAX_VALUE, Double.MAX_VALUE));
}
}
/**
* convert participation factor glsk point to scalable
* @param network iidm network
* @param glskShiftKey shift key
* @param percentages list of percentage factor of scalable
* @param scalables list of scalable
*/
private static void convertParticipationFactor(Network network, GlskShiftKey glskShiftKey, List<Double> percentages, List<Scalable> scalables) {
List<GlskRegisteredResource> danglingLineResources = glskShiftKey.getRegisteredResourceArrayList().stream()
.filter(danglingLineResource -> danglingLineResource.getDanglingLineId(network) != null &&
NetworkUtil.isCorrect(network.getDanglingLine(danglingLineResource.getDanglingLineId(network))))
.toList();
double totalFactor = danglingLineResources.stream().mapToDouble(GlskRegisteredResource::getParticipationFactor).sum();
if (glskShiftKey.getPsrType().equals("A04")) {
LOGGER.debug("GLSK Type B43 GSK");
List<GlskRegisteredResource> generatorResources = glskShiftKey.getRegisteredResourceArrayList().stream()
.filter(generatorResource -> NetworkUtil.isCorrect(network.getGenerator(generatorResource.getGeneratorId())))
.toList();
totalFactor += generatorResources.stream().mapToDouble(GlskRegisteredResource::getParticipationFactor).sum();
for (GlskRegisteredResource generatorResource : generatorResources) {
double generatorPercentage = 100.0 * glskShiftKey.getQuantity() * generatorResource.getParticipationFactor() / totalFactor;
percentages.add(generatorPercentage);
scalables.add(getGeneratorScalableWithLimits(network, generatorResource));
}
} else if (glskShiftKey.getPsrType().equals("A05")) {
LOGGER.debug("GLSK Type B43 LSK");
List<GlskRegisteredResource> loadResources = glskShiftKey.getRegisteredResourceArrayList().stream()
.filter(loadResource -> NetworkUtil.isCorrect(network.getLoad(loadResource.getLoadId())))
.toList();
totalFactor += loadResources.stream().mapToDouble(GlskRegisteredResource::getParticipationFactor).sum();
for (GlskRegisteredResource loadResource : loadResources) {
double loadPercentage = 100.0 * glskShiftKey.getQuantity() * loadResource.getParticipationFactor() / totalFactor;
percentages.add(loadPercentage);
scalables.add(getLoadScalableWithLimits(network, loadResource));
}
}
for (GlskRegisteredResource danglingLineResource : danglingLineResources) {
double loadPercentage = 100.0 * glskShiftKey.getQuantity() * danglingLineResource.getParticipationFactor() / totalFactor;
percentages.add(loadPercentage);
scalables.add(getDanglingLineScalableWithLimits(network, danglingLineResource));
}
}
private static Country getSubstationNullableCountry(Optional<Substation> substation) {
return substation.map(Substation::getNullableCountry).orElse(null);
}
private static boolean isGenerator(Network network, GlskRegisteredResource glskRegisteredResource) {
return network.getGenerator(glskRegisteredResource.getGeneratorId()) != null;
}
private static boolean isLoad(Network network, GlskRegisteredResource glskRegisteredResource) {
return network.getLoad(glskRegisteredResource.getLoadId()) != null;
}
private static Scalable getGeneratorScalableWithLimits(Network network, GlskRegisteredResource generatorRegisteredResource) {
String generatorId = generatorRegisteredResource.getGeneratorId();
double incomingMaxP = generatorRegisteredResource.getMaximumCapacity().orElse(Double.MAX_VALUE);
double incomingMinP = generatorRegisteredResource.getMinimumCapacity().orElse(-Double.MAX_VALUE);
// Fixes some inconsistencies between GLSK and network that may raise an exception in
// PowSyBl when actually scaling the network.
// TODO: Solve this issue in PowSyBl framework.
Generator generator = network.getGenerator(generatorId);
if (generator != null) {
double generatorTargetP = generator.getTargetP();
if (!Double.isNaN(incomingMaxP) && incomingMaxP < generatorTargetP) {
LOGGER.warn("Generator '{}' has initial target P that is above GLSK max P. Extending GLSK max P from {} to {}.", generatorId, incomingMaxP, generatorTargetP);
incomingMaxP = generatorTargetP;
}
if (!Double.isNaN(incomingMinP) && incomingMinP > generatorTargetP) {
LOGGER.warn("Generator '{}' has initial target P that is above GLSK min P. Extending GLSK min P from {} to {}.", generatorId, incomingMinP, generatorTargetP);
incomingMinP = generatorTargetP;
}
}
return Scalable.onGenerator(generatorId, incomingMinP, incomingMaxP);
}
private static Scalable getLoadScalableWithLimits(Network network, GlskRegisteredResource loadRegisteredResource) {
String loadId = loadRegisteredResource.getLoadId();
double incomingMaxP = loadRegisteredResource.getMaximumCapacity().orElse(Double.MAX_VALUE);
double incomingMinP = loadRegisteredResource.getMinimumCapacity().orElse(-Double.MAX_VALUE);
// Fixes some inconsistencies between GLSK and network that may raise an exception in
// PowSyBl when actually scaling the network.
// TODO: Solve this issue in PowSyBl framework.
Load load = network.getLoad(loadId);
if (load != null) {
double loadP0 = load.getP0();
if (!Double.isNaN(incomingMaxP) && incomingMaxP < loadP0) {
LOGGER.warn("Load '{}' has initial P0 that is above GLSK max P. Extending GLSK max P from {} to {}.", loadId, incomingMaxP, loadP0);
incomingMaxP = loadP0;
}
if (!Double.isNaN(incomingMinP) && incomingMinP > loadP0) {
LOGGER.warn("Load '{}' has initial P0 that is above GLSK min P. Extending GLSK min P from {} to {}.", loadId, incomingMinP, loadP0);
incomingMinP = loadP0;
}
}
return Scalable.onLoad(loadId, incomingMinP, incomingMaxP);
}
private static Scalable getDanglingLineScalableWithLimits(Network network, GlskRegisteredResource danglingLineRegisteredResource) {
String danglingLineId = danglingLineRegisteredResource.getDanglingLineId(network);
// We have to invert min and max because dangling lines act like loads but are scaled but in generator convention
double incomingMinP = -danglingLineRegisteredResource.getMaximumCapacity().orElse(Double.MAX_VALUE);
double incomingMaxP = -danglingLineRegisteredResource.getMinimumCapacity().orElse(-Double.MAX_VALUE);
// Fixes some inconsistencies between GLSK and network that may raise an exception in
// PowSyBl when actually scaling the network.
// TODO: Solve this issue in PowSyBl framework.
DanglingLine danglingLine = network.getDanglingLine(danglingLineId);
if (danglingLine != null) {
double danglingLineP0 = danglingLine.getP0();
if (!Double.isNaN(incomingMaxP) && incomingMaxP < danglingLineP0) {
LOGGER.warn("Dangling line '{}' has initial P0 that is above GLSK max P. Extending GLSK max P from {} to {}.", danglingLineId, incomingMaxP, danglingLineP0);
incomingMaxP = danglingLineP0;
}
if (!Double.isNaN(incomingMinP) && incomingMinP > danglingLineP0) {
LOGGER.warn("Dangling line '{}' has initial P0 that is above GLSK min P. Extending GLSK min P from {} to {}.", danglingLineId, incomingMinP, danglingLineP0);
incomingMinP = danglingLineP0;
}
}
return Scalable.onDanglingLine(danglingLineId, incomingMinP, incomingMaxP);
}
}