diff --git a/matpower/matpower-converter/src/main/java/com/powsybl/matpower/converter/MatpowerExporter.java b/matpower/matpower-converter/src/main/java/com/powsybl/matpower/converter/MatpowerExporter.java index 6999562b27c..8dec4edeba0 100644 --- a/matpower/matpower-converter/src/main/java/com/powsybl/matpower/converter/MatpowerExporter.java +++ b/matpower/matpower-converter/src/main/java/com/powsybl/matpower/converter/MatpowerExporter.java @@ -117,11 +117,7 @@ private static MBus.Type getType(Bus bus, Context context) { if (context.refBusId.contains(bus.getId()) || hasSlackExtension(bus)) { return MBus.Type.REF; } - for (Generator g : bus.getGenerators()) { - if (g.isVoltageRegulatorOn()) { - return MBus.Type.PV; - } - } + // PV buses will be defined at the end of the export process return MBus.Type.PQ; } @@ -139,6 +135,11 @@ static class Context { final List generatorIdsConvertedToLoad = new ArrayList<>(); final Set synchronousComponentsToBeExported = new HashSet<>(); + final Map> generatorsToBeExported = new HashMap<>(); + + private record GenRc(String id, double targetVpu, double targetP, double minP, double maxP, double targetQ, double minQ, double maxQ, + boolean isValidVoltageRegulation, boolean isRemoteRegulation, double ratedS) { + } public Context(double maxGeneratorActivePowerLimit, double maxGeneratorReactivePowerLimit) { this.maxGeneratorActivePowerLimit = maxGeneratorActivePowerLimit; @@ -735,35 +736,35 @@ private void createBranches(Network network, MatpowerModel model, Context contex createTransformerLegs(network, model, context); } - private void createDanglingLineGenerators(Network network, MatpowerModel model, Context context) { + private void findDanglingLineGenerators(Network network, Context context) { for (DanglingLine dl : network.getDanglingLines(DanglingLineFilter.UNPAIRED)) { Terminal t = dl.getTerminal(); Bus bus = t.getBusView().getBus(); if (isExported(bus, context)) { var g = dl.getGeneration(); if (g != null) { + int busNumber = context.mBusesNumbersByIds.get(dl.getId()); VoltageLevel vl = t.getVoltageLevel(); - MGen mGen = new MGen(); - mGen.setNumber(context.mBusesNumbersByIds.get(dl.getId())); - mGen.setStatus(CONNECTED_STATUS); - mGen.setRealPowerOutput(g.getTargetP()); - mGen.setReactivePowerOutput(g.getTargetQ()); - mGen.setVoltageMagnitudeSetpoint(g.isVoltageRegulationOn() ? g.getTargetV() / vl.getNominalV() : 0); - mGen.setMinimumRealPowerOutput(Math.max(g.getMinP(), -context.maxGeneratorActivePowerLimit)); - mGen.setMaximumRealPowerOutput(Math.min(g.getMaxP(), context.maxGeneratorActivePowerLimit)); - mGen.setMinimumReactivePowerOutput(Math.max(g.getReactiveLimits().getMinQ(g.getTargetP()), -context.maxGeneratorReactivePowerLimit)); - mGen.setMaximumReactivePowerOutput(Math.min(g.getReactiveLimits().getMaxQ(g.getTargetP()), context.maxGeneratorReactivePowerLimit)); - model.addGenerator(mGen); + addMgen(context, busNumber, dl.getId(), + checkAndFixTargetVpu(g.getTargetV() / vl.getNominalV()), + g.getTargetP(), + Math.max(g.getMinP(), -context.maxGeneratorActivePowerLimit), + Math.min(g.getMaxP(), context.maxGeneratorActivePowerLimit), + g.getTargetQ(), + Math.max(g.getReactiveLimits().getMinQ(g.getTargetP()), -context.maxGeneratorReactivePowerLimit), + Math.min(g.getReactiveLimits().getMaxQ(g.getTargetP()), context.maxGeneratorReactivePowerLimit), + g.isVoltageRegulationOn(), false, Double.NaN); } } } } - private void createGenerators(Network network, MatpowerModel model, Context context) { + private void findGenerators(Network network, Context context) { for (Generator g : network.getGenerators()) { Terminal t = g.getTerminal(); Bus bus = t.getBusView().getBus(); if (isExported(bus, context)) { + int busNumber = context.mBusesNumbersByIds.get(bus.getId()); String id = g.getId(); double targetP = g.getTargetP(); double targetQ = g.getTargetQ(); @@ -773,10 +774,10 @@ private void createGenerators(Network network, MatpowerModel model, Context cont double maxQ = g.getReactiveLimits().getMaxQ(g.getTargetP()); double minQ = g.getReactiveLimits().getMinQ(g.getTargetP()); Bus regulatedBus = g.getRegulatingTerminal().getBusView().getBus(); - boolean voltageRegulation = g.isVoltageRegulatorOn(); + boolean isValidVoltageRegulation = isValidVoltageRegulation(g.isVoltageRegulatorOn(), regulatedBus); + boolean isRemoteRegulation = isRemoteRegulation(bus, regulatedBus); double ratedS = g.getRatedS(); - addMgen(model, context, bus, id, targetVpu, targetP, minP, maxP, targetQ, Math.min(minQ, maxQ), Math.max(minQ, maxQ), regulatedBus, - voltageRegulation, ratedS); + addMgen(context, busNumber, id, targetVpu, targetP, minP, maxP, targetQ, Math.min(minQ, maxQ), Math.max(minQ, maxQ), isValidVoltageRegulation, isRemoteRegulation, ratedS); } } } @@ -786,11 +787,12 @@ private static double findTargetVpu(Generator generator) { return generator.getTargetV() / generator.getRegulatingTerminal().getVoltageLevel().getNominalV(); } - private void createStaticVarCompensators(Network network, MatpowerModel model, Context context) { + private void findStaticVarCompensatorGenerators(Network network, Context context) { for (StaticVarCompensator svc : network.getStaticVarCompensators()) { Terminal t = svc.getTerminal(); Bus bus = t.getBusView().getBus(); if (isExported(bus, context)) { + int busNumber = context.mBusesNumbersByIds.get(bus.getId()); String id = svc.getId(); double targetQ; if (StaticVarCompensator.RegulationMode.REACTIVE_POWER.equals(svc.getRegulationMode())) { @@ -803,9 +805,9 @@ private void createStaticVarCompensators(Network network, MatpowerModel model, C double maxQ = svc.getBmax() * vSquared; double targetVpu = checkAndFixTargetVpu(findTargetVpu(svc)); Bus regulatedBus = svc.getRegulatingTerminal().getBusView().getBus(); - boolean voltageRegulation = StaticVarCompensator.RegulationMode.VOLTAGE.equals(svc.getRegulationMode()); - addMgen(model, context, bus, id, targetVpu, 0, 0, 0, targetQ, minQ, - maxQ, regulatedBus, voltageRegulation, Double.NaN); + boolean isValidVoltageRegulation = isValidVoltageRegulation(StaticVarCompensator.RegulationMode.VOLTAGE.equals(svc.getRegulationMode()), regulatedBus); + boolean isRemoteRegulation = isRemoteRegulation(bus, regulatedBus); + addMgen(context, busNumber, id, targetVpu, 0, 0, 0, targetQ, minQ, maxQ, isValidVoltageRegulation, isRemoteRegulation, Double.NaN); } } } @@ -840,8 +842,8 @@ private static void exportVscHvdcLine(VscConverterStation rectifierVscConverterS if (isExportedAsDcLine(rectifierVscConverterStation, inverterVscConverterStation)) { createDcLine(rectifierVscConverterStation, inverterVscConverterStation, hvdcLine, model, context); } else { - createGeneratorOrLoadFromVscConverter(rectifierVscConverterStation, model, context); - createGeneratorOrLoadFromVscConverter(inverterVscConverterStation, model, context); + createGeneratorOrLoadFromVscConverter(rectifierVscConverterStation, context); + createGeneratorOrLoadFromVscConverter(inverterVscConverterStation, context); } } @@ -935,11 +937,12 @@ private static double calculateL1(double l0, double losses, double rectifierTarg return rectifierTargetP != 0.0 ? (losses - l0) / rectifierTargetP : 0.0; } - private static void createGeneratorOrLoadFromVscConverter(VscConverterStation vscConverterStation, MatpowerModel model, Context context) { + private static void createGeneratorOrLoadFromVscConverter(VscConverterStation vscConverterStation, Context context) { Terminal terminal = vscConverterStation.getTerminal(); Bus bus = findBus(terminal); if (isExported(bus, context)) { + int busNumber = context.mBusesNumbersByIds.get(bus.getId()); String id = vscConverterStation.getId(); double targetQ = checkAndFixTargetQ(vscConverterStation.getReactivePowerSetpoint()); double targetVpu = checkAndFixTargetVpu(findTargetVpu(vscConverterStation)); @@ -947,49 +950,76 @@ private static void createGeneratorOrLoadFromVscConverter(VscConverterStation vs double targetP = HvdcUtils.getConverterStationTargetP(vscConverterStation); double minQ = checkAndFixMinQ(vscConverterStation.getReactiveLimits().getMinQ(targetP)); // approximation double maxQ = checkAndFixMaxQ(vscConverterStation.getReactiveLimits().getMaxQ(targetP)); // approximation - boolean voltageRegulation = vscConverterStation.isVoltageRegulatorOn(); + boolean isValidVoltageRegulation = isValidVoltageRegulation(vscConverterStation.isVoltageRegulatorOn(), regulatedBus); double maxP = vscConverterStation.getHvdcLine().getMaxP(); - addMgen(model, context, bus, id, targetVpu, targetP, -maxP, maxP, targetQ, minQ, - maxQ, regulatedBus, voltageRegulation, Double.NaN); + boolean isRemoteRegulation = isRemoteRegulation(bus, regulatedBus); + addMgen(context, busNumber, id, targetVpu, targetP, -maxP, maxP, targetQ, minQ, maxQ, isValidVoltageRegulation, isRemoteRegulation, Double.NaN); } } - private static void addMgen(MatpowerModel model, Context context, Bus bus, - String id, double targetVpu, double targetP, double minP, double maxP, double targetQ, - double minQ, double maxQ, Bus regulatedBus, boolean voltageRegulation, double ratedS) { - int busNum = context.mBusesNumbersByIds.get(bus.getId()); - MBus mBus = model.getBusByNum(busNum); - boolean validVoltageRegulation = voltageRegulation && regulatedBus != null; - // Matpower power flow does not support bus with multiple generators that do not have the same voltage regulation - // status. if the bus has PV type, all of its generator must have a valid voltage set point. - if (!validVoltageRegulation && mBus.getType() == MBus.Type.PV) { - // convert to load - mBus.setRealPowerDemand(mBus.getRealPowerDemand() - targetP); - mBus.setReactivePowerDemand(mBus.getReactivePowerDemand() - targetQ); - context.generatorIdsConvertedToLoad.add(id); - } else { - MGen mGen = new MGen(); - mGen.setNumber(busNum); - mGen.setStatus(CONNECTED_STATUS); - mGen.setRealPowerOutput(targetP); - mGen.setReactivePowerOutput(Double.isNaN(targetQ) ? 0 : targetQ); - if (validVoltageRegulation) { - if (!regulatedBus.getId().equals(bus.getId())) { - LOGGER.warn("Generator remote voltage control not supported in Matpower model, control has been localized {}", id); - } - mGen.setVoltageMagnitudeSetpoint(targetVpu); + private static void addMgen(Context context, int busNum, String id, double targetVpu, double targetP, double minP, double maxP, + double targetQ, double minQ, double maxQ, boolean isValidVoltageRegulation, boolean isRemoteRegulation, double ratedS) { + Context.GenRc genRc = new Context.GenRc(id, targetVpu, targetP, minP, maxP, targetQ, minQ, maxQ, isValidVoltageRegulation, isRemoteRegulation, ratedS); + context.generatorsToBeExported.computeIfAbsent(busNum, k -> new ArrayList<>()).add(genRc); + } + + // Matpower power flow does not support bus with multiple generators that do not have the same voltage regulation + // status. if the bus has PV type, all of its generator must have a valid voltage set point. + private static void createGeneratorsAndDefinePVBuses(MatpowerModel model, Context context) { + context.generatorsToBeExported.keySet().stream().sorted().forEach(busNumber -> { + List genRcs = context.generatorsToBeExported.get(busNumber); + MBus mBus = model.getBusByNum(busNumber); + List genRcsWithRegulationOn = genRcs.stream().filter(genRc -> genRc.isValidVoltageRegulation).toList(); + List genRcsWithRegulationOff = genRcs.stream().filter(genRc -> !genRc.isValidVoltageRegulation).toList(); + if (genRcsWithRegulationOn.isEmpty()) { + genRcsWithRegulationOff.forEach(genRc -> { + MGen mGen = createMGen(model, busNumber, genRc, context); + // we can safely set voltage setpoint to zero, because a PQ bus never go back to PV even if reactive limits + // are activated in Matpower power flow + mGen.setVoltageMagnitudeSetpoint(0); + }); } else { - // we can safely set voltage setpoint to zero, because a PQ bus never go back to PV even if reactive limits - // are activated in Matpower power flow - mGen.setVoltageMagnitudeSetpoint(0); + if (mBus.getType().equals(MBus.Type.PQ)) { + mBus.setType(MBus.Type.PV); + } + genRcsWithRegulationOn.forEach(genRc -> createMGen(model, busNumber, genRc, context)); + + genRcsWithRegulationOff.forEach(genRc -> { + mBus.setRealPowerDemand(mBus.getRealPowerDemand() - genRc.targetP); + mBus.setReactivePowerDemand(mBus.getReactivePowerDemand() - genRc.targetQ); + context.generatorIdsConvertedToLoad.add(genRc.id); + }); } - mGen.setMinimumRealPowerOutput(Math.max(minP, -context.maxGeneratorActivePowerLimit)); - mGen.setMaximumRealPowerOutput(Math.min(maxP, context.maxGeneratorActivePowerLimit)); - mGen.setMinimumReactivePowerOutput(Math.max(minQ, -context.maxGeneratorReactivePowerLimit)); - mGen.setMaximumReactivePowerOutput(Math.min(maxQ, context.maxGeneratorReactivePowerLimit)); - mGen.setTotalMbase(Double.isNaN(ratedS) ? 0 : ratedS); - model.addGenerator(mGen); + }); + } + + private static MGen createMGen(MatpowerModel model, int busNumber, Context.GenRc genRc, Context context) { + MGen mGen = new MGen(); + mGen.setNumber(busNumber); + mGen.setStatus(CONNECTED_STATUS); + mGen.setRealPowerOutput(genRc.targetP); + mGen.setReactivePowerOutput(Double.isNaN(genRc.targetQ) ? 0 : genRc.targetQ); + mGen.setVoltageMagnitudeSetpoint(genRc.targetVpu); + + mGen.setMinimumRealPowerOutput(Math.max(genRc.minP, -context.maxGeneratorActivePowerLimit)); + mGen.setMaximumRealPowerOutput(Math.min(genRc.maxP, context.maxGeneratorActivePowerLimit)); + mGen.setMinimumReactivePowerOutput(Math.max(genRc.minQ, -context.maxGeneratorReactivePowerLimit)); + mGen.setMaximumReactivePowerOutput(Math.min(genRc.maxQ, context.maxGeneratorReactivePowerLimit)); + mGen.setTotalMbase(Double.isNaN(genRc.ratedS) ? 0 : genRc.ratedS); + model.addGenerator(mGen); + + if (genRc.isRemoteRegulation) { + LOGGER.warn("Generator remote voltage control not supported in Matpower model, control has been localized {}", genRc.id); } + return mGen; + } + + private static boolean isValidVoltageRegulation(boolean voltageRegulation, Bus regulatedBus) { + return voltageRegulation && regulatedBus != null; + } + + private static boolean isRemoteRegulation(Bus bus, Bus regulatedBus) { + return !(bus != null && regulatedBus != null && bus.getId().equals(regulatedBus.getId())); } private static double checkAndFixVoltageMagnitude(double voltageMagnitude) { @@ -1071,11 +1101,13 @@ public void export(Network network, Properties parameters, DataSource dataSource context.num = preserveBusIds(network, context); createBuses(network, model, context); createBranches(network, model, context); - createGenerators(network, model, context); - createStaticVarCompensators(network, model, context); - createDanglingLineGenerators(network, model, context); + findGenerators(network, context); + findStaticVarCompensatorGenerators(network, context); + findDanglingLineGenerators(network, context); createDcLines(network, model, context); + createGeneratorsAndDefinePVBuses(model, context); + if (!context.generatorIdsConvertedToLoad.isEmpty()) { LOGGER.debug("{} generators have been converted to a load: {}", context.generatorIdsConvertedToLoad.size(), context.generatorIdsConvertedToLoad); } diff --git a/matpower/matpower-converter/src/test/resources/dangling-line-generation.json b/matpower/matpower-converter/src/test/resources/dangling-line-generation.json index 1b3a455a95d..689745cfeda 100644 --- a/matpower/matpower-converter/src/test/resources/dangling-line-generation.json +++ b/matpower/matpower-converter/src/test/resources/dangling-line-generation.json @@ -19,7 +19,7 @@ "minimumVoltageMagnitude" : 0.8 }, { "number" : 2, - "type" : "PQ", + "type" : "PV", "name" : "DL", "realPowerDemand" : 50.0, "reactivePowerDemand" : 30.0, @@ -58,7 +58,7 @@ }, { "number" : 2, "realPowerOutput" : 440.0, - "reactivePowerOutput" : "NaN", + "reactivePowerOutput" : 0.0, "maximumReactivePowerOutput" : 46.25, "minimumReactivePowerOutput" : -54.55, "voltageMagnitudeSetpoint" : 1.01, diff --git a/matpower/matpower-converter/src/test/resources/fourSubstationFactory.json b/matpower/matpower-converter/src/test/resources/fourSubstationFactory.json index d84c7b1e8ca..d61f1db9379 100644 --- a/matpower/matpower-converter/src/test/resources/fourSubstationFactory.json +++ b/matpower/matpower-converter/src/test/resources/fourSubstationFactory.json @@ -34,7 +34,7 @@ "minimumVoltageMagnitude" : 0.975 }, { "number" : 3, - "type" : "PQ", + "type" : "PV", "name" : "S4VL1_0", "realPowerDemand" : 240.0, "reactivePowerDemand" : 10.0, @@ -71,16 +71,16 @@ "rampQ" : 0.0, "apf" : 0.0 }, { - "number" : 2, - "realPowerOutput" : 250.9944, - "reactivePowerOutput" : 71.8487, - "maximumReactivePowerOutput" : 185.0972075, - "minimumReactivePowerOutput" : -172.5943015, - "voltageMagnitudeSetpoint" : 1.0, + "number" : 1, + "realPowerOutput" : 9.780605394241391, + "reactivePowerOutput" : 120.0, + "maximumReactivePowerOutput" : 500.0, + "minimumReactivePowerOutput" : -400.0, + "voltageMagnitudeSetpoint" : 0.0, "totalMbase" : 0.0, "status" : 1, - "maximumRealPowerOutput" : 400.0, - "minimumRealPowerOutput" : 0.0, + "maximumRealPowerOutput" : 300.0, + "minimumRealPowerOutput" : -300.0, "pc1" : 0.0, "pc2" : 0.0, "qc1Min" : 0.0, @@ -93,15 +93,15 @@ "rampQ" : 0.0, "apf" : 0.0 }, { - "number" : 3, - "realPowerOutput" : 0.0, - "reactivePowerOutput" : 0.0, - "maximumReactivePowerOutput" : 8000.0, - "minimumReactivePowerOutput" : -8000.0, + "number" : 2, + "realPowerOutput" : 250.9944, + "reactivePowerOutput" : 71.8487, + "maximumReactivePowerOutput" : 185.0972075, + "minimumReactivePowerOutput" : -172.5943015, "voltageMagnitudeSetpoint" : 1.0, "totalMbase" : 0.0, "status" : 1, - "maximumRealPowerOutput" : 0.0, + "maximumRealPowerOutput" : 400.0, "minimumRealPowerOutput" : 0.0, "pc1" : 0.0, "pc2" : 0.0, @@ -115,16 +115,16 @@ "rampQ" : 0.0, "apf" : 0.0 }, { - "number" : 1, - "realPowerOutput" : 9.780605394241391, - "reactivePowerOutput" : 120.0, - "maximumReactivePowerOutput" : 500.0, - "minimumReactivePowerOutput" : -400.0, - "voltageMagnitudeSetpoint" : 0.0, + "number" : 3, + "realPowerOutput" : 0.0, + "reactivePowerOutput" : 0.0, + "maximumReactivePowerOutput" : 8000.0, + "minimumReactivePowerOutput" : -8000.0, + "voltageMagnitudeSetpoint" : 1.0, "totalMbase" : 0.0, "status" : 1, - "maximumRealPowerOutput" : 300.0, - "minimumRealPowerOutput" : -300.0, + "maximumRealPowerOutput" : 0.0, + "minimumRealPowerOutput" : 0.0, "pc1" : 0.0, "pc2" : 0.0, "qc1Min" : 0.0,