From 61d0dbdebf6a7eeb6d7bd1612248bfec63c13bb7 Mon Sep 17 00:00:00 2001 From: Riad Benradi Date: Thu, 2 Apr 2026 15:29:33 +0200 Subject: [PATCH 1/3] Initial implementation of mixed-mode security analysis Adds a new provider that orchestrates a two-pass security analysis. It first runs a static analysis on all contingencies. Based on configurable criteria (e.g., `NOT_CONVERGED`, `SPS_TRIGGERED`), it can then run a dynamic analysis on a subset of contingencies that require further investigation. Signed-off-by: Riad Benradi --- .../mixed-security-analysis/pom.xml | 107 +++++++ .../analysis/MixedSecurityAnalysis.java | 209 +++++++++++++ .../MixedSecurityAnalysisProvider.java | 108 +++++++ .../criteria/AnalysisSwitchCriteria.java | 65 ++++ .../analysis/criteria/SwitchDecision.java | 48 +++ .../MixedModeParametersExtension.java | 68 ++++ .../MixedSecurityAnalysisProviderTest.java | 48 +++ .../analysis/MixedSecurityAnalysisTest.java | 293 ++++++++++++++++++ .../criteria/AnalysisSwitchCriteriaTest.java | 144 +++++++++ .../analysis/criteria/SwitchDecisionTest.java | 43 +++ .../MixedModeParametersExtensionTest.java | 57 ++++ security-analysis/pom.xml | 1 + 12 files changed, 1191 insertions(+) create mode 100644 security-analysis/mixed-security-analysis/pom.xml create mode 100644 security-analysis/mixed-security-analysis/src/main/java/com/powsybl/mixed/security/analysis/MixedSecurityAnalysis.java create mode 100644 security-analysis/mixed-security-analysis/src/main/java/com/powsybl/mixed/security/analysis/MixedSecurityAnalysisProvider.java create mode 100644 security-analysis/mixed-security-analysis/src/main/java/com/powsybl/mixed/security/analysis/criteria/AnalysisSwitchCriteria.java create mode 100644 security-analysis/mixed-security-analysis/src/main/java/com/powsybl/mixed/security/analysis/criteria/SwitchDecision.java create mode 100644 security-analysis/mixed-security-analysis/src/main/java/com/powsybl/mixed/security/analysis/parameters/MixedModeParametersExtension.java create mode 100644 security-analysis/mixed-security-analysis/src/test/java/com/powsybl/mixed/security/analysis/MixedSecurityAnalysisProviderTest.java create mode 100644 security-analysis/mixed-security-analysis/src/test/java/com/powsybl/mixed/security/analysis/MixedSecurityAnalysisTest.java create mode 100644 security-analysis/mixed-security-analysis/src/test/java/com/powsybl/mixed/security/analysis/criteria/AnalysisSwitchCriteriaTest.java create mode 100644 security-analysis/mixed-security-analysis/src/test/java/com/powsybl/mixed/security/analysis/criteria/SwitchDecisionTest.java create mode 100644 security-analysis/mixed-security-analysis/src/test/java/com/powsybl/mixed/security/analysis/parameters/MixedModeParametersExtensionTest.java diff --git a/security-analysis/mixed-security-analysis/pom.xml b/security-analysis/mixed-security-analysis/pom.xml new file mode 100644 index 00000000000..f77a3a564b2 --- /dev/null +++ b/security-analysis/mixed-security-analysis/pom.xml @@ -0,0 +1,107 @@ + + + 4.0.0 + + com.powsybl + powsybl-security-analysis + 7.3.0-SNAPSHOT + + + mixed-security-analysis + + + 21 + 21 + UTF-8 + + + + + com.fasterxml.jackson.core + jackson-databind + + + com.powsybl + powsybl-security-analysis-api + + + com.powsybl + powsybl-open-loadflow + 2.2.0 + + + com.powsybl + powsybl-dynaflow + 3.2.0-SNAPSHOT + + + com.powsybl + powsybl-iidm-serde + ${project.version} + + + + com.google.jimfs + jimfs + test + + + org.junit.jupiter + junit-jupiter + test + + + org.assertj + assertj-core + test + + + org.mockito + mockito-core + test + + + org.slf4j + log4j-over-slf4j + test + + + org.slf4j + slf4j-simple + test + + + ${project.groupId} + powsybl-commons-test + ${project.version} + test + + + ${project.groupId} + powsybl-config-test + ${project.version} + test + + + ${project.groupId} + powsybl-iidm-impl + ${project.version} + test + + + ${project.groupId} + powsybl-iidm-test + ${project.version} + test + + + ${project.groupId} + powsybl-tools-test + ${project.version} + test + + + + \ No newline at end of file diff --git a/security-analysis/mixed-security-analysis/src/main/java/com/powsybl/mixed/security/analysis/MixedSecurityAnalysis.java b/security-analysis/mixed-security-analysis/src/main/java/com/powsybl/mixed/security/analysis/MixedSecurityAnalysis.java new file mode 100644 index 00000000000..614f9289e90 --- /dev/null +++ b/security-analysis/mixed-security-analysis/src/main/java/com/powsybl/mixed/security/analysis/MixedSecurityAnalysis.java @@ -0,0 +1,209 @@ +package com.powsybl.mixed.security.analysis; + +import com.powsybl.contingency.ContingenciesProvider; +import com.powsybl.contingency.Contingency; +import com.powsybl.iidm.network.Network; +import com.powsybl.mixed.security.analysis.criteria.AnalysisSwitchCriteria; +import com.powsybl.mixed.security.analysis.criteria.SwitchDecision; +import com.powsybl.mixed.security.analysis.parameters.MixedModeParametersExtension; +import com.powsybl.security.SecurityAnalysisProvider; +import com.powsybl.security.SecurityAnalysisReport; +import com.powsybl.security.SecurityAnalysisResult; +import com.powsybl.security.SecurityAnalysisRunParameters; +import com.powsybl.security.results.PostContingencyResult; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.Map; +import java.util.ServiceLoader; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +/** + * Business logic for mixed-mode security analysis: static pass on all contingencies, + * then dynamic pass on those that meet the switch criteria, results merged. + * + * @author Riad Benradi {@literal } + */ +public class MixedSecurityAnalysis { + private static final Logger LOGGER = LoggerFactory.getLogger(MixedSecurityAnalysis.class); + + private final Network network; + private final String workingVariantId; + private final ContingenciesProvider contingenciesProvider; + private final SecurityAnalysisRunParameters runParameters; + private final MixedModeParametersExtension extension; + private final List providers; + + public MixedSecurityAnalysis(Network network, String workingVariantId, ContingenciesProvider contingenciesProvider, + SecurityAnalysisRunParameters runParameters, MixedModeParametersExtension extension) { + this(network, workingVariantId, contingenciesProvider, runParameters, extension, null); + } + + public MixedSecurityAnalysis(Network network, String workingVariantId, ContingenciesProvider contingenciesProvider, + SecurityAnalysisRunParameters runParameters, MixedModeParametersExtension extension, + List providers) { + this.network = network; + this.workingVariantId = workingVariantId; + this.contingenciesProvider = contingenciesProvider; + this.runParameters = runParameters; + this.extension = extension; + this.providers = providers; + } + + /** + * Executes the full mixed-mode analysis workflow. + */ + public CompletableFuture run() { + LOGGER.info("Starting mixed-mode security analysis"); + LOGGER.debug("Static simulator: {}, Dynamic simulator: {}", + extension.getStaticSimulator(), extension.getDynamicSimulator()); + + // Step 1: Get all contingencies + List allContingencies = contingenciesProvider.getContingencies(network); + LOGGER.info("Total contingencies to analyze: {}", allContingencies.size()); + + // Step 2: Run static analysis + String staticProviderName = extension.getStaticSimulator(); + SecurityAnalysisProvider staticProvider = findProvider(staticProviderName); + + CompletableFuture staticAnalysisFuture = staticProvider.run( + network, workingVariantId, contingenciesProvider, runParameters); + + // Step 3: Chain dynamic analysis based on static results + return staticAnalysisFuture.thenCompose(staticReport -> { + LOGGER.info("Static analysis completed"); + + // Evaluate switch criteria + AnalysisSwitchCriteria switchCriteria = new AnalysisSwitchCriteria(extension); + List contingenciesToRunDynamic = identifyDynamicContingencies( + staticReport.getResult(), switchCriteria); + + LOGGER.info("Contingencies requiring dynamic analysis: {}", contingenciesToRunDynamic.size()); + + // If no contingencies need dynamic analysis, return static results + if (contingenciesToRunDynamic.isEmpty()) { + LOGGER.info("No contingencies require dynamic analysis, returning static results"); + return CompletableFuture.completedFuture(staticReport); + } + + // Run dynamic analysis on filtered contingencies + return runDynamicAnalysis(contingenciesToRunDynamic, allContingencies) + .thenApply(dynamicReport -> mergeResults(staticReport, dynamicReport)); + }).exceptionally(ex -> { + LOGGER.error("Error during mixed-mode security analysis", ex); + return new SecurityAnalysisReport(SecurityAnalysisResult.empty()); + }); + } + + /** + * Identifies which contingencies should be analyzed with the dynamic simulator. + */ + private List identifyDynamicContingencies(SecurityAnalysisResult staticResult, + AnalysisSwitchCriteria switchCriteria) { + return staticResult.getPostContingencyResults().stream() + .filter(result -> shouldRunDynamic(result, switchCriteria)) + .map(result -> result.getContingency().getId()) + .collect(Collectors.toList()); + } + + /** + * Evaluates if a contingency result should trigger dynamic analysis. + */ + private boolean shouldRunDynamic(PostContingencyResult result, AnalysisSwitchCriteria switchCriteria) { + SwitchDecision decision = switchCriteria.evaluate(result); + LOGGER.debug("Contingency {} - Switch decision: {}", + result.getContingency().getId(), decision.getReason()); + return decision.shouldSwitch(); + } + + /** + * Runs dynamic analysis on a subset of contingencies. + * Uses the already-resolved {@code allContingencies} list to avoid a second provider call. + */ + private CompletableFuture runDynamicAnalysis(List contingencyIds, + List allContingencies) { + LOGGER.info("Starting dynamic analysis pass for {} contingencies", contingencyIds.size()); + + ContingenciesProvider filteredProvider = (net) -> + allContingencies.stream() + .filter(c -> contingencyIds.contains(c.getId())) + .collect(Collectors.toList()); + + String dynamicProviderName = extension.getDynamicSimulator(); + SecurityAnalysisProvider dynamicProvider = findProvider(dynamicProviderName); + + return dynamicProvider.run(network, workingVariantId, filteredProvider, runParameters); + } + + /** + * Merges static and dynamic analysis results. + * Strategy: For each contingency, keep the result from the last (most relevant) analysis: + * - If analyzed in dynamic pass: use dynamic result + * - Otherwise: use static result + */ + private SecurityAnalysisReport mergeResults(SecurityAnalysisReport staticReport, + SecurityAnalysisReport dynamicReport) { + LOGGER.info("Merging static and dynamic analysis results"); + + SecurityAnalysisResult staticResult = staticReport.getResult(); + SecurityAnalysisResult dynamicResult = dynamicReport.getResult(); + + // Create a map of dynamic results by contingency ID + Map dynamicResultsMap = dynamicResult.getPostContingencyResults() + .stream() + .collect(Collectors.toMap(r -> r.getContingency().getId(), r -> r)); + + // Merge: use dynamic result if available, otherwise use static + List mergedResults = staticResult.getPostContingencyResults() + .stream() + .map(staticResultItem -> { + String contingencyId = staticResultItem.getContingency().getId(); + if (dynamicResultsMap.containsKey(contingencyId)) { + LOGGER.debug("Using dynamic result for contingency {}", contingencyId); + return dynamicResultsMap.get(contingencyId); + } else { + LOGGER.debug("Using static result for contingency {}", contingencyId); + return staticResultItem; + } + }) + .collect(Collectors.toList()); + + // Create final result + SecurityAnalysisResult finalResult = new SecurityAnalysisResult( + staticResult.getPreContingencyResult(), + mergedResults, + staticResult.getOperatorStrategyResults()); + + if (staticResult.getNetworkMetadata() != null) { + finalResult.setNetworkMetadata(staticResult.getNetworkMetadata()); + } + + LOGGER.info("Merge complete: {} post-contingency results", mergedResults.size()); + return new SecurityAnalysisReport(finalResult) + .setLogBytes(staticReport.getLogBytes().orElse(null)); + } + + /** + * Finds a security analysis provider by name using ServiceLoader. + */ + private SecurityAnalysisProvider findProvider(String providerName) { + Map allProviders = StreamSupport.stream(ServiceLoader.load(SecurityAnalysisProvider.class).spliterator(), false) + .collect(Collectors.toMap(SecurityAnalysisProvider::getName, p -> p)); + + if (providers != null) { + providers.forEach(p -> allProviders.put(p.getName(), p)); + } + + SecurityAnalysisProvider foundProvider = allProviders.get(providerName); + + if (foundProvider == null) { + throw new IllegalArgumentException( + "Security analysis provider '" + providerName + "' not found. " + + "Available providers: " + String.join(", ", allProviders.keySet())); + } + return foundProvider; + } +} \ No newline at end of file diff --git a/security-analysis/mixed-security-analysis/src/main/java/com/powsybl/mixed/security/analysis/MixedSecurityAnalysisProvider.java b/security-analysis/mixed-security-analysis/src/main/java/com/powsybl/mixed/security/analysis/MixedSecurityAnalysisProvider.java new file mode 100644 index 00000000000..86fe0df5963 --- /dev/null +++ b/security-analysis/mixed-security-analysis/src/main/java/com/powsybl/mixed/security/analysis/MixedSecurityAnalysisProvider.java @@ -0,0 +1,108 @@ +package com.powsybl.mixed.security.analysis; + +import com.google.auto.service.AutoService; +import com.powsybl.commons.config.PlatformConfig; +import com.powsybl.commons.extensions.Extension; +import com.powsybl.contingency.ContingenciesProvider; +import com.powsybl.iidm.network.Network; +import com.powsybl.mixed.security.analysis.parameters.MixedModeParametersExtension; +import com.powsybl.security.SecurityAnalysisParameters; +import com.powsybl.security.SecurityAnalysisProvider; +import com.powsybl.security.SecurityAnalysisReport; +import com.powsybl.security.SecurityAnalysisRunParameters; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +/** + * {@link SecurityAnalysisProvider} orchestrating a static pass followed by a selective + * dynamic pass, discovered automatically via {@link java.util.ServiceLoader}. + * + * @author Riad Benradi {@literal } + */ + +@AutoService({SecurityAnalysisProvider.class}) +public class MixedSecurityAnalysisProvider implements SecurityAnalysisProvider { + + private static final Logger LOGGER = LoggerFactory.getLogger(MixedSecurityAnalysisProvider.class); + + private static final String PROVIDER_NAME = "MixedSecurityAnalysis"; + + /** @return {@code "MixedSecurityAnalysis"} */ + @Override + public String getName() { + return PROVIDER_NAME; + } + + /** @return the provider version */ + @Override + public String getVersion() { + return "1.0.0-SNAPSHOT"; + } + + /** + * Reads {@link MixedModeParametersExtension} from {@code runParameters} and delegates + * to {@link MixedSecurityAnalysis}. + * + * @throws IllegalArgumentException if {@link MixedModeParametersExtension} is absent + */ + @Override + public CompletableFuture run(Network network, + String workingVariantId, + ContingenciesProvider contingenciesProvider, + SecurityAnalysisRunParameters runParameters) { + + Objects.requireNonNull(network); + Objects.requireNonNull(workingVariantId); + Objects.requireNonNull(contingenciesProvider); + Objects.requireNonNull(runParameters); + LOGGER.info("Starting mixed-mode security analysis for network: {}", network.getId()); + + MixedModeParametersExtension extension = runParameters.getSecurityAnalysisParameters().getExtension(MixedModeParametersExtension.class); + if(extension == null) { + + LOGGER.warn("MixedModeParametersExtension configuration is missing from SecurityAnalysisRunParameters, using defaults."); + extension = (MixedModeParametersExtension) loadSpecificParameters(PlatformConfig.defaultConfig()).orElseThrow(() -> + new IllegalArgumentException("Failed to load MixedModeParametersExtension configuration")); + } + LOGGER.debug("Configuration loaded - Static simulator: {}, Dynamic simulator: {}, " + + "Switch criteria: {}", + extension.getStaticSimulator(), + extension.getDynamicSimulator(), + extension.getSwitchCriteria()); + + MixedSecurityAnalysis analysis = new MixedSecurityAnalysis( + network, + workingVariantId, + contingenciesProvider, + runParameters, + extension + ); + + return analysis.run(); + } + + @Override + public Optional> loadSpecificParameters(PlatformConfig config) { + MixedModeParametersExtension ext = new MixedModeParametersExtension(); + + // Default values — used if the key is absent from the config file + ext.setStaticSimulator("load-flow"); + ext.setDynamicSimulator("dynaflow"); + ext.setSwitchCriteria(List.of("NOT_CONVERGED")); + + // Override with values from config file if the section exists + // Reads from a YAML block named "mixed-mode-analysis" + config.getOptionalModuleConfig(MixedModeParametersExtension.NAME).ifPresent(module -> { + ext.setStaticSimulator(module.getStringProperty("static-simulator", ext.getStaticSimulator())); + ext.setDynamicSimulator(module.getStringProperty("dynamic-simulator", ext.getDynamicSimulator())); + ext.setSwitchCriteria(module.getStringListProperty("switch-criteria", ext.getSwitchCriteria())); + }); + + return Optional.of(ext); + } +} diff --git a/security-analysis/mixed-security-analysis/src/main/java/com/powsybl/mixed/security/analysis/criteria/AnalysisSwitchCriteria.java b/security-analysis/mixed-security-analysis/src/main/java/com/powsybl/mixed/security/analysis/criteria/AnalysisSwitchCriteria.java new file mode 100644 index 00000000000..49d28695b0a --- /dev/null +++ b/security-analysis/mixed-security-analysis/src/main/java/com/powsybl/mixed/security/analysis/criteria/AnalysisSwitchCriteria.java @@ -0,0 +1,65 @@ +package com.powsybl.mixed.security.analysis.criteria; + +import com.powsybl.mixed.security.analysis.parameters.MixedModeParametersExtension; +import com.powsybl.security.PostContingencyComputationStatus; +import com.powsybl.security.results.PostContingencyResult; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Objects; +/** + * Evaluates whether a contingency result should trigger a switch to dynamic analysis. + * + * @author Riad Benradi {@literal } + */ +public class AnalysisSwitchCriteria { + private static final Logger LOGGER = LoggerFactory.getLogger(AnalysisSwitchCriteria.class); + private final MixedModeParametersExtension extension; + public AnalysisSwitchCriteria(MixedModeParametersExtension extension) { + this.extension = Objects.requireNonNull(extension); + } + public SwitchDecision evaluate(PostContingencyResult result) { + Objects.requireNonNull(result); + if (extension.getSwitchCriteria() == null || extension.getSwitchCriteria().isEmpty()) { + LOGGER.debug("No switch criteria defined, keeping current result"); + return new SwitchDecision(false, "No criteria defined"); + } + for (String criterion : extension.getSwitchCriteria()) { + if (evaluateCriterion(result, criterion)) { + return new SwitchDecision(true, "Criterion '" + criterion + "' met"); + } + } + return new SwitchDecision(false, "No criteria met"); + } + private boolean evaluateCriterion(PostContingencyResult result, String criterion) { + return switch (criterion.toUpperCase()) { + case "FAILED" -> evaluateNonConvergence(result); + case "LIMIT_VIOLATIONS" -> evaluateLimitViolations(result); + case "SPS_TRIGGERED" -> evaluateSpsTriggered(result); + default -> { + LOGGER.warn("Unknown criterion: {}", criterion); + yield false; + } + }; + } + private boolean evaluateNonConvergence(PostContingencyResult result) { + boolean converged = result.getStatus() == PostContingencyComputationStatus.CONVERGED; + if (!converged) { + LOGGER.debug("Non-convergence detected for contingency {}", + result.getContingency().getId()); + } + return !converged; + } + + private boolean evaluateLimitViolations(PostContingencyResult result) { + boolean hasViolations = !result.getLimitViolationsResult().getLimitViolations().isEmpty(); + if (hasViolations) { + LOGGER.debug("Limit violations detected for contingency {}", + result.getContingency().getId()); + } + return hasViolations; + } + private boolean evaluateSpsTriggered(PostContingencyResult result) { + return false; + } +} diff --git a/security-analysis/mixed-security-analysis/src/main/java/com/powsybl/mixed/security/analysis/criteria/SwitchDecision.java b/security-analysis/mixed-security-analysis/src/main/java/com/powsybl/mixed/security/analysis/criteria/SwitchDecision.java new file mode 100644 index 00000000000..6c67f5b0e63 --- /dev/null +++ b/security-analysis/mixed-security-analysis/src/main/java/com/powsybl/mixed/security/analysis/criteria/SwitchDecision.java @@ -0,0 +1,48 @@ +package com.powsybl.mixed.security.analysis.criteria; + +import java.util.Objects; + +/** + * Represents a decision on whether to switch to dynamic analysis for a contingency. + * + * @author Riad Benradi {@literal } + */ +public class SwitchDecision { + private final boolean shouldSwitch; + private final String reason; + + public SwitchDecision(boolean shouldSwitch, String reason) { + this.shouldSwitch = shouldSwitch; + this.reason = Objects.requireNonNull(reason); + } + + public boolean shouldSwitch() { + return shouldSwitch; + } + + public String getReason() { + return reason; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SwitchDecision that = (SwitchDecision) o; + return shouldSwitch == that.shouldSwitch && Objects.equals(reason, that.reason); + } + + @Override + public int hashCode() { + return Objects.hash(shouldSwitch, reason); + } + + @Override + public String toString() { + return "SwitchDecision{" + + "shouldSwitch=" + shouldSwitch + + ", reason='" + reason + '\'' + + '}'; + } +} + diff --git a/security-analysis/mixed-security-analysis/src/main/java/com/powsybl/mixed/security/analysis/parameters/MixedModeParametersExtension.java b/security-analysis/mixed-security-analysis/src/main/java/com/powsybl/mixed/security/analysis/parameters/MixedModeParametersExtension.java new file mode 100644 index 00000000000..4dac2c74c85 --- /dev/null +++ b/security-analysis/mixed-security-analysis/src/main/java/com/powsybl/mixed/security/analysis/parameters/MixedModeParametersExtension.java @@ -0,0 +1,68 @@ +package com.powsybl.mixed.security.analysis.parameters; + +import com.powsybl.commons.extensions.AbstractExtension; +import com.powsybl.security.SecurityAnalysisParameters; + +import java.util.List; + +/** + * This class contains configuration parameters specific to + * mixed-mode security analysis. + * It is designed to be used as an extension of PowSybl's standard parameters. + * PowSybl will handle reading a configuration file (e.g., YAML) and populate + * the fields of this class automatically. + */ +public class MixedModeParametersExtension extends AbstractExtension { + + public static final String NAME = "mixed-mode-analysis"; + /** + * The name of the static simulator to use for the first pass. + * For example: "load-flow" + */ + private String staticSimulator; + + /** + * The name of the dynamic simulator to use for the second pass (for complex cases). + * For example: "dynaflow" + */ + private String dynamicSimulator; + + /** + * The list of criteria that trigger a switch to the dynamic simulator. + * Possible values: "NON_CONVERGENCE", "SEVERITY_THRESHOLD". + */ + private List switchCriteria; + + // --- Getters and Setters --- + // These are necessary so that configuration tools like Jackson (used by PowSybl) + // can populate the fields of this object. + + public String getStaticSimulator() { + return staticSimulator; + } + + public void setStaticSimulator(String staticSimulator) { + this.staticSimulator = staticSimulator; + } + + public String getDynamicSimulator() { + return dynamicSimulator; + } + + public void setDynamicSimulator(String dynamicSimulator) { + this.dynamicSimulator = dynamicSimulator; + } + + public List getSwitchCriteria() { + return switchCriteria; + } + + public void setSwitchCriteria(List switchCriteria) { + this.switchCriteria = switchCriteria; + } + + @Override + public String getName() { + return NAME; + } +} diff --git a/security-analysis/mixed-security-analysis/src/test/java/com/powsybl/mixed/security/analysis/MixedSecurityAnalysisProviderTest.java b/security-analysis/mixed-security-analysis/src/test/java/com/powsybl/mixed/security/analysis/MixedSecurityAnalysisProviderTest.java new file mode 100644 index 00000000000..f3e0a3ced5e --- /dev/null +++ b/security-analysis/mixed-security-analysis/src/test/java/com/powsybl/mixed/security/analysis/MixedSecurityAnalysisProviderTest.java @@ -0,0 +1,48 @@ +package com.powsybl.mixed.security.analysis; + +import com.powsybl.contingency.ContingenciesProvider; +import com.powsybl.iidm.network.Network; +import com.powsybl.mixed.security.analysis.parameters.MixedModeParametersExtension; +import com.powsybl.security.SecurityAnalysisParameters; +import com.powsybl.security.SecurityAnalysisRunParameters; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +class MixedSecurityAnalysisProviderTest { + @Mock + private Network network; + @Mock + private ContingenciesProvider contingenciesProvider; + @Mock + private SecurityAnalysisRunParameters runParameters; + private MixedSecurityAnalysisProvider provider; + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + provider = new MixedSecurityAnalysisProvider(); + } + @Test + void testProviderName() { + assertEquals("MixedSecurityAnalysis", provider.getName()); + } + @Test + void testProviderVersion() { + String version = provider.getVersion(); + assertNotNull(version); + assertFalse(version.isEmpty()); + } + @Test + void testRunMissingExtensionThrows() { + SecurityAnalysisParameters mockSaParams = mock(SecurityAnalysisParameters.class); + when(runParameters.getSecurityAnalysisParameters()).thenReturn(mockSaParams); + when(mockSaParams.getExtension(MixedModeParametersExtension.class)).thenReturn(null); + assertThrows(Exception.class, () -> + provider.run(network, "main", contingenciesProvider, runParameters).join() + ); + } +} diff --git a/security-analysis/mixed-security-analysis/src/test/java/com/powsybl/mixed/security/analysis/MixedSecurityAnalysisTest.java b/security-analysis/mixed-security-analysis/src/test/java/com/powsybl/mixed/security/analysis/MixedSecurityAnalysisTest.java new file mode 100644 index 00000000000..25a81fe31a0 --- /dev/null +++ b/security-analysis/mixed-security-analysis/src/test/java/com/powsybl/mixed/security/analysis/MixedSecurityAnalysisTest.java @@ -0,0 +1,293 @@ +package com.powsybl.mixed.security.analysis; + +import com.powsybl.commons.report.ReportNode; +import com.powsybl.commons.report.TypedValue; +import com.powsybl.contingency.ContingenciesProvider; +import com.powsybl.contingency.Contingency; +import com.powsybl.iidm.network.Network; +import com.powsybl.iidm.network.test.EurostagTutorialExample1Factory; +import com.powsybl.loadflow.LoadFlowParameters; +import com.powsybl.mixed.security.analysis.parameters.MixedModeParametersExtension; +import com.powsybl.security.*; +import com.powsybl.security.results.ConnectivityResult; +import com.powsybl.security.results.NetworkResult; +import com.powsybl.security.results.PostContingencyResult; +import com.powsybl.security.results.PreContingencyResult; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import static com.powsybl.loadflow.LoadFlowResult.ComponentResult.Status.CONVERGED; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +class MixedSecurityAnalysisTest { + @Mock + private Network network; + @Mock + private ContingenciesProvider contingenciesProvider; + @Mock + private SecurityAnalysisRunParameters runParameters; + @Mock + private SecurityAnalysisParameters securityAnalysisParameters; + @Mock + private LoadFlowParameters loadFlowParameters; + @Mock + private SecurityAnalysisProvider staticProvider; + @Mock + private SecurityAnalysisProvider dynamicProvider; + private MixedSecurityAnalysis mixedAnalysis; + private Contingency contingency1; + private Contingency contingency2; + + @BeforeEach + void setUp() { + ReportNode reportNode = ReportNode.newRootReportNode() + .withSeverity(TypedValue.TRACE_SEVERITY) + .withMessageTemplate("test") + .build(); + MockitoAnnotations.openMocks(this); + runParameters.setReportNode(reportNode); + when(runParameters.getSecurityAnalysisParameters()).thenReturn(securityAnalysisParameters); + when(securityAnalysisParameters.getLoadFlowParameters()).thenReturn(loadFlowParameters); + when(staticProvider.getName()).thenReturn("OpenLoadFlow"); + when(dynamicProvider.getName()).thenReturn("dynaFlow"); + MixedModeParametersExtension extension = new MixedModeParametersExtension(); + extension.setStaticSimulator("OpenLoadFlow"); + extension.setDynamicSimulator("dynaFlow"); + extension.setSwitchCriteria(Collections.singletonList("FAILED")); + contingency1 = mock(Contingency.class); + when(contingency1.getId()).thenReturn("contingency-1"); + contingency2 = mock(Contingency.class); + when(contingency2.getId()).thenReturn("contingency-2"); + mixedAnalysis = new MixedSecurityAnalysis(network, "main", contingenciesProvider, + runParameters, extension, + Arrays.asList(staticProvider, dynamicProvider)); + } + + @Test + void testRunSuccessfulNoSwitchNeeded() { + List allContingencies = Arrays.asList(contingency1, contingency2); + when(contingenciesProvider.getContingencies(network)).thenReturn(allContingencies); + PostContingencyResult staticResult1 = createPostContingencyResult(contingency1, true, 0); + PostContingencyResult staticResult2 = createPostContingencyResult(contingency2, true, 0); + PreContingencyResult preResult = new PreContingencyResult(CONVERGED, + new LimitViolationsResult(Collections.emptyList()), + NetworkResult.empty(), Double.NaN); + SecurityAnalysisReport staticReport = new SecurityAnalysisReport( + new SecurityAnalysisResult(preResult, + Arrays.asList(staticResult1, staticResult2), + Collections.emptyList()) + ); + when(staticProvider.run(any(), any(), any(), any())) + .thenReturn(CompletableFuture.completedFuture(staticReport)); + + SecurityAnalysisReport report = mixedAnalysis.run().join(); + + verify(staticProvider).run(network, "main", contingenciesProvider, runParameters); + verify(dynamicProvider, never()).run(any(), any(), any(), any()); + assertEquals(2, report.getResult().getPostContingencyResults().size()); + } + + @Test + void testRunWithSwitchTooDynamic() { + List allContingencies = Arrays.asList(contingency1, contingency2); + when(contingenciesProvider.getContingencies(network)).thenReturn(allContingencies); + // contingency1 fails static → triggers switch to dynamic + PostContingencyResult staticResult1 = createPostContingencyResult(contingency1, false, 0); + PostContingencyResult staticResult2 = createPostContingencyResult(contingency2, true, 0); + PreContingencyResult preResult = new PreContingencyResult(CONVERGED, + new LimitViolationsResult(Collections.emptyList()), + NetworkResult.empty(), Double.NaN); + SecurityAnalysisReport staticReport = new SecurityAnalysisReport( + new SecurityAnalysisResult(preResult, + Arrays.asList(staticResult1, staticResult2), + Collections.emptyList()) + ); + PostContingencyResult dynamicResult1 = createPostContingencyResult(contingency1, true, 0); + SecurityAnalysisReport dynamicReport = new SecurityAnalysisReport( + new SecurityAnalysisResult(preResult, + Collections.singletonList(dynamicResult1), + Collections.emptyList()) + ); + when(staticProvider.run(any(), any(), any(), any())) + .thenReturn(CompletableFuture.completedFuture(staticReport)); + when(dynamicProvider.run(any(), any(), any(), any())) + .thenReturn(CompletableFuture.completedFuture(dynamicReport)); + + SecurityAnalysisReport report = mixedAnalysis.run().join(); + + verify(staticProvider).run(network, "main", contingenciesProvider, runParameters); + verify(dynamicProvider).run(any(), any(), any(), any()); + assertEquals(2, report.getResult().getPostContingencyResults().size()); + } + + @Test + void testRunMergesResultsProperly() { + List allContingencies = Arrays.asList(contingency1, contingency2); + when(contingenciesProvider.getContingencies(network)).thenReturn(allContingencies); + // contingency1: static FAILED (1 violation), dynamic CONVERGED (2 violations) + PostContingencyResult staticResult1 = createPostContingencyResult(contingency1, false, 1); + PostContingencyResult staticResult2 = createPostContingencyResult(contingency2, true, 0); + PreContingencyResult preResult = new PreContingencyResult(CONVERGED, + new LimitViolationsResult(Collections.emptyList()), + NetworkResult.empty(), Double.NaN); + SecurityAnalysisReport staticReport = new SecurityAnalysisReport( + new SecurityAnalysisResult(preResult, + Arrays.asList(staticResult1, staticResult2), + Collections.emptyList()) + ); + PostContingencyResult dynamicResult1 = createPostContingencyResult(contingency1, true, 2); + SecurityAnalysisReport dynamicReport = new SecurityAnalysisReport( + new SecurityAnalysisResult(preResult, + Collections.singletonList(dynamicResult1), + Collections.emptyList()) + ); + when(staticProvider.run(any(), any(), any(), any())) + .thenReturn(CompletableFuture.completedFuture(staticReport)); + when(dynamicProvider.run(any(), any(), any(), any())) + .thenReturn(CompletableFuture.completedFuture(dynamicReport)); + + SecurityAnalysisReport report = mixedAnalysis.run().join(); + + List results = report.getResult().getPostContingencyResults(); + assertEquals(2, results.size()); + PostContingencyResult result1 = results.stream() + .filter(r -> r.getContingency().getId().equals("contingency-1")) + .findFirst() + .orElseThrow(); + // dynamic result overrides static: CONVERGED status and 2 violations win + assertSame(PostContingencyComputationStatus.CONVERGED, result1.getStatus()); + assertEquals(2, result1.getLimitViolationsResult().getLimitViolations().size()); + } + + @Test + void testRunHandlesException() { + List allContingencies = Collections.singletonList(contingency1); + when(contingenciesProvider.getContingencies(network)).thenReturn(allContingencies); + when(staticProvider.run(any(), any(), any(), any())) + .thenReturn(CompletableFuture.failedFuture(new RuntimeException("Analysis failed"))); + + SecurityAnalysisReport report = mixedAnalysis.run().join(); + + assertNotNull(report); + assertEquals(0, report.getResult().getPostContingencyResults().size()); + } + + /** + * Integration test covering both workflow paths: + * Phase 1 — real OpenLoadFlow, both contingencies converge, DynaFlow never called. + * Phase 2 — mock providers, static FAILED triggers dynamic dispatch, results merged. + */ + @Test + void testSimpleMixedSecurityAnalysis() { + Network network = EurostagTutorialExample1Factory.create(); + Contingency contingencyLine1 = Contingency.line("NHV1_NHV2_1"); + Contingency contingencyLine2 = Contingency.line("NHV1_NHV2_2"); + when(contingenciesProvider.getContingencies(network)) + .thenReturn(Arrays.asList(contingencyLine1, contingencyLine2)); + + MixedModeParametersExtension ext = new MixedModeParametersExtension(); + ext.setStaticSimulator("OpenLoadFlow"); + ext.setDynamicSimulator("DynaFlow"); + ext.setSwitchCriteria(Collections.singletonList("FAILED")); + + SecurityAnalysisProvider mockDynaFlow = mock(SecurityAnalysisProvider.class); + when(mockDynaFlow.getName()).thenReturn("DynaFlow"); + SecurityAnalysisRunParameters params = SecurityAnalysisRunParameters.getDefault(); + + // Phase 1: real OpenLoadFlow — both contingencies converge, no dynamic switch + SecurityAnalysisReport staticOnlyReport = new MixedSecurityAnalysis( + network, "InitialState", contingenciesProvider, params, ext, + Collections.singletonList(mockDynaFlow)).run().join(); + + PreContingencyResult preResult = staticOnlyReport.getResult().getPreContingencyResult(); + assertNotNull(preResult); + assertEquals(CONVERGED, preResult.getStatus()); + assertTrue(preResult.getLimitViolationsResult().getLimitViolations().isEmpty()); + + List staticResults = staticOnlyReport.getResult().getPostContingencyResults(); + assertEquals(2, staticResults.size()); + staticResults.forEach(r -> { + assertSame(PostContingencyComputationStatus.CONVERGED, r.getStatus()); + assertTrue(r.getLimitViolationsResult().getLimitViolations().isEmpty()); + }); + verify(mockDynaFlow, never()).run(any(), any(), any(), any()); + + // Phase 2: static FAILED on line1 → DynaFlow dispatched for line1 only, results merged + PreContingencyResult mockPreResult = new PreContingencyResult( + CONVERGED, new LimitViolationsResult(Collections.emptyList()), + NetworkResult.empty(), Double.NaN); + + PostContingencyResult staticLine1Failed = new PostContingencyResult( + contingencyLine1, PostContingencyComputationStatus.FAILED, + new LimitViolationsResult(Collections.emptyList()), + NetworkResult.empty(), ConnectivityResult.empty(), Double.NaN); + PostContingencyResult staticLine2Converged = new PostContingencyResult( + contingencyLine2, PostContingencyComputationStatus.CONVERGED, + new LimitViolationsResult(Collections.emptyList()), + NetworkResult.empty(), ConnectivityResult.empty(), Double.NaN); + PostContingencyResult dynamicLine1Converged = new PostContingencyResult( + contingencyLine1, PostContingencyComputationStatus.CONVERGED, + new LimitViolationsResult(Collections.emptyList()), + NetworkResult.empty(), ConnectivityResult.empty(), Double.NaN); + + SecurityAnalysisProvider mockOpenLoadFlow = mock(SecurityAnalysisProvider.class); + when(mockOpenLoadFlow.getName()).thenReturn("OpenLoadFlow"); + when(mockOpenLoadFlow.run(any(), any(), any(), any())).thenReturn( + CompletableFuture.completedFuture(new SecurityAnalysisReport(new SecurityAnalysisResult( + mockPreResult, Arrays.asList(staticLine1Failed, staticLine2Converged), + Collections.emptyList())))); + when(mockDynaFlow.run(any(), any(), any(), any())).thenReturn( + CompletableFuture.completedFuture(new SecurityAnalysisReport(new SecurityAnalysisResult( + mockPreResult, Collections.singletonList(dynamicLine1Converged), + Collections.emptyList())))); + + SecurityAnalysisReport fullReport = new MixedSecurityAnalysis( + network, "InitialState", contingenciesProvider, params, ext, + Arrays.asList(mockOpenLoadFlow, mockDynaFlow)).run().join(); + + verify(mockDynaFlow, times(1)).run(any(), any(), any(), any()); + + List merged = fullReport.getResult().getPostContingencyResults(); + assertEquals(2, merged.size()); + + PostContingencyResult mergedLine1 = merged.stream() + .filter(r -> r.getContingency().getId().equals("NHV1_NHV2_1")) + .findFirst().orElseThrow(); + PostContingencyResult mergedLine2 = merged.stream() + .filter(r -> r.getContingency().getId().equals("NHV1_NHV2_2")) + .findFirst().orElseThrow(); + // line1: dynamic (CONVERGED) overrides static (FAILED) + assertSame(PostContingencyComputationStatus.CONVERGED, mergedLine1.getStatus()); + // line2: kept from static, DynaFlow was not invoked for it + assertSame(PostContingencyComputationStatus.CONVERGED, mergedLine2.getStatus()); + } + + private PostContingencyResult createPostContingencyResult(Contingency contingency, + boolean converged, + int violationCount) { + PostContingencyComputationStatus status = converged + ? PostContingencyComputationStatus.CONVERGED + : PostContingencyComputationStatus.FAILED; + LimitViolationsResult limitViolations = new LimitViolationsResult( + new ArrayList<>(Collections.nCopies(violationCount, null)) + ); + return new PostContingencyResult( + contingency, + status, + limitViolations, + NetworkResult.empty(), + ConnectivityResult.empty(), + Double.NaN + ); + } +} \ No newline at end of file diff --git a/security-analysis/mixed-security-analysis/src/test/java/com/powsybl/mixed/security/analysis/criteria/AnalysisSwitchCriteriaTest.java b/security-analysis/mixed-security-analysis/src/test/java/com/powsybl/mixed/security/analysis/criteria/AnalysisSwitchCriteriaTest.java new file mode 100644 index 00000000000..aae854ef4a2 --- /dev/null +++ b/security-analysis/mixed-security-analysis/src/test/java/com/powsybl/mixed/security/analysis/criteria/AnalysisSwitchCriteriaTest.java @@ -0,0 +1,144 @@ +package com.powsybl.mixed.security.analysis.criteria; + +import com.powsybl.contingency.Contingency; +import com.powsybl.contingency.violations.LimitViolation; +import com.powsybl.mixed.security.analysis.parameters.MixedModeParametersExtension; +import com.powsybl.security.LimitViolationsResult; +import com.powsybl.security.PostContingencyComputationStatus; +import com.powsybl.security.results.ConnectivityResult; +import com.powsybl.security.results.NetworkResult; +import com.powsybl.security.results.PostContingencyResult; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.util.Arrays; +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class AnalysisSwitchCriteriaTest { + private MixedModeParametersExtension extension; + private AnalysisSwitchCriteria criteria; + private PostContingencyResult result; + private Contingency contingency; + + @BeforeEach + void setUp() { + extension = new MixedModeParametersExtension(); + extension.setStaticSimulator("load-flow"); + extension.setDynamicSimulator("dynaflow"); + contingency = Mockito.mock(Contingency.class); + when(contingency.getId()).thenReturn("contingency-1"); + criteria = new AnalysisSwitchCriteria(extension); + } + + @Test + void testNullExtensionThrows() { + assertThrows(NullPointerException.class, () -> new AnalysisSwitchCriteria(null)); + } + + @Test + void testNullResultThrows() { + assertThrows(NullPointerException.class, () -> criteria.evaluate(null)); + } + + @Test + void testNoCriteriaDefinedReturnsNoSwitch() { + extension.setSwitchCriteria(null); + result = createPostContingencyResult(true, 0); + SwitchDecision decision = criteria.evaluate(result); + assertFalse(decision.shouldSwitch()); + assertEquals("No criteria defined", decision.getReason()); + } + + @Test + void testEmptyCriteriaReturnsNoSwitch() { + extension.setSwitchCriteria(Collections.emptyList()); + result = createPostContingencyResult(true, 0); + SwitchDecision decision = criteria.evaluate(result); + assertFalse(decision.shouldSwitch()); + assertEquals("No criteria defined", decision.getReason()); + } + + @Test + void testFailedCriteriaTriggersSwitch() { + extension.setSwitchCriteria(Collections.singletonList("FAILED")); + result = createPostContingencyResult(false, 0); + SwitchDecision decision = criteria.evaluate(result); + assertTrue(decision.shouldSwitch()); + assertTrue(decision.getReason().contains("FAILED")); + } + + @Test + void testFailedCriteriaNoSwitchWhenConverged() { + extension.setSwitchCriteria(Collections.singletonList("FAILED")); + result = createPostContingencyResult(true, 0); + SwitchDecision decision = criteria.evaluate(result); + assertFalse(decision.shouldSwitch()); + assertEquals("No criteria met", decision.getReason()); + } + + @Test + void testLimitViolationsCriteriaTriggersSwitch() { + extension.setSwitchCriteria(Collections.singletonList("LIMIT_VIOLATIONS")); + result = createPostContingencyResult(true, 1); + SwitchDecision decision = criteria.evaluate(result); + assertTrue(decision.shouldSwitch()); + assertTrue(decision.getReason().contains("LIMIT_VIOLATIONS")); + } + + @Test + void testLimitViolationsCriteriaNoSwitchWithoutViolations() { + extension.setSwitchCriteria(Collections.singletonList("LIMIT_VIOLATIONS")); + result = createPostContingencyResult(true, 0); + SwitchDecision decision = criteria.evaluate(result); + assertFalse(decision.shouldSwitch()); + assertEquals("No criteria met", decision.getReason()); + } + + @Test + void testMultipleCriteria() { + extension.setSwitchCriteria(Arrays.asList("FAILED", "LIMIT_VIOLATIONS")); + result = createPostContingencyResult(true, 1); + SwitchDecision decision = criteria.evaluate(result); + assertTrue(decision.shouldSwitch()); + } + + @Test + void testUnknownCriteriaIgnored() { + extension.setSwitchCriteria(Collections.singletonList("UNKNOWN_CRITERION")); + result = createPostContingencyResult(true, 0); + SwitchDecision decision = criteria.evaluate(result); + assertFalse(decision.shouldSwitch()); + assertEquals("No criteria met", decision.getReason()); + } + + @Test + void testSpsTriggerCriteriaNotImplemented() { + extension.setSwitchCriteria(Collections.singletonList("SPS_TRIGGERED")); + result = createPostContingencyResult(true, 0); + SwitchDecision decision = criteria.evaluate(result); + assertFalse(decision.shouldSwitch()); + } + + private PostContingencyResult createPostContingencyResult(boolean converged, int violationCount) { + PostContingencyComputationStatus status = converged + ? PostContingencyComputationStatus.CONVERGED + : PostContingencyComputationStatus.FAILED; + LimitViolationsResult limitViolations = Mockito.mock(LimitViolationsResult.class); + when(limitViolations.getLimitViolations()).thenReturn( + Collections.nCopies(violationCount, mock(LimitViolation.class)) + ); + return new PostContingencyResult( + contingency, + status, + limitViolations, + NetworkResult.empty(), + ConnectivityResult.empty(), + Double.NaN + ); + } +} \ No newline at end of file diff --git a/security-analysis/mixed-security-analysis/src/test/java/com/powsybl/mixed/security/analysis/criteria/SwitchDecisionTest.java b/security-analysis/mixed-security-analysis/src/test/java/com/powsybl/mixed/security/analysis/criteria/SwitchDecisionTest.java new file mode 100644 index 00000000000..c2bda0badcf --- /dev/null +++ b/security-analysis/mixed-security-analysis/src/test/java/com/powsybl/mixed/security/analysis/criteria/SwitchDecisionTest.java @@ -0,0 +1,43 @@ +package com.powsybl.mixed.security.analysis.criteria; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; +class SwitchDecisionTest { + @Test + void testSwitchDecisionTrue() { + SwitchDecision decision = new SwitchDecision(true, "Test reason"); + assertTrue(decision.shouldSwitch()); + assertEquals("Test reason", decision.getReason()); + } + @Test + void testSwitchDecisionFalse() { + SwitchDecision decision = new SwitchDecision(false, "No switch"); + assertFalse(decision.shouldSwitch()); + assertEquals("No switch", decision.getReason()); + } + @Test + void testSwitchDecisionNullReasonThrows() { + assertThrows(NullPointerException.class, () -> new SwitchDecision(true, null)); + } + @Test + void testSwitchDecisionToString() { + SwitchDecision decision = new SwitchDecision(true, "Test reason"); + String str = decision.toString(); + assertNotNull(str); + assertTrue(str.contains("shouldSwitch=true")); + assertTrue(str.contains("Test reason")); + } + @Test + void testSwitchDecisionEquals() { + SwitchDecision decision1 = new SwitchDecision(true, "Test reason"); + SwitchDecision decision2 = new SwitchDecision(true, "Test reason"); + SwitchDecision decision3 = new SwitchDecision(false, "Test reason"); + assertEquals(decision1, decision2); + assertNotEquals(decision1, decision3); + } + @Test + void testSwitchDecisionHashCode() { + SwitchDecision decision1 = new SwitchDecision(true, "Test reason"); + SwitchDecision decision2 = new SwitchDecision(true, "Test reason"); + assertEquals(decision1.hashCode(), decision2.hashCode()); + } +} diff --git a/security-analysis/mixed-security-analysis/src/test/java/com/powsybl/mixed/security/analysis/parameters/MixedModeParametersExtensionTest.java b/security-analysis/mixed-security-analysis/src/test/java/com/powsybl/mixed/security/analysis/parameters/MixedModeParametersExtensionTest.java new file mode 100644 index 00000000000..cfb58114bdd --- /dev/null +++ b/security-analysis/mixed-security-analysis/src/test/java/com/powsybl/mixed/security/analysis/parameters/MixedModeParametersExtensionTest.java @@ -0,0 +1,57 @@ +package com.powsybl.mixed.security.analysis.parameters; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import java.util.Arrays; +import java.util.Collections; +import static org.junit.jupiter.api.Assertions.*; +class MixedModeParametersExtensionTest { + private MixedModeParametersExtension extension; + @BeforeEach + void setUp() { + extension = new MixedModeParametersExtension(); + } + @Test + void testDefaultValues() { + assertNull(extension.getStaticSimulator()); + assertNull(extension.getDynamicSimulator()); + assertNull(extension.getSwitchCriteria()); + } + @Test + void testSetGetStaticSimulator() { + extension.setStaticSimulator("load-flow"); + assertEquals("load-flow", extension.getStaticSimulator()); + } + @Test + void testSetGetDynamicSimulator() { + extension.setDynamicSimulator("dynaflow"); + assertEquals("dynaflow", extension.getDynamicSimulator()); + } + @Test + void testSetGetSwitchCriteria() { + extension.setSwitchCriteria(Arrays.asList("NON_CONVERGENCE", "LIMIT_VIOLATIONS")); + assertEquals(2, extension.getSwitchCriteria().size()); + assertTrue(extension.getSwitchCriteria().contains("NON_CONVERGENCE")); + assertTrue(extension.getSwitchCriteria().contains("LIMIT_VIOLATIONS")); + } + + @Test + void testSetEmptySwitchCriteria() { + extension.setSwitchCriteria(Collections.emptyList()); + assertNotNull(extension.getSwitchCriteria()); + assertEquals(0, extension.getSwitchCriteria().size()); + } + @Test + void testSetNullSwitchCriteria() { + extension.setSwitchCriteria(null); + assertNull(extension.getSwitchCriteria()); + } + @Test + void testCompleteConfiguration() { + extension.setStaticSimulator("load-flow"); + extension.setDynamicSimulator("dynaflow"); + extension.setSwitchCriteria(Collections.singletonList("NON_CONVERGENCE")); + assertEquals("load-flow", extension.getStaticSimulator()); + assertEquals("dynaflow", extension.getDynamicSimulator()); + assertEquals(1, extension.getSwitchCriteria().size()); + } +} diff --git a/security-analysis/pom.xml b/security-analysis/pom.xml index aef1982fef3..2f26a903623 100644 --- a/security-analysis/pom.xml +++ b/security-analysis/pom.xml @@ -25,6 +25,7 @@ security-analysis-api + mixed-security-analysis From a12f95c53d5603451841c24f86ea1e23edf4e9e9 Mon Sep 17 00:00:00 2001 From: Riad Benradi Date: Mon, 13 Apr 2026 08:32:01 +0200 Subject: [PATCH 2/3] Checkstyles corrections Signed-off-by: Riad Benradi --- .../analysis/MixedSecurityAnalysis.java | 27 ++-- .../MixedSecurityAnalysisProvider.java | 2 +- .../criteria/AnalysisSwitchCriteria.java | 15 +- .../analysis/criteria/SwitchDecision.java | 14 +- .../MixedModeParametersExtension.java | 7 - .../MixedSecurityAnalysisProviderTest.java | 5 + .../analysis/MixedSecurityAnalysisTest.java | 132 +++++++----------- .../criteria/AnalysisSwitchCriteriaTest.java | 10 +- .../analysis/criteria/SwitchDecisionTest.java | 6 + .../MixedModeParametersExtensionTest.java | 7 + .../PostContingencyComputationStatus.java | 3 +- 11 files changed, 104 insertions(+), 124 deletions(-) diff --git a/security-analysis/mixed-security-analysis/src/main/java/com/powsybl/mixed/security/analysis/MixedSecurityAnalysis.java b/security-analysis/mixed-security-analysis/src/main/java/com/powsybl/mixed/security/analysis/MixedSecurityAnalysis.java index 614f9289e90..1fc8b506054 100644 --- a/security-analysis/mixed-security-analysis/src/main/java/com/powsybl/mixed/security/analysis/MixedSecurityAnalysis.java +++ b/security-analysis/mixed-security-analysis/src/main/java/com/powsybl/mixed/security/analysis/MixedSecurityAnalysis.java @@ -58,8 +58,7 @@ public MixedSecurityAnalysis(Network network, String workingVariantId, Contingen */ public CompletableFuture run() { LOGGER.info("Starting mixed-mode security analysis"); - LOGGER.debug("Static simulator: {}, Dynamic simulator: {}", - extension.getStaticSimulator(), extension.getDynamicSimulator()); + LOGGER.debug("Static simulator: {}, Dynamic simulator: {}", extension.getStaticSimulator(), extension.getDynamicSimulator()); // Step 1: Get all contingencies List allContingencies = contingenciesProvider.getContingencies(network); @@ -70,16 +69,15 @@ public CompletableFuture run() { SecurityAnalysisProvider staticProvider = findProvider(staticProviderName); CompletableFuture staticAnalysisFuture = staticProvider.run( - network, workingVariantId, contingenciesProvider, runParameters); + network, workingVariantId, contingenciesProvider, runParameters); // Step 3: Chain dynamic analysis based on static results return staticAnalysisFuture.thenCompose(staticReport -> { LOGGER.info("Static analysis completed"); - // Evaluate switch criteria AnalysisSwitchCriteria switchCriteria = new AnalysisSwitchCriteria(extension); List contingenciesToRunDynamic = identifyDynamicContingencies( - staticReport.getResult(), switchCriteria); + staticReport.getResult(), switchCriteria); LOGGER.info("Contingencies requiring dynamic analysis: {}", contingenciesToRunDynamic.size()); @@ -91,7 +89,7 @@ public CompletableFuture run() { // Run dynamic analysis on filtered contingencies return runDynamicAnalysis(contingenciesToRunDynamic, allContingencies) - .thenApply(dynamicReport -> mergeResults(staticReport, dynamicReport)); + .thenApply(dynamicReport -> mergeResults(staticReport, dynamicReport)); }).exceptionally(ex -> { LOGGER.error("Error during mixed-mode security analysis", ex); return new SecurityAnalysisReport(SecurityAnalysisResult.empty()); @@ -114,8 +112,7 @@ private List identifyDynamicContingencies(SecurityAnalysisResult staticR */ private boolean shouldRunDynamic(PostContingencyResult result, AnalysisSwitchCriteria switchCriteria) { SwitchDecision decision = switchCriteria.evaluate(result); - LOGGER.debug("Contingency {} - Switch decision: {}", - result.getContingency().getId(), decision.getReason()); + LOGGER.debug("Contingency {} - Switch decision: {}", result.getContingency().getId(), decision.getReason()); return decision.shouldSwitch(); } @@ -127,10 +124,10 @@ private CompletableFuture runDynamicAnalysis(List allContingencies) { LOGGER.info("Starting dynamic analysis pass for {} contingencies", contingencyIds.size()); - ContingenciesProvider filteredProvider = (net) -> - allContingencies.stream() - .filter(c -> contingencyIds.contains(c.getId())) - .collect(Collectors.toList()); + ContingenciesProvider filteredProvider = network -> + allContingencies.stream() + .filter(c -> contingencyIds.contains(c.getId())) + .collect(Collectors.toList()); String dynamicProviderName = extension.getDynamicSimulator(); SecurityAnalysisProvider dynamicProvider = findProvider(dynamicProviderName); @@ -144,10 +141,8 @@ private CompletableFuture runDynamicAnalysis(List run(Network network, LOGGER.info("Starting mixed-mode security analysis for network: {}", network.getId()); MixedModeParametersExtension extension = runParameters.getSecurityAnalysisParameters().getExtension(MixedModeParametersExtension.class); - if(extension == null) { + if (extension == null) { LOGGER.warn("MixedModeParametersExtension configuration is missing from SecurityAnalysisRunParameters, using defaults."); extension = (MixedModeParametersExtension) loadSpecificParameters(PlatformConfig.defaultConfig()).orElseThrow(() -> diff --git a/security-analysis/mixed-security-analysis/src/main/java/com/powsybl/mixed/security/analysis/criteria/AnalysisSwitchCriteria.java b/security-analysis/mixed-security-analysis/src/main/java/com/powsybl/mixed/security/analysis/criteria/AnalysisSwitchCriteria.java index 49d28695b0a..869bf4bc961 100644 --- a/security-analysis/mixed-security-analysis/src/main/java/com/powsybl/mixed/security/analysis/criteria/AnalysisSwitchCriteria.java +++ b/security-analysis/mixed-security-analysis/src/main/java/com/powsybl/mixed/security/analysis/criteria/AnalysisSwitchCriteria.java @@ -12,12 +12,16 @@ * * @author Riad Benradi {@literal } */ + public class AnalysisSwitchCriteria { + private static final Logger LOGGER = LoggerFactory.getLogger(AnalysisSwitchCriteria.class); private final MixedModeParametersExtension extension; + public AnalysisSwitchCriteria(MixedModeParametersExtension extension) { this.extension = Objects.requireNonNull(extension); } + public SwitchDecision evaluate(PostContingencyResult result) { Objects.requireNonNull(result); if (extension.getSwitchCriteria() == null || extension.getSwitchCriteria().isEmpty()) { @@ -31,6 +35,7 @@ public SwitchDecision evaluate(PostContingencyResult result) { } return new SwitchDecision(false, "No criteria met"); } + private boolean evaluateCriterion(PostContingencyResult result, String criterion) { return switch (criterion.toUpperCase()) { case "FAILED" -> evaluateNonConvergence(result); @@ -42,11 +47,11 @@ private boolean evaluateCriterion(PostContingencyResult result, String criterion } }; } + private boolean evaluateNonConvergence(PostContingencyResult result) { boolean converged = result.getStatus() == PostContingencyComputationStatus.CONVERGED; if (!converged) { - LOGGER.debug("Non-convergence detected for contingency {}", - result.getContingency().getId()); + LOGGER.debug("Non-convergence detected for contingency {}", result.getContingency().getId()); } return !converged; } @@ -54,12 +59,12 @@ private boolean evaluateNonConvergence(PostContingencyResult result) { private boolean evaluateLimitViolations(PostContingencyResult result) { boolean hasViolations = !result.getLimitViolationsResult().getLimitViolations().isEmpty(); if (hasViolations) { - LOGGER.debug("Limit violations detected for contingency {}", - result.getContingency().getId()); + LOGGER.debug("Limit violations detected for contingency {}", result.getContingency().getId()); } return hasViolations; } + private boolean evaluateSpsTriggered(PostContingencyResult result) { - return false; + return true; } } diff --git a/security-analysis/mixed-security-analysis/src/main/java/com/powsybl/mixed/security/analysis/criteria/SwitchDecision.java b/security-analysis/mixed-security-analysis/src/main/java/com/powsybl/mixed/security/analysis/criteria/SwitchDecision.java index 6c67f5b0e63..2378289be23 100644 --- a/security-analysis/mixed-security-analysis/src/main/java/com/powsybl/mixed/security/analysis/criteria/SwitchDecision.java +++ b/security-analysis/mixed-security-analysis/src/main/java/com/powsybl/mixed/security/analysis/criteria/SwitchDecision.java @@ -26,8 +26,12 @@ public String getReason() { @Override public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } SwitchDecision that = (SwitchDecision) o; return shouldSwitch == that.shouldSwitch && Objects.equals(reason, that.reason); } @@ -40,9 +44,9 @@ public int hashCode() { @Override public String toString() { return "SwitchDecision{" + - "shouldSwitch=" + shouldSwitch + - ", reason='" + reason + '\'' + - '}'; + "shouldSwitch=" + shouldSwitch + + ", reason='" + reason + '\'' + + '}'; } } diff --git a/security-analysis/mixed-security-analysis/src/main/java/com/powsybl/mixed/security/analysis/parameters/MixedModeParametersExtension.java b/security-analysis/mixed-security-analysis/src/main/java/com/powsybl/mixed/security/analysis/parameters/MixedModeParametersExtension.java index 4dac2c74c85..95b2343614c 100644 --- a/security-analysis/mixed-security-analysis/src/main/java/com/powsybl/mixed/security/analysis/parameters/MixedModeParametersExtension.java +++ b/security-analysis/mixed-security-analysis/src/main/java/com/powsybl/mixed/security/analysis/parameters/MixedModeParametersExtension.java @@ -17,26 +17,19 @@ public class MixedModeParametersExtension extends AbstractExtension switchCriteria; - // --- Getters and Setters --- - // These are necessary so that configuration tools like Jackson (used by PowSybl) - // can populate the fields of this object. - public String getStaticSimulator() { return staticSimulator; } diff --git a/security-analysis/mixed-security-analysis/src/test/java/com/powsybl/mixed/security/analysis/MixedSecurityAnalysisProviderTest.java b/security-analysis/mixed-security-analysis/src/test/java/com/powsybl/mixed/security/analysis/MixedSecurityAnalysisProviderTest.java index f3e0a3ced5e..5e6e95748e3 100644 --- a/security-analysis/mixed-security-analysis/src/test/java/com/powsybl/mixed/security/analysis/MixedSecurityAnalysisProviderTest.java +++ b/security-analysis/mixed-security-analysis/src/test/java/com/powsybl/mixed/security/analysis/MixedSecurityAnalysisProviderTest.java @@ -13,6 +13,7 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; + class MixedSecurityAnalysisProviderTest { @Mock private Network network; @@ -21,21 +22,25 @@ class MixedSecurityAnalysisProviderTest { @Mock private SecurityAnalysisRunParameters runParameters; private MixedSecurityAnalysisProvider provider; + @BeforeEach void setUp() { MockitoAnnotations.openMocks(this); provider = new MixedSecurityAnalysisProvider(); } + @Test void testProviderName() { assertEquals("MixedSecurityAnalysis", provider.getName()); } + @Test void testProviderVersion() { String version = provider.getVersion(); assertNotNull(version); assertFalse(version.isEmpty()); } + @Test void testRunMissingExtensionThrows() { SecurityAnalysisParameters mockSaParams = mock(SecurityAnalysisParameters.class); diff --git a/security-analysis/mixed-security-analysis/src/test/java/com/powsybl/mixed/security/analysis/MixedSecurityAnalysisTest.java b/security-analysis/mixed-security-analysis/src/test/java/com/powsybl/mixed/security/analysis/MixedSecurityAnalysisTest.java index 25a81fe31a0..ed1adb16228 100644 --- a/security-analysis/mixed-security-analysis/src/test/java/com/powsybl/mixed/security/analysis/MixedSecurityAnalysisTest.java +++ b/security-analysis/mixed-security-analysis/src/test/java/com/powsybl/mixed/security/analysis/MixedSecurityAnalysisTest.java @@ -18,6 +18,7 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -27,6 +28,7 @@ import static com.powsybl.loadflow.LoadFlowResult.ComponentResult.Status.CONVERGED; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.*; class MixedSecurityAnalysisTest { @@ -69,8 +71,8 @@ void setUp() { contingency2 = mock(Contingency.class); when(contingency2.getId()).thenReturn("contingency-2"); mixedAnalysis = new MixedSecurityAnalysis(network, "main", contingenciesProvider, - runParameters, extension, - Arrays.asList(staticProvider, dynamicProvider)); + runParameters, extension, + Arrays.asList(staticProvider, dynamicProvider)); } @Test @@ -98,7 +100,7 @@ void testRunSuccessfulNoSwitchNeeded() { } @Test - void testRunWithSwitchTooDynamic() { + void testRunWithSwitchOnNotConverged() { List allContingencies = Arrays.asList(contingency1, contingency2); when(contingenciesProvider.getContingencies(network)).thenReturn(allContingencies); // contingency1 fails static → triggers switch to dynamic @@ -182,94 +184,64 @@ void testRunHandlesException() { assertEquals(0, report.getResult().getPostContingencyResults().size()); } - /** - * Integration test covering both workflow paths: - * Phase 1 — real OpenLoadFlow, both contingencies converge, DynaFlow never called. - * Phase 2 — mock providers, static FAILED triggers dynamic dispatch, results merged. - */ @Test - void testSimpleMixedSecurityAnalysis() { + void testSwitchOnSpsTriggered() throws IOException { + // 1. Load the network Network network = EurostagTutorialExample1Factory.create(); - Contingency contingencyLine1 = Contingency.line("NHV1_NHV2_1"); - Contingency contingencyLine2 = Contingency.line("NHV1_NHV2_2"); - when(contingenciesProvider.getContingencies(network)) - .thenReturn(Arrays.asList(contingencyLine1, contingencyLine2)); + // 2. Define a simple contingency that will be tested + Contingency simpleContingency = Contingency.line("NHV1_NHV2_2"); + when(contingenciesProvider.getContingencies(network)).thenReturn(Collections.singletonList(simpleContingency)); + + // 3. Configure the mixed analysis to switch on the 'SPS_TRIGGERED' status MixedModeParametersExtension ext = new MixedModeParametersExtension(); ext.setStaticSimulator("OpenLoadFlow"); - ext.setDynamicSimulator("DynaFlow"); - ext.setSwitchCriteria(Collections.singletonList("FAILED")); - - SecurityAnalysisProvider mockDynaFlow = mock(SecurityAnalysisProvider.class); - when(mockDynaFlow.getName()).thenReturn("DynaFlow"); - SecurityAnalysisRunParameters params = SecurityAnalysisRunParameters.getDefault(); - - // Phase 1: real OpenLoadFlow — both contingencies converge, no dynamic switch - SecurityAnalysisReport staticOnlyReport = new MixedSecurityAnalysis( - network, "InitialState", contingenciesProvider, params, ext, - Collections.singletonList(mockDynaFlow)).run().join(); - - PreContingencyResult preResult = staticOnlyReport.getResult().getPreContingencyResult(); - assertNotNull(preResult); - assertEquals(CONVERGED, preResult.getStatus()); - assertTrue(preResult.getLimitViolationsResult().getLimitViolations().isEmpty()); + ext.setDynamicSimulator("dynaFlow"); + ext.setSwitchCriteria(Collections.singletonList("SPS_TRIGGERED")); - List staticResults = staticOnlyReport.getResult().getPostContingencyResults(); - assertEquals(2, staticResults.size()); - staticResults.forEach(r -> { - assertSame(PostContingencyComputationStatus.CONVERGED, r.getStatus()); - assertTrue(r.getLimitViolationsResult().getLimitViolations().isEmpty()); - }); - verify(mockDynaFlow, never()).run(any(), any(), any(), any()); + // 4. Create the MixedSecurityAnalysis instance with the network and mock providers + MixedSecurityAnalysis analysis = new MixedSecurityAnalysis( + network, "InitialState", contingenciesProvider, + runParameters, ext, + Arrays.asList(staticProvider, dynamicProvider)); - // Phase 2: static FAILED on line1 → DynaFlow dispatched for line1 only, results merged - PreContingencyResult mockPreResult = new PreContingencyResult( - CONVERGED, new LimitViolationsResult(Collections.emptyList()), - NetworkResult.empty(), Double.NaN); - - PostContingencyResult staticLine1Failed = new PostContingencyResult( - contingencyLine1, PostContingencyComputationStatus.FAILED, - new LimitViolationsResult(Collections.emptyList()), - NetworkResult.empty(), ConnectivityResult.empty(), Double.NaN); - PostContingencyResult staticLine2Converged = new PostContingencyResult( - contingencyLine2, PostContingencyComputationStatus.CONVERGED, - new LimitViolationsResult(Collections.emptyList()), - NetworkResult.empty(), ConnectivityResult.empty(), Double.NaN); - PostContingencyResult dynamicLine1Converged = new PostContingencyResult( - contingencyLine1, PostContingencyComputationStatus.CONVERGED, + // 5. Mock the static provider to return a result with 'SPS_TRIGGERED' status + PostContingencyResult staticSpsResult = new PostContingencyResult( + simpleContingency, + PostContingencyComputationStatus.SPS_TRIGGERED, new LimitViolationsResult(Collections.emptyList()), - NetworkResult.empty(), ConnectivityResult.empty(), Double.NaN); - - SecurityAnalysisProvider mockOpenLoadFlow = mock(SecurityAnalysisProvider.class); - when(mockOpenLoadFlow.getName()).thenReturn("OpenLoadFlow"); - when(mockOpenLoadFlow.run(any(), any(), any(), any())).thenReturn( - CompletableFuture.completedFuture(new SecurityAnalysisReport(new SecurityAnalysisResult( - mockPreResult, Arrays.asList(staticLine1Failed, staticLine2Converged), - Collections.emptyList())))); - when(mockDynaFlow.run(any(), any(), any(), any())).thenReturn( - CompletableFuture.completedFuture(new SecurityAnalysisReport(new SecurityAnalysisResult( - mockPreResult, Collections.singletonList(dynamicLine1Converged), - Collections.emptyList())))); + NetworkResult.empty(), + ConnectivityResult.empty(), + Double.NaN + ); + SecurityAnalysisReport staticReport = new SecurityAnalysisReport( + new SecurityAnalysisResult(new PreContingencyResult(CONVERGED, new LimitViolationsResult(Collections.emptyList()), NetworkResult.empty(), Double.NaN), + Collections.singletonList(staticSpsResult), Collections.emptyList()) + ); + when(staticProvider.run(any(Network.class), anyString(), any(ContingenciesProvider.class), any(SecurityAnalysisRunParameters.class))) + .thenReturn(CompletableFuture.completedFuture(staticReport)); - SecurityAnalysisReport fullReport = new MixedSecurityAnalysis( - network, "InitialState", contingenciesProvider, params, ext, - Arrays.asList(mockOpenLoadFlow, mockDynaFlow)).run().join(); + // 6. Mock the dynamic provider to return a successful 'CONVERGED' result + PostContingencyResult dynamicConvergedResult = createPostContingencyResult(simpleContingency, true, 0); + SecurityAnalysisReport dynamicReport = new SecurityAnalysisReport( + new SecurityAnalysisResult(new PreContingencyResult(CONVERGED, new LimitViolationsResult(Collections.emptyList()), NetworkResult.empty(), Double.NaN), + Collections.singletonList(dynamicConvergedResult), Collections.emptyList()) + ); + when(dynamicProvider.run(any(Network.class), anyString(), any(ContingenciesProvider.class), any(SecurityAnalysisRunParameters.class))) + .thenReturn(CompletableFuture.completedFuture(dynamicReport)); - verify(mockDynaFlow, times(1)).run(any(), any(), any(), any()); + // 7. Execute the mixed analysis + SecurityAnalysisReport report = analysis.run().join(); - List merged = fullReport.getResult().getPostContingencyResults(); - assertEquals(2, merged.size()); + // 8. Verify the logic: both providers were called, and the final result is the converged one from the dynamic analysis + verify(staticProvider).run(network, "InitialState", contingenciesProvider, runParameters); + verify(dynamicProvider).run(any(Network.class), anyString(), any(ContingenciesProvider.class), any(SecurityAnalysisRunParameters.class)); - PostContingencyResult mergedLine1 = merged.stream() - .filter(r -> r.getContingency().getId().equals("NHV1_NHV2_1")) - .findFirst().orElseThrow(); - PostContingencyResult mergedLine2 = merged.stream() - .filter(r -> r.getContingency().getId().equals("NHV1_NHV2_2")) - .findFirst().orElseThrow(); - // line1: dynamic (CONVERGED) overrides static (FAILED) - assertSame(PostContingencyComputationStatus.CONVERGED, mergedLine1.getStatus()); - // line2: kept from static, DynaFlow was not invoked for it - assertSame(PostContingencyComputationStatus.CONVERGED, mergedLine2.getStatus()); + List results = report.getResult().getPostContingencyResults(); + assertEquals(1, results.size()); + PostContingencyResult finalResult = results.getFirst(); + assertEquals(simpleContingency.getId(), finalResult.getContingency().getId()); + assertSame(PostContingencyComputationStatus.CONVERGED, finalResult.getStatus()); } private PostContingencyResult createPostContingencyResult(Contingency contingency, @@ -290,4 +262,4 @@ private PostContingencyResult createPostContingencyResult(Contingency contingenc Double.NaN ); } -} \ No newline at end of file +} diff --git a/security-analysis/mixed-security-analysis/src/test/java/com/powsybl/mixed/security/analysis/criteria/AnalysisSwitchCriteriaTest.java b/security-analysis/mixed-security-analysis/src/test/java/com/powsybl/mixed/security/analysis/criteria/AnalysisSwitchCriteriaTest.java index aae854ef4a2..eb31947e5c2 100644 --- a/security-analysis/mixed-security-analysis/src/test/java/com/powsybl/mixed/security/analysis/criteria/AnalysisSwitchCriteriaTest.java +++ b/security-analysis/mixed-security-analysis/src/test/java/com/powsybl/mixed/security/analysis/criteria/AnalysisSwitchCriteriaTest.java @@ -116,14 +116,6 @@ void testUnknownCriteriaIgnored() { assertEquals("No criteria met", decision.getReason()); } - @Test - void testSpsTriggerCriteriaNotImplemented() { - extension.setSwitchCriteria(Collections.singletonList("SPS_TRIGGERED")); - result = createPostContingencyResult(true, 0); - SwitchDecision decision = criteria.evaluate(result); - assertFalse(decision.shouldSwitch()); - } - private PostContingencyResult createPostContingencyResult(boolean converged, int violationCount) { PostContingencyComputationStatus status = converged ? PostContingencyComputationStatus.CONVERGED @@ -141,4 +133,4 @@ private PostContingencyResult createPostContingencyResult(boolean converged, int Double.NaN ); } -} \ No newline at end of file +} diff --git a/security-analysis/mixed-security-analysis/src/test/java/com/powsybl/mixed/security/analysis/criteria/SwitchDecisionTest.java b/security-analysis/mixed-security-analysis/src/test/java/com/powsybl/mixed/security/analysis/criteria/SwitchDecisionTest.java index c2bda0badcf..207672b8930 100644 --- a/security-analysis/mixed-security-analysis/src/test/java/com/powsybl/mixed/security/analysis/criteria/SwitchDecisionTest.java +++ b/security-analysis/mixed-security-analysis/src/test/java/com/powsybl/mixed/security/analysis/criteria/SwitchDecisionTest.java @@ -2,22 +2,26 @@ import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; class SwitchDecisionTest { + @Test void testSwitchDecisionTrue() { SwitchDecision decision = new SwitchDecision(true, "Test reason"); assertTrue(decision.shouldSwitch()); assertEquals("Test reason", decision.getReason()); } + @Test void testSwitchDecisionFalse() { SwitchDecision decision = new SwitchDecision(false, "No switch"); assertFalse(decision.shouldSwitch()); assertEquals("No switch", decision.getReason()); } + @Test void testSwitchDecisionNullReasonThrows() { assertThrows(NullPointerException.class, () -> new SwitchDecision(true, null)); } + @Test void testSwitchDecisionToString() { SwitchDecision decision = new SwitchDecision(true, "Test reason"); @@ -26,6 +30,7 @@ void testSwitchDecisionToString() { assertTrue(str.contains("shouldSwitch=true")); assertTrue(str.contains("Test reason")); } + @Test void testSwitchDecisionEquals() { SwitchDecision decision1 = new SwitchDecision(true, "Test reason"); @@ -34,6 +39,7 @@ void testSwitchDecisionEquals() { assertEquals(decision1, decision2); assertNotEquals(decision1, decision3); } + @Test void testSwitchDecisionHashCode() { SwitchDecision decision1 = new SwitchDecision(true, "Test reason"); diff --git a/security-analysis/mixed-security-analysis/src/test/java/com/powsybl/mixed/security/analysis/parameters/MixedModeParametersExtensionTest.java b/security-analysis/mixed-security-analysis/src/test/java/com/powsybl/mixed/security/analysis/parameters/MixedModeParametersExtensionTest.java index cfb58114bdd..33c29fd7741 100644 --- a/security-analysis/mixed-security-analysis/src/test/java/com/powsybl/mixed/security/analysis/parameters/MixedModeParametersExtensionTest.java +++ b/security-analysis/mixed-security-analysis/src/test/java/com/powsybl/mixed/security/analysis/parameters/MixedModeParametersExtensionTest.java @@ -6,26 +6,31 @@ import static org.junit.jupiter.api.Assertions.*; class MixedModeParametersExtensionTest { private MixedModeParametersExtension extension; + @BeforeEach void setUp() { extension = new MixedModeParametersExtension(); } + @Test void testDefaultValues() { assertNull(extension.getStaticSimulator()); assertNull(extension.getDynamicSimulator()); assertNull(extension.getSwitchCriteria()); } + @Test void testSetGetStaticSimulator() { extension.setStaticSimulator("load-flow"); assertEquals("load-flow", extension.getStaticSimulator()); } + @Test void testSetGetDynamicSimulator() { extension.setDynamicSimulator("dynaflow"); assertEquals("dynaflow", extension.getDynamicSimulator()); } + @Test void testSetGetSwitchCriteria() { extension.setSwitchCriteria(Arrays.asList("NON_CONVERGENCE", "LIMIT_VIOLATIONS")); @@ -40,11 +45,13 @@ void testSetEmptySwitchCriteria() { assertNotNull(extension.getSwitchCriteria()); assertEquals(0, extension.getSwitchCriteria().size()); } + @Test void testSetNullSwitchCriteria() { extension.setSwitchCriteria(null); assertNull(extension.getSwitchCriteria()); } + @Test void testCompleteConfiguration() { extension.setStaticSimulator("load-flow"); diff --git a/security-analysis/security-analysis-api/src/main/java/com/powsybl/security/PostContingencyComputationStatus.java b/security-analysis/security-analysis-api/src/main/java/com/powsybl/security/PostContingencyComputationStatus.java index 65a25fa5235..1ec59fea24d 100644 --- a/security-analysis/security-analysis-api/src/main/java/com/powsybl/security/PostContingencyComputationStatus.java +++ b/security-analysis/security-analysis-api/src/main/java/com/powsybl/security/PostContingencyComputationStatus.java @@ -15,5 +15,6 @@ public enum PostContingencyComputationStatus { MAX_ITERATION_REACHED, SOLVER_FAILED, FAILED, - NO_IMPACT + NO_IMPACT, + SPS_TRIGGERED } From c4ab743d6083e44a97957df2ed66dbbf43032c33 Mon Sep 17 00:00:00 2001 From: Riad Benradi Date: Mon, 13 Apr 2026 09:25:06 +0200 Subject: [PATCH 3/3] quick fixes Signed-off-by: Riad Benradi --- .../pom.xml | 9 +++------ .../mixed/security/analysis/MixedSecurityAnalysis.java | 0 .../security/analysis/MixedSecurityAnalysisProvider.java | 0 .../analysis/criteria/AnalysisSwitchCriteria.java | 4 ++-- .../mixed/security/analysis/criteria/SwitchDecision.java | 0 .../parameters/MixedModeParametersExtension.java | 0 .../analysis/MixedSecurityAnalysisProviderTest.java | 0 .../security/analysis/MixedSecurityAnalysisTest.java | 3 +-- .../analysis/criteria/AnalysisSwitchCriteriaTest.java | 0 .../security/analysis/criteria/SwitchDecisionTest.java | 0 .../parameters/MixedModeParametersExtensionTest.java | 0 security-analysis/pom.xml | 2 +- 12 files changed, 7 insertions(+), 11 deletions(-) rename security-analysis/{mixed-security-analysis => mixed-security-analysis-api}/pom.xml (93%) rename security-analysis/{mixed-security-analysis => mixed-security-analysis-api}/src/main/java/com/powsybl/mixed/security/analysis/MixedSecurityAnalysis.java (100%) rename security-analysis/{mixed-security-analysis => mixed-security-analysis-api}/src/main/java/com/powsybl/mixed/security/analysis/MixedSecurityAnalysisProvider.java (100%) rename security-analysis/{mixed-security-analysis => mixed-security-analysis-api}/src/main/java/com/powsybl/mixed/security/analysis/criteria/AnalysisSwitchCriteria.java (95%) rename security-analysis/{mixed-security-analysis => mixed-security-analysis-api}/src/main/java/com/powsybl/mixed/security/analysis/criteria/SwitchDecision.java (100%) rename security-analysis/{mixed-security-analysis => mixed-security-analysis-api}/src/main/java/com/powsybl/mixed/security/analysis/parameters/MixedModeParametersExtension.java (100%) rename security-analysis/{mixed-security-analysis => mixed-security-analysis-api}/src/test/java/com/powsybl/mixed/security/analysis/MixedSecurityAnalysisProviderTest.java (100%) rename security-analysis/{mixed-security-analysis => mixed-security-analysis-api}/src/test/java/com/powsybl/mixed/security/analysis/MixedSecurityAnalysisTest.java (99%) rename security-analysis/{mixed-security-analysis => mixed-security-analysis-api}/src/test/java/com/powsybl/mixed/security/analysis/criteria/AnalysisSwitchCriteriaTest.java (100%) rename security-analysis/{mixed-security-analysis => mixed-security-analysis-api}/src/test/java/com/powsybl/mixed/security/analysis/criteria/SwitchDecisionTest.java (100%) rename security-analysis/{mixed-security-analysis => mixed-security-analysis-api}/src/test/java/com/powsybl/mixed/security/analysis/parameters/MixedModeParametersExtensionTest.java (100%) diff --git a/security-analysis/mixed-security-analysis/pom.xml b/security-analysis/mixed-security-analysis-api/pom.xml similarity index 93% rename from security-analysis/mixed-security-analysis/pom.xml rename to security-analysis/mixed-security-analysis-api/pom.xml index f77a3a564b2..cc4196763e2 100644 --- a/security-analysis/mixed-security-analysis/pom.xml +++ b/security-analysis/mixed-security-analysis-api/pom.xml @@ -9,7 +9,9 @@ 7.3.0-SNAPSHOT - mixed-security-analysis + mixed-security-analysis-api + Mixed-Mode Security Analysis API + An API for running mixed-mode (static and dynamic) security analyses. 21 @@ -31,11 +33,6 @@ powsybl-open-loadflow 2.2.0 - - com.powsybl - powsybl-dynaflow - 3.2.0-SNAPSHOT - com.powsybl powsybl-iidm-serde diff --git a/security-analysis/mixed-security-analysis/src/main/java/com/powsybl/mixed/security/analysis/MixedSecurityAnalysis.java b/security-analysis/mixed-security-analysis-api/src/main/java/com/powsybl/mixed/security/analysis/MixedSecurityAnalysis.java similarity index 100% rename from security-analysis/mixed-security-analysis/src/main/java/com/powsybl/mixed/security/analysis/MixedSecurityAnalysis.java rename to security-analysis/mixed-security-analysis-api/src/main/java/com/powsybl/mixed/security/analysis/MixedSecurityAnalysis.java diff --git a/security-analysis/mixed-security-analysis/src/main/java/com/powsybl/mixed/security/analysis/MixedSecurityAnalysisProvider.java b/security-analysis/mixed-security-analysis-api/src/main/java/com/powsybl/mixed/security/analysis/MixedSecurityAnalysisProvider.java similarity index 100% rename from security-analysis/mixed-security-analysis/src/main/java/com/powsybl/mixed/security/analysis/MixedSecurityAnalysisProvider.java rename to security-analysis/mixed-security-analysis-api/src/main/java/com/powsybl/mixed/security/analysis/MixedSecurityAnalysisProvider.java diff --git a/security-analysis/mixed-security-analysis/src/main/java/com/powsybl/mixed/security/analysis/criteria/AnalysisSwitchCriteria.java b/security-analysis/mixed-security-analysis-api/src/main/java/com/powsybl/mixed/security/analysis/criteria/AnalysisSwitchCriteria.java similarity index 95% rename from security-analysis/mixed-security-analysis/src/main/java/com/powsybl/mixed/security/analysis/criteria/AnalysisSwitchCriteria.java rename to security-analysis/mixed-security-analysis-api/src/main/java/com/powsybl/mixed/security/analysis/criteria/AnalysisSwitchCriteria.java index 869bf4bc961..fd5b3a2a6d9 100644 --- a/security-analysis/mixed-security-analysis/src/main/java/com/powsybl/mixed/security/analysis/criteria/AnalysisSwitchCriteria.java +++ b/security-analysis/mixed-security-analysis-api/src/main/java/com/powsybl/mixed/security/analysis/criteria/AnalysisSwitchCriteria.java @@ -40,7 +40,7 @@ private boolean evaluateCriterion(PostContingencyResult result, String criterion return switch (criterion.toUpperCase()) { case "FAILED" -> evaluateNonConvergence(result); case "LIMIT_VIOLATIONS" -> evaluateLimitViolations(result); - case "SPS_TRIGGERED" -> evaluateSpsTriggered(result); + case "SPS_TRIGGERED" -> evaluateSpsTriggered(); default -> { LOGGER.warn("Unknown criterion: {}", criterion); yield false; @@ -64,7 +64,7 @@ private boolean evaluateLimitViolations(PostContingencyResult result) { return hasViolations; } - private boolean evaluateSpsTriggered(PostContingencyResult result) { + private boolean evaluateSpsTriggered() { return true; } } diff --git a/security-analysis/mixed-security-analysis/src/main/java/com/powsybl/mixed/security/analysis/criteria/SwitchDecision.java b/security-analysis/mixed-security-analysis-api/src/main/java/com/powsybl/mixed/security/analysis/criteria/SwitchDecision.java similarity index 100% rename from security-analysis/mixed-security-analysis/src/main/java/com/powsybl/mixed/security/analysis/criteria/SwitchDecision.java rename to security-analysis/mixed-security-analysis-api/src/main/java/com/powsybl/mixed/security/analysis/criteria/SwitchDecision.java diff --git a/security-analysis/mixed-security-analysis/src/main/java/com/powsybl/mixed/security/analysis/parameters/MixedModeParametersExtension.java b/security-analysis/mixed-security-analysis-api/src/main/java/com/powsybl/mixed/security/analysis/parameters/MixedModeParametersExtension.java similarity index 100% rename from security-analysis/mixed-security-analysis/src/main/java/com/powsybl/mixed/security/analysis/parameters/MixedModeParametersExtension.java rename to security-analysis/mixed-security-analysis-api/src/main/java/com/powsybl/mixed/security/analysis/parameters/MixedModeParametersExtension.java diff --git a/security-analysis/mixed-security-analysis/src/test/java/com/powsybl/mixed/security/analysis/MixedSecurityAnalysisProviderTest.java b/security-analysis/mixed-security-analysis-api/src/test/java/com/powsybl/mixed/security/analysis/MixedSecurityAnalysisProviderTest.java similarity index 100% rename from security-analysis/mixed-security-analysis/src/test/java/com/powsybl/mixed/security/analysis/MixedSecurityAnalysisProviderTest.java rename to security-analysis/mixed-security-analysis-api/src/test/java/com/powsybl/mixed/security/analysis/MixedSecurityAnalysisProviderTest.java diff --git a/security-analysis/mixed-security-analysis/src/test/java/com/powsybl/mixed/security/analysis/MixedSecurityAnalysisTest.java b/security-analysis/mixed-security-analysis-api/src/test/java/com/powsybl/mixed/security/analysis/MixedSecurityAnalysisTest.java similarity index 99% rename from security-analysis/mixed-security-analysis/src/test/java/com/powsybl/mixed/security/analysis/MixedSecurityAnalysisTest.java rename to security-analysis/mixed-security-analysis-api/src/test/java/com/powsybl/mixed/security/analysis/MixedSecurityAnalysisTest.java index ed1adb16228..e281ffdf547 100644 --- a/security-analysis/mixed-security-analysis/src/test/java/com/powsybl/mixed/security/analysis/MixedSecurityAnalysisTest.java +++ b/security-analysis/mixed-security-analysis-api/src/test/java/com/powsybl/mixed/security/analysis/MixedSecurityAnalysisTest.java @@ -18,7 +18,6 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -185,7 +184,7 @@ void testRunHandlesException() { } @Test - void testSwitchOnSpsTriggered() throws IOException { + void testSwitchOnSpsTriggered() { // 1. Load the network Network network = EurostagTutorialExample1Factory.create(); diff --git a/security-analysis/mixed-security-analysis/src/test/java/com/powsybl/mixed/security/analysis/criteria/AnalysisSwitchCriteriaTest.java b/security-analysis/mixed-security-analysis-api/src/test/java/com/powsybl/mixed/security/analysis/criteria/AnalysisSwitchCriteriaTest.java similarity index 100% rename from security-analysis/mixed-security-analysis/src/test/java/com/powsybl/mixed/security/analysis/criteria/AnalysisSwitchCriteriaTest.java rename to security-analysis/mixed-security-analysis-api/src/test/java/com/powsybl/mixed/security/analysis/criteria/AnalysisSwitchCriteriaTest.java diff --git a/security-analysis/mixed-security-analysis/src/test/java/com/powsybl/mixed/security/analysis/criteria/SwitchDecisionTest.java b/security-analysis/mixed-security-analysis-api/src/test/java/com/powsybl/mixed/security/analysis/criteria/SwitchDecisionTest.java similarity index 100% rename from security-analysis/mixed-security-analysis/src/test/java/com/powsybl/mixed/security/analysis/criteria/SwitchDecisionTest.java rename to security-analysis/mixed-security-analysis-api/src/test/java/com/powsybl/mixed/security/analysis/criteria/SwitchDecisionTest.java diff --git a/security-analysis/mixed-security-analysis/src/test/java/com/powsybl/mixed/security/analysis/parameters/MixedModeParametersExtensionTest.java b/security-analysis/mixed-security-analysis-api/src/test/java/com/powsybl/mixed/security/analysis/parameters/MixedModeParametersExtensionTest.java similarity index 100% rename from security-analysis/mixed-security-analysis/src/test/java/com/powsybl/mixed/security/analysis/parameters/MixedModeParametersExtensionTest.java rename to security-analysis/mixed-security-analysis-api/src/test/java/com/powsybl/mixed/security/analysis/parameters/MixedModeParametersExtensionTest.java diff --git a/security-analysis/pom.xml b/security-analysis/pom.xml index 2f26a903623..c568eeb86d5 100644 --- a/security-analysis/pom.xml +++ b/security-analysis/pom.xml @@ -25,7 +25,7 @@ security-analysis-api - mixed-security-analysis + mixed-security-analysis-api