diff --git a/src/main/java/org/gridsuite/modification/server/modifications/GenerationDispatch.java b/src/main/java/org/gridsuite/modification/server/modifications/GenerationDispatch.java index 5146a609a..d55a9aef6 100644 --- a/src/main/java/org/gridsuite/modification/server/modifications/GenerationDispatch.java +++ b/src/main/java/org/gridsuite/modification/server/modifications/GenerationDispatch.java @@ -148,53 +148,69 @@ private static Double getGeneratorMarginalCost(Generator generator) { return null; } - private static List computeAdjustableGenerators(Network network, Component component, List generatorsWithFixedSupply, - List substationsGeneratorsOrderingInfos, - Reporter reporter) { - List generatorsWithMarginalCost; - - // get all generators in the component - List generators = component.getBusStream().flatMap(Bus::getGeneratorStream).collect(Collectors.toList()); - - // remove generators with fixed supply - generators.removeIf(generator -> generatorsWithFixedSupply.contains(generator.getId())); + private static Map> getGeneratorsByMarginalCost(List generators, Reporter reporter, String reporterSuffixKey) { + Map> generatorsByMarginalCost = new TreeMap<>(); // set targetP to 0 generators.forEach(generator -> generator.setTargetP(0.)); // get generators with marginal cost - generatorsWithMarginalCost = generators.stream().filter(generator -> { - Double marginalCost = getGeneratorMarginalCost(generator); - if (marginalCost == null) { - report(reporter, Integer.toString(component.getNum()), "MissingMarginalCostForGenerator", "The generator ${generator} does not have a marginal cost", - Map.of(GENERATOR, generator.getId()), TypedValue.WARN_SEVERITY); - } - return marginalCost != null; - }).collect(Collectors.toList()); + List generatorsWithMarginalCost = generators.stream() + .filter(generator -> getGeneratorMarginalCost(generator) != null) + .collect(Collectors.toList()); + int nbNoCost = generators.size() - generatorsWithMarginalCost.size(); + if (nbNoCost > 0) { + report(reporter, reporterSuffixKey, "NbGeneratorsWithNoCost", "${nbNoCost} generator${isPlural} been discarded from generation dispatch because of missing marginal cost. Their active power set point has been set to 0", + Map.of("nbNoCost", nbNoCost, + "isPlural", nbNoCost > 1 ? "s have" : " has"), + TypedValue.WARN_SEVERITY); + } + generators.stream() + .filter(generator -> getGeneratorMarginalCost(generator) == null) + .forEach(g -> report(reporter, reporterSuffixKey, "MissingMarginalCostForGenerator", "The generator ${generator} does not have a marginal cost", + Map.of(GENERATOR, g.getId()), TypedValue.TRACE_SEVERITY) + ); // build map of generators by marginal cost generatorsWithMarginalCost.sort(Comparator.comparing(GenerationDispatch::getGeneratorMarginalCost)); - Map> generatorsByMarginalCost = new TreeMap<>(); generatorsWithMarginalCost.forEach(g -> { Double marginalCost = getGeneratorMarginalCost(g); generatorsByMarginalCost.computeIfAbsent(marginalCost, k -> new ArrayList<>()); generatorsByMarginalCost.get(marginalCost).add(g.getId()); }); - List generatorsToReturn = new ArrayList<>(); + return generatorsByMarginalCost; + } - // log substations not found + private static void reportUnknownSubstations(Network network, List substationsGeneratorsOrderingInfos, Reporter reporter, String reporterSuffixKey) { if (!CollectionUtils.isEmpty(substationsGeneratorsOrderingInfos)) { substationsGeneratorsOrderingInfos.forEach(sInfo -> - sInfo.getSubstationIds().forEach(sId -> { - Substation substation = network.getSubstation(sId); - if (substation == null) { - report(reporter, Integer.toString(component.getNum()), "SubstationNotFound", "Substation ${substation} not found", - Map.of(SUBSTATION, sId), TypedValue.WARN_SEVERITY); - } - })); + sInfo.getSubstationIds().forEach(sId -> { + Substation substation = network.getSubstation(sId); + if (substation == null) { + report(reporter, reporterSuffixKey, "SubstationNotFound", "Substation ${substation} not found", + Map.of(SUBSTATION, sId), TypedValue.WARN_SEVERITY); + } + })); } + } + private static List computeAdjustableGenerators(Network network, Component component, List generatorsWithFixedSupply, + List substationsGeneratorsOrderingInfos, + Reporter reporter) { + List generatorsToReturn = new ArrayList<>(); + String reporterSuffixKey = Integer.toString(component.getNum()); + + // log substations not found + reportUnknownSubstations(network, substationsGeneratorsOrderingInfos, reporter, reporterSuffixKey); + + // get all connected generators in the component + List generators = component.getBusStream().flatMap(Bus::getGeneratorStream).collect(Collectors.toList()); + + // remove generators with fixed supply + generators.removeIf(generator -> generatorsWithFixedSupply.contains(generator.getId())); + + Map> generatorsByMarginalCost = getGeneratorsByMarginalCost(generators, reporter, reporterSuffixKey); generatorsByMarginalCost.forEach((mCost, gList) -> { // loop on generators of same cost if (!CollectionUtils.isEmpty(substationsGeneratorsOrderingInfos)) { // substations hierarchy provided // build mapGeneratorsBySubstationsList, that will contain all the generators with the same marginal cost as mCost contained in each list of substations @@ -247,7 +263,7 @@ private static List computeAdjustableGenerators(Network network, Comp }); if (generatorsToReturn.isEmpty()) { - report(reporter, Integer.toString(component.getNum()), "NoAvailableAdjustableGenerator", "There is no adjustable generator", + report(reporter, reporterSuffixKey, "NoAvailableAdjustableGenerator", "There is no adjustable generator", Map.of(), TypedValue.WARN_SEVERITY); } @@ -257,6 +273,7 @@ private static List computeAdjustableGenerators(Network network, Comp private static class GeneratorTargetPListener extends DefaultNetworkListener { private final Reporter reporter; private final String suffixKey; + private final List updatedGenerators = new ArrayList<>(); GeneratorTargetPListener(Reporter reporter, String suffixKey) { this.reporter = reporter; @@ -265,11 +282,29 @@ private static class GeneratorTargetPListener extends DefaultNetworkListener { @Override public void onUpdate(Identifiable identifiable, String attribute, String variantId, Object oldValue, Object newValue) { - if (identifiable.getType() == IdentifiableType.GENERATOR && - attribute.equals("targetP") && - Double.compare((double) oldValue, (double) newValue) != 0) { - report(reporter, suffixKey, "GeneratorSetTargetP", "Generator ${generator} targetP : ${oldValue} MW --> ${newValue} MW", - Map.of(GENERATOR, identifiable.getId(), "oldValue", oldValue, "newValue", newValue), TypedValue.INFO_SEVERITY); + if (identifiable.getType() == IdentifiableType.GENERATOR && attribute.equals("targetP") && Double.compare((double) oldValue, (double) newValue) != 0) { + updatedGenerators.add((Generator) identifiable); + } + } + + public void endReport(List adjustableGenerators) { + // report updated generators + report(reporter, suffixKey, "TotalGeneratorSetTargetP", "The active power set points of ${nbUpdatedGenerator} generator${isPlural} have been updated as a result of generation dispatch", + Map.of("nbUpdatedGenerator", updatedGenerators.size(), "isPlural", updatedGenerators.size() > 1 ? "s" : ""), TypedValue.INFO_SEVERITY); + updatedGenerators.forEach(g -> report(reporter, suffixKey, "GeneratorSetTargetP", "The active power set point of generator ${generator} has been set to ${newValue} MW", + Map.of(GENERATOR, g.getId(), "newValue", g.getTargetP()), TypedValue.TRACE_SEVERITY)); + + // report unchanged generators + int nbUnchangedGenerators = adjustableGenerators.size() - updatedGenerators.size(); + if (nbUnchangedGenerators > 0) { + List updatedGeneratorsIds = updatedGenerators.stream().map(Identifiable::getId).toList(); + report(reporter, suffixKey, "TotalGeneratorUnchangedTargetP", "${nbUnchangedGenerator} eligible generator${isPlural} not been selected by the merit order algorithm. Their active power set point has been set to 0", + Map.of("nbUnchangedGenerator", nbUnchangedGenerators, + "isPlural", nbUnchangedGenerators > 1 ? "s have" : " has"), TypedValue.INFO_SEVERITY); + adjustableGenerators.stream() + .filter(g -> !updatedGeneratorsIds.contains(g.getId())) + .forEach(g -> report(reporter, suffixKey, "GeneratorUnchangedTargetP", "Generator ${generator} has not been selected by the merit order algorithm. Its active power set point has been set to 0", + Map.of(GENERATOR, g.getId()), TypedValue.TRACE_SEVERITY)); } } } @@ -379,6 +414,23 @@ private double reduceGeneratorMaxPValue(Generator generator, return Math.max(generator.getMinP(), res * (1. - genFrequencyReserve / 100.)); } + private void reportDisconnectedGenerators(List globalDisconnectedGenerators, int componentNum, Reporter reporter) { + List componentDisconnectedGenerators = globalDisconnectedGenerators.stream() + .filter(g -> g.getTerminal().getBusView() != null && g.getTerminal().getBusView().getConnectableBus() != null && + g.getTerminal().getBusView().getConnectableBus().getSynchronousComponent().getNum() == componentNum) + .toList(); + if (!componentDisconnectedGenerators.isEmpty()) { + report(reporter, Integer.toString(componentNum), "TotalDisconnectedGenerator", "${nbDisconnectedGenerator} generator${isPlural} been discarded from generation dispatch because their are disconnected. Their active power set point remains unchanged", + Map.of("nbDisconnectedGenerator", componentDisconnectedGenerators.size(), + "isPlural", componentDisconnectedGenerators.size() > 1 ? "s have" : " has"), + TypedValue.INFO_SEVERITY); + componentDisconnectedGenerators.forEach(g -> + report(reporter, Integer.toString(componentNum), "DisconnectedGenerator", "Generator ${generator} has been discarded from generation dispatch because it is disconnected. Its active power set point remains unchanged", + Map.of(GENERATOR, g.getId()), TypedValue.TRACE_SEVERITY) + ); + } + } + @Override public void apply(Network network, Reporter subReporter) { Collection synchronousComponents = network.getBusView().getBusStream() @@ -386,6 +438,17 @@ public void apply(Network network, Reporter subReporter) { .map(Bus::getSynchronousComponent) .collect(collectingAndThen(toCollection(() -> new TreeSet<>(comparingInt(Component::getNum))), ArrayList::new)); + report(subReporter, "", "NbSynchronousComponents", "Network has ${scNumber} synchronous component${isPlural}: ${scList}", + Map.of("scNumber", synchronousComponents.size(), + "isPlural", synchronousComponents.size() > 1 ? "s" : "", + "scList", synchronousComponents.stream().map(sc -> "SC" + sc.getNum()).collect(Collectors.joining(", "))), + TypedValue.INFO_SEVERITY); + + // all disconnected generators at network level (for report purpose) + List disconnectedGenerators = network.getGeneratorStream() + .filter(g -> !g.getTerminal().isConnected()) + .toList(); + // get generators for which there will be no reduction of maximal power List generatorsWithoutOutage = collectGeneratorsWithoutOutage(network, subReporter); @@ -402,6 +465,9 @@ public void apply(Network network, Reporter subReporter) { Reporter powerToDispatchReporter = componentReporter.createSubReporter(POWER_TO_DISPATCH, POWER_TO_DISPATCH); + // log disconnected generators attached to this synchronous component + reportDisconnectedGenerators(disconnectedGenerators, componentNum, powerToDispatchReporter); + // get total value of connected loads in the connected component double totalDemand = computeTotalDemand(component, generationDispatchInfos.getLossCoefficient()); report(powerToDispatchReporter, Integer.toString(componentNum), "TotalDemand", "The total demand is : ${totalDemand} MW", @@ -449,6 +515,7 @@ public void apply(Network network, Reporter subReporter) { Scalable scalable = Scalable.stack(generatorsScalable.toArray(Scalable[]::new)); realized = scalable.scale(network, totalAmountSupplyToBeDispatched, new ScalingParameters().setAllowsGeneratorOutOfActivePowerLimits(true)); + listener.endReport(adjustableGenerators); network.removeListener(listener); } diff --git a/src/test/java/org/gridsuite/modification/server/modifications/GenerationDispatchTest.java b/src/test/java/org/gridsuite/modification/server/modifications/GenerationDispatchTest.java index 9f2c3d776..455bb8c0d 100644 --- a/src/test/java/org/gridsuite/modification/server/modifications/GenerationDispatchTest.java +++ b/src/test/java/org/gridsuite/modification/server/modifications/GenerationDispatchTest.java @@ -537,19 +537,19 @@ public void testGenerationDispatchWithMaxValueLessThanMinP() throws Exception { assertLogMessage("The total amount of fixed supply is : 0.0 MW", "TotalAmountFixedSupply" + firstSynchronousComponentNum, reportService); assertLogMessage("The HVDC balance is : 90.0 MW", "TotalOutwardHvdcFlow" + firstSynchronousComponentNum, reportService); assertLogMessage("The total amount of supply to be dispatched is : 438.0 MW", "TotalAmountSupplyToBeDispatched" + firstSynchronousComponentNum, reportService); - assertLogNthMessage("Generator TEST1 targetP : 0.0 MW --> 40.375 MW", "GeneratorSetTargetP" + firstSynchronousComponentNum, reportService, 1); - assertLogNthMessage("Generator GTH1 targetP : 0.0 MW --> 80.0 MW", "GeneratorSetTargetP" + firstSynchronousComponentNum, reportService, 2); - assertLogNthMessage("Generator GTH2 targetP : 0.0 MW --> 146.0 MW", "GeneratorSetTargetP" + firstSynchronousComponentNum, reportService, 3); + assertLogNthMessage("The active power set point of generator TEST1 has been set to 40.375 MW", "GeneratorSetTargetP" + firstSynchronousComponentNum, reportService, 1); + assertLogNthMessage("The active power set point of generator GTH1 has been set to 80.0 MW", "GeneratorSetTargetP" + firstSynchronousComponentNum, reportService, 2); + assertLogNthMessage("The active power set point of generator GTH2 has been set to 146.0 MW", "GeneratorSetTargetP" + firstSynchronousComponentNum, reportService, 3); assertLogMessage("The supply-demand balance could not be met : the remaining power imbalance is 171.625 MW", "SupplyDemandBalanceCouldNotBeMet" + firstSynchronousComponentNum, reportService); int secondSynchronousComponentNum = getNetwork().getGenerator(GH1_ID).getTerminal().getBusView().getBus().getSynchronousComponent().getNum(); // GH1 is in second synchronous component assertLogMessage("The total demand is : 240.0 MW", "TotalDemand" + secondSynchronousComponentNum, reportService); assertLogMessage("The total amount of fixed supply is : 0.0 MW", "TotalAmountFixedSupply" + secondSynchronousComponentNum, reportService); assertLogMessage("The HVDC balance is : -90.0 MW", "TotalOutwardHvdcFlow" + secondSynchronousComponentNum, reportService); assertLogMessage("The total amount of supply to be dispatched is : 330.0 MW", "TotalAmountSupplyToBeDispatched" + secondSynchronousComponentNum, reportService); - assertLogNthMessage("Generator GH1 targetP : 0.0 MW --> 80.0 MW", "GeneratorSetTargetP" + secondSynchronousComponentNum, reportService, 1); - assertLogNthMessage("Generator GH2 targetP : 0.0 MW --> 60.0 MW", "GeneratorSetTargetP" + secondSynchronousComponentNum, reportService, 2); - assertLogNthMessage("Generator GH3 targetP : 0.0 MW --> 126.1 MW", "GeneratorSetTargetP" + secondSynchronousComponentNum, reportService, 3); - assertLogNthMessage("Generator ABC targetP : 0.0 MW --> 63.900000000000006 MW", "GeneratorSetTargetP" + secondSynchronousComponentNum, reportService, 4); + assertLogNthMessage("The active power set point of generator GH1 has been set to 80.0 MW", "GeneratorSetTargetP" + secondSynchronousComponentNum, reportService, 1); + assertLogNthMessage("The active power set point of generator GH2 has been set to 60.0 MW", "GeneratorSetTargetP" + secondSynchronousComponentNum, reportService, 2); + assertLogNthMessage("The active power set point of generator GH3 has been set to 126.1 MW", "GeneratorSetTargetP" + secondSynchronousComponentNum, reportService, 3); + assertLogNthMessage("The active power set point of generator ABC has been set to 63.900000000000006 MW", "GeneratorSetTargetP" + secondSynchronousComponentNum, reportService, 4); assertLogMessage("The supply-demand balance could be met", "SupplyDemandBalanceCouldBeMet" + secondSynchronousComponentNum, reportService); wireMockUtils.verifyGetRequest(stubIdForPmaxReduction, PATH, handleQueryParams(getNetworkUuid(), getGeneratorsWithoutOutageFilters123().stream().map(FilterEquipments::getFilterId).collect(Collectors.toList())), false);