diff --git a/docs/user/itools/loadflow-validation.md b/docs/user/itools/loadflow-validation.md index 45d6fb207f1..688418192e4 100644 --- a/docs/user/itools/loadflow-validation.md +++ b/docs/user/itools/loadflow-validation.md @@ -14,6 +14,7 @@ usage: itools [OPTIONS] loadflow-validation --case-file [--load-flow] --output-folder [--output-format ] [--run-computation ] [--types ] [--verbose] + [--with-extensions-validation] Available options are: --config-name Override configuration file name @@ -86,6 +87,9 @@ Use the `--load-flow` parameter to run a load-flow before the validation. This o `--output-format`
Use the `--output-format` parameter to specify the format of the output files. The available output formats are `CSV` or `CSV_MULTILINE`. +`--with-extensions-validation`
+Use the `--with-extensions-validation` parameter to perform extensions validation. + If this parameter is set to `CSV`, in the output files a line contains all values of validated equipment. If the parameter is set to `CSV_MULTILINE`, in the output files the values of a piece of equipment are split in multiple lines, one value for each line, see examples below: diff --git a/loadflow/loadflow-validation/pom.xml b/loadflow/loadflow-validation/pom.xml index 2e93a365a5e..21c8ce0474c 100644 --- a/loadflow/loadflow-validation/pom.xml +++ b/loadflow/loadflow-validation/pom.xml @@ -99,7 +99,15 @@ org.slf4j slf4j-simple + + com.powsybl + powsybl-tools-test + test + + + ${project.groupId} + powsybl-iidm-serde + test + - - diff --git a/loadflow/loadflow-validation/src/main/java/com/powsybl/loadflow/validation/ValidationTool.java b/loadflow/loadflow-validation/src/main/java/com/powsybl/loadflow/validation/ValidationTool.java index fd1f8fe83e8..582c09566c8 100644 --- a/loadflow/loadflow-validation/src/main/java/com/powsybl/loadflow/validation/ValidationTool.java +++ b/loadflow/loadflow-validation/src/main/java/com/powsybl/loadflow/validation/ValidationTool.java @@ -9,7 +9,6 @@ import com.google.auto.service.AutoService; import com.google.common.base.Preconditions; -import com.google.common.collect.Sets; import com.powsybl.commons.PowsyblException; import com.powsybl.iidm.network.ImportConfig; import com.powsybl.iidm.network.Network; @@ -17,6 +16,7 @@ import com.powsybl.iidm.network.tools.ConversionToolUtils; import com.powsybl.loadflow.LoadFlow; import com.powsybl.loadflow.LoadFlowParameters; +import com.powsybl.loadflow.validation.extension.ExtensionsValidation; import com.powsybl.loadflow.validation.io.ValidationWriters; import com.powsybl.tools.Command; import com.powsybl.tools.Tool; @@ -28,11 +28,8 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Arrays; -import java.util.Objects; -import java.util.Properties; -import java.util.Set; + +import java.util.*; import java.util.stream.Collectors; import static com.powsybl.iidm.network.tools.ConversionToolUtils.*; @@ -53,6 +50,7 @@ public class ValidationTool implements Tool { private static final String COMPARE_RESULTS = "compare-results"; private static final String RUN_COMPUTATION = "run-computation"; private static final String COMPARE_CASE_FILE = "compare-case-file"; + public static final String WITH_EXTENSIONS_OPTION = "with-extensions-validation"; private static final Command COMMAND = new Command() { @@ -120,6 +118,11 @@ public Options getOptions() { .build()); options.addOption(createImportParametersFileOption()); options.addOption(createImportParameterOption()); + options.addOption(Option.builder().longOpt(WITH_EXTENSIONS_OPTION) + .desc("enable extension validation") + .hasArg(false) + .argName("EXTENSIONS") + .build()); return options; } @@ -137,8 +140,8 @@ public Command getCommand() { @Override public void run(CommandLine line, ToolRunningContext context) throws Exception { - Path caseFile = Paths.get(line.getOptionValue(CASE_FILE)); - Path outputFolder = Paths.get(line.getOptionValue(OUTPUT_FOLDER)); + Path caseFile = context.getFileSystem().getPath(line.getOptionValue(CASE_FILE)); + Path outputFolder = context.getFileSystem().getPath(line.getOptionValue(OUTPUT_FOLDER)); if (!Files.exists(outputFolder)) { Files.createDirectories(outputFolder); } @@ -154,11 +157,11 @@ public void run(CommandLine line, ToolRunningContext context) throws Exception { config.setCompareResults(true); comparisonType = ComparisonType.valueOf(line.getOptionValue(COMPARE_RESULTS)); } - Set validationTypes = Sets.newHashSet(ValidationType.values()); + Set validationTypes = new TreeSet<>(Arrays.asList(ValidationType.values())); if (line.hasOption(TYPES)) { validationTypes = Arrays.stream(line.getOptionValue(TYPES).split(",")) - .map(ValidationType::valueOf) - .collect(Collectors.toSet()); + .map(ValidationType::valueOf) + .collect(Collectors.toCollection(TreeSet::new)); } Network network = loadNetwork(caseFile, line, context); try (ValidationWriters validationWriters = new ValidationWriters(network.getId(), validationTypes, outputFolder, config)) { @@ -182,12 +185,16 @@ public void run(CommandLine line, ToolRunningContext context) throws Exception { if (config.isCompareResults() && ComparisonType.BASECASE.equals(comparisonType)) { Preconditions.checkArgument(line.hasOption(COMPARE_CASE_FILE), - "Basecases comparison requires to provide a second basecase (option --" + COMPARE_CASE_FILE + ")."); - Path compareCaseFile = Paths.get(line.getOptionValue(COMPARE_CASE_FILE)); + "Base cases comparison requires to provide a second basecase (option --" + COMPARE_CASE_FILE + ")."); + Path compareCaseFile = context.getFileSystem().getPath(line.getOptionValue(COMPARE_CASE_FILE)); Network compareNetwork = loadNetwork(compareCaseFile, line, context); context.getOutputStream().println("Running validation on network " + compareNetwork.getId() + " to compare"); runValidation(compareNetwork, config, validationTypes, validationWriters, context); } + if (line.hasOption(WITH_EXTENSIONS_OPTION)) { + ExtensionsValidation extensionsValidation = new ExtensionsValidation(); + extensionsValidation.runExtensionValidations(network, config, context); + } } } diff --git a/loadflow/loadflow-validation/src/main/java/com/powsybl/loadflow/validation/extension/ExtensionValidation.java b/loadflow/loadflow-validation/src/main/java/com/powsybl/loadflow/validation/extension/ExtensionValidation.java new file mode 100644 index 00000000000..cbe20a20990 --- /dev/null +++ b/loadflow/loadflow-validation/src/main/java/com/powsybl/loadflow/validation/extension/ExtensionValidation.java @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2026, 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/. + * SPDX-License-Identifier: MPL-2.0 + */ +package com.powsybl.loadflow.validation.extension; +import com.powsybl.iidm.network.Network; +import com.powsybl.loadflow.validation.ValidationConfig; + +/** + * + * @author Samir Romdhani {@literal } + */ +public interface ExtensionValidation { + + String getName(); + + boolean check(Network network, ValidationConfig config); + +} diff --git a/loadflow/loadflow-validation/src/main/java/com/powsybl/loadflow/validation/extension/ExtensionsValidation.java b/loadflow/loadflow-validation/src/main/java/com/powsybl/loadflow/validation/extension/ExtensionsValidation.java new file mode 100644 index 00000000000..c05abb1dcff --- /dev/null +++ b/loadflow/loadflow-validation/src/main/java/com/powsybl/loadflow/validation/extension/ExtensionsValidation.java @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2026, 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/. + * SPDX-License-Identifier: MPL-2.0 + */ +package com.powsybl.loadflow.validation.extension; + +import com.google.common.base.Suppliers; +import com.google.common.collect.Lists; +import com.powsybl.iidm.network.Network; +import com.powsybl.loadflow.validation.ValidationConfig; +import com.powsybl.tools.ToolRunningContext; + +import java.util.List; +import java.util.Optional; +import java.util.ServiceLoader; +import java.util.function.Supplier; + +/** + * @author Samir Romdhani {@literal } + */ +public class ExtensionsValidation { + + private static final Supplier> EXTENSIONS = Suppliers.memoize(() -> Lists.newArrayList( + ServiceLoader.load(ExtensionValidation.class, ExtensionsValidation.class.getClassLoader()))); + + public static List getExtensions() { + return EXTENSIONS.get(); + } + + public static List getExtensionsNames() { + return getExtensions().stream().map(ExtensionValidation::getName).toList(); + } + + public static Optional getExtension(String name) { + return getExtensions().stream().filter(v -> v.getName().equals(name)).findFirst(); + } + + public void runExtensionValidations(Network network, ValidationConfig config, ToolRunningContext context) { + getExtensions().forEach(extensionValidation -> { + boolean success = extensionValidation.check(network, config); + context.getOutputStream().println("Validate load-flow results of network " + network.getId() + + " - validation type: Extension/" + extensionValidation.getName() + + " - result: " + (success ? "success" : "fail")); + }); + } +} diff --git a/loadflow/loadflow-validation/src/test/java/com/powsybl/loadflow/validation/ValidationConfigTest.java b/loadflow/loadflow-validation/src/test/java/com/powsybl/loadflow/validation/ValidationConfigTest.java index 09a9c5e7e0b..7e0c5f8af77 100644 --- a/loadflow/loadflow-validation/src/test/java/com/powsybl/loadflow/validation/ValidationConfigTest.java +++ b/loadflow/loadflow-validation/src/test/java/com/powsybl/loadflow/validation/ValidationConfigTest.java @@ -99,7 +99,7 @@ void checkCompleteConfig() throws Exception { } @Test - void checkSetters() throws Exception { + void checkSetters() { ValidationConfig config = ValidationConfig.load(platformConfig); config.setThreshold(threshold); config.setVerbose(verbose); diff --git a/loadflow/loadflow-validation/src/test/java/com/powsybl/loadflow/validation/ValidationToolTest.java b/loadflow/loadflow-validation/src/test/java/com/powsybl/loadflow/validation/ValidationToolTest.java new file mode 100644 index 00000000000..3bb4ca77e8a --- /dev/null +++ b/loadflow/loadflow-validation/src/test/java/com/powsybl/loadflow/validation/ValidationToolTest.java @@ -0,0 +1,89 @@ +/** + * Copyright (c) 2026, 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/. + * SPDX-License-Identifier: MPL-2.0 + */ +package com.powsybl.loadflow.validation; + +import com.powsybl.tools.Tool; +import com.powsybl.tools.test.AbstractToolTest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.nio.file.Files; +import java.util.Collections; +import java.util.Objects; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * + * @author Samir Romdhani {@literal } + */ +class ValidationToolTest extends AbstractToolTest { + + private static final String COMMAND_NAME = "loadflow-validation"; + private final ValidationTool tool = new ValidationTool(); + + @Override + @BeforeEach + public void setUp() throws Exception { + super.setUp(); + Files.copy(Objects.requireNonNull(getClass().getResourceAsStream("/network.xiidm")), fileSystem.getPath("network.xiidm")); + } + + @Override + protected Iterable getTools() { + return Collections.singleton(tool); + } + + @Override + public void assertCommand() { + assertEquals(COMMAND_NAME, tool.getCommand().getName()); + assertEquals("Computation", tool.getCommand().getTheme()); + assertEquals("Validate load-flow results of a network", tool.getCommand().getDescription()); + + assertCommand(tool.getCommand(), COMMAND_NAME, 12, 2); + assertOption(tool.getCommand().getOptions(), "case-file", true, true); + assertOption(tool.getCommand().getOptions(), "output-folder", true, true); + assertOption(tool.getCommand().getOptions(), "load-flow", false, false); + assertOption(tool.getCommand().getOptions(), "types", false, true); + assertOption(tool.getCommand().getOptions(), "with-extensions-validation", false, false); + } + + @Test + void testCommand() { + assertCommand(); + } + + @Test + void loadFlowValidationShouldSucceedWithRequiredOption() { + String expectedOut = String.join(System.lineSeparator(), + "Loading case network.xiidm", + "Validate load-flow results of network network - validation type: FLOWS - result: success", + "Validate load-flow results of network network - validation type: GENERATORS - result: success", + "Validate load-flow results of network network - validation type: BUSES - result: success", + "Validate load-flow results of network network - validation type: SVCS - result: success", + "Validate load-flow results of network network - validation type: SHUNTS - result: success", + "Validate load-flow results of network network - validation type: TWTS - result: success", + "Validate load-flow results of network network - validation type: TWTS3W - result: success" + System.lineSeparator()); + assertCommandSuccessfulMatch(new String[]{"loadflow-validation", "--case-file", "network.xiidm", "--output-folder", "test"}, expectedOut); + } + + @Test + void loadFlowValidationShouldComputePrivateValidation() { + String expectedOut = String.join(System.lineSeparator(), + "Loading case network.xiidm", + "Validate load-flow results of network network - validation type: FLOWS - result: success", + "Validate load-flow results of network network - validation type: GENERATORS - result: success", + "Validate load-flow results of network network - validation type: BUSES - result: success", + "Validate load-flow results of network network - validation type: SVCS - result: success", + "Validate load-flow results of network network - validation type: SHUNTS - result: success", + "Validate load-flow results of network network - validation type: TWTS - result: success", + "Validate load-flow results of network network - validation type: TWTS3W - result: success", + "Validate load-flow results of network network - validation type: Extension/extensionValidationMock1 - result: success" + System.lineSeparator()); + assertCommandSuccessfulMatch(new String[]{"loadflow-validation", "--case-file", "network.xiidm", "--output-folder", "test", "--with-extensions-validation"}, expectedOut); + } +} diff --git a/loadflow/loadflow-validation/src/test/java/com/powsybl/loadflow/validation/extension/ExtensionValidationMock.java b/loadflow/loadflow-validation/src/test/java/com/powsybl/loadflow/validation/extension/ExtensionValidationMock.java new file mode 100644 index 00000000000..b3d42a6660b --- /dev/null +++ b/loadflow/loadflow-validation/src/test/java/com/powsybl/loadflow/validation/extension/ExtensionValidationMock.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2026, 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/. + * SPDX-License-Identifier: MPL-2.0 + */ +package com.powsybl.loadflow.validation.extension; +import com.google.auto.service.AutoService; +import com.powsybl.iidm.network.Network; +import com.powsybl.loadflow.validation.ValidationConfig; + +/** + * + * @author Samir Romdhani {@literal } + */ +@AutoService(ExtensionValidation.class) +public class ExtensionValidationMock implements ExtensionValidation { + + @Override + public String getName() { + return "extensionValidationMock1"; + } + + @Override + public boolean check(Network network, ValidationConfig config) { + return true; + } + +} diff --git a/loadflow/loadflow-validation/src/test/java/com/powsybl/loadflow/validation/extension/ExtensionsValidationTest.java b/loadflow/loadflow-validation/src/test/java/com/powsybl/loadflow/validation/extension/ExtensionsValidationTest.java new file mode 100644 index 00000000000..e3da497981f --- /dev/null +++ b/loadflow/loadflow-validation/src/test/java/com/powsybl/loadflow/validation/extension/ExtensionsValidationTest.java @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2026, 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/. + * SPDX-License-Identifier: MPL-2.0 + */ +package com.powsybl.loadflow.validation.extension; + +import com.powsybl.computation.ComputationManager; +import com.powsybl.iidm.network.Network; +import com.powsybl.iidm.network.test.EurostagTutorialExample1Factory; +import com.powsybl.loadflow.validation.ValidationConfig; +import com.powsybl.tools.ToolRunningContext; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.MockedStatic; + +import java.io.PrintStream; +import java.nio.file.FileSystem; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +/** + * + * @author Samir Romdhani {@literal } + */ +class ExtensionsValidationTest { + + ExtensionsValidation extensionsValidation = new ExtensionsValidation(); + private final Network network = EurostagTutorialExample1Factory.create(); + + @Test + void getExtensionsShouldSucceed() { + assertEquals(1, ExtensionsValidation.getExtensions().size()); + } + + @Test + void getExtensionsNamesShouldReturnExistingExtension() { + assertEquals(List.of("extensionValidationMock1"), ExtensionsValidation.getExtensionsNames()); + } + + @Test + void getExtensionShouldReturnExistingExtension() { + assertTrue(ExtensionsValidation.getExtension("extensionValidationMock1").isPresent()); + assertEquals("extensionValidationMock1", ExtensionsValidation.getExtension("extensionValidationMock1").get().getName()); + } + + @Test + void runExtensionValidationsShouldSucceed() { + //Given + ValidationConfig config = ValidationConfig.load(); + ToolRunningContext context = new ToolRunningContext(mock(PrintStream.class), mock(PrintStream.class), mock(FileSystem.class), mock(ComputationManager.class), mock(ComputationManager.class)); + + ExtensionValidation extension1 = mock(ExtensionValidation.class); + ExtensionValidation extension2 = mock(ExtensionValidation.class); + + when(extension1.getName()).thenReturn("ExtensionName1"); + when(extension1.check(any(), any())).thenReturn(true); + + when(extension2.getName()).thenReturn("ExtensionName2"); + when(extension2.check(any(), any())).thenReturn(false); + + try (MockedStatic mocked = mockStatic(ExtensionsValidation.class)) { + mocked.when(ExtensionsValidation::getExtensions).thenReturn(List.of(extension1, extension2)); + // When + extensionsValidation.runExtensionValidations(network, config, context); + // Then + ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); + verify(context.getOutputStream(), times(2)).println(captor.capture()); + List messages = captor.getAllValues(); + assertTrue(messages.get(0).contains("validation type: Extension/ExtensionName1 - result: success")); + assertTrue(messages.get(1).contains("validation type: Extension/ExtensionName2 - result: fail")); + // check method was called on extensions + verify(extension1, times(1)).check(network, config); + verify(extension2, times(1)).check(network, config); + } + } +} diff --git a/loadflow/loadflow-validation/src/test/resources/network.xiidm b/loadflow/loadflow-validation/src/test/resources/network.xiidm new file mode 100644 index 00000000000..751d77db05b --- /dev/null +++ b/loadflow/loadflow-validation/src/test/resources/network.xiidm @@ -0,0 +1,3 @@ + + +