diff --git a/security-analysis/mixed-security-analysis-api/pom.xml b/security-analysis/mixed-security-analysis-api/pom.xml
new file mode 100644
index 00000000000..cc4196763e2
--- /dev/null
+++ b/security-analysis/mixed-security-analysis-api/pom.xml
@@ -0,0 +1,104 @@
+
+
+ 4.0.0
+
+ com.powsybl
+ powsybl-security-analysis
+ 7.3.0-SNAPSHOT
+
+
+ mixed-security-analysis-api
+ Mixed-Mode Security Analysis API
+ An API for running mixed-mode (static and dynamic) security analyses.
+
+
+ 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-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-api/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
new file mode 100644
index 00000000000..1fc8b506054
--- /dev/null
+++ b/security-analysis/mixed-security-analysis-api/src/main/java/com/powsybl/mixed/security/analysis/MixedSecurityAnalysis.java
@@ -0,0 +1,204 @@
+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 = network ->
+ 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;
+ }
+}
diff --git a/security-analysis/mixed-security-analysis-api/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
new file mode 100644
index 00000000000..a91cab511d0
--- /dev/null
+++ b/security-analysis/mixed-security-analysis-api/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-api/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
new file mode 100644
index 00000000000..fd5b3a2a6d9
--- /dev/null
+++ b/security-analysis/mixed-security-analysis-api/src/main/java/com/powsybl/mixed/security/analysis/criteria/AnalysisSwitchCriteria.java
@@ -0,0 +1,70 @@
+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();
+ 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() {
+ return true;
+ }
+}
diff --git a/security-analysis/mixed-security-analysis-api/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
new file mode 100644
index 00000000000..2378289be23
--- /dev/null
+++ b/security-analysis/mixed-security-analysis-api/src/main/java/com/powsybl/mixed/security/analysis/criteria/SwitchDecision.java
@@ -0,0 +1,52 @@
+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-api/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
new file mode 100644
index 00000000000..95b2343614c
--- /dev/null
+++ b/security-analysis/mixed-security-analysis-api/src/main/java/com/powsybl/mixed/security/analysis/parameters/MixedModeParametersExtension.java
@@ -0,0 +1,61 @@
+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.
+ */
+ private String staticSimulator;
+
+ /**
+ * The name of the dynamic simulator to use for the second pass (for complex cases).
+ */
+ private String dynamicSimulator;
+
+ /**
+ * The list of criteria that trigger a switch to the dynamic simulator.
+ */
+ private List switchCriteria;
+
+ 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-api/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
new file mode 100644
index 00000000000..5e6e95748e3
--- /dev/null
+++ b/security-analysis/mixed-security-analysis-api/src/test/java/com/powsybl/mixed/security/analysis/MixedSecurityAnalysisProviderTest.java
@@ -0,0 +1,53 @@
+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-api/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
new file mode 100644
index 00000000000..e281ffdf547
--- /dev/null
+++ b/security-analysis/mixed-security-analysis-api/src/test/java/com/powsybl/mixed/security/analysis/MixedSecurityAnalysisTest.java
@@ -0,0 +1,264 @@
+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.ArgumentMatchers.anyString;
+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 testRunWithSwitchOnNotConverged() {
+ 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());
+ }
+
+ @Test
+ void testSwitchOnSpsTriggered() {
+ // 1. Load the network
+ Network network = EurostagTutorialExample1Factory.create();
+
+ // 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("SPS_TRIGGERED"));
+
+ // 4. Create the MixedSecurityAnalysis instance with the network and mock providers
+ MixedSecurityAnalysis analysis = new MixedSecurityAnalysis(
+ network, "InitialState", contingenciesProvider,
+ runParameters, ext,
+ Arrays.asList(staticProvider, dynamicProvider));
+
+ // 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
+ );
+ 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));
+
+ // 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));
+
+ // 7. Execute the mixed analysis
+ SecurityAnalysisReport report = analysis.run().join();
+
+ // 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));
+
+ 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,
+ 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
+ );
+ }
+}
diff --git a/security-analysis/mixed-security-analysis-api/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
new file mode 100644
index 00000000000..eb31947e5c2
--- /dev/null
+++ b/security-analysis/mixed-security-analysis-api/src/test/java/com/powsybl/mixed/security/analysis/criteria/AnalysisSwitchCriteriaTest.java
@@ -0,0 +1,136 @@
+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());
+ }
+
+ 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
+ );
+ }
+}
diff --git a/security-analysis/mixed-security-analysis-api/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
new file mode 100644
index 00000000000..207672b8930
--- /dev/null
+++ b/security-analysis/mixed-security-analysis-api/src/test/java/com/powsybl/mixed/security/analysis/criteria/SwitchDecisionTest.java
@@ -0,0 +1,49 @@
+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-api/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
new file mode 100644
index 00000000000..33c29fd7741
--- /dev/null
+++ b/security-analysis/mixed-security-analysis-api/src/test/java/com/powsybl/mixed/security/analysis/parameters/MixedModeParametersExtensionTest.java
@@ -0,0 +1,64 @@
+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..c568eeb86d5 100644
--- a/security-analysis/pom.xml
+++ b/security-analysis/pom.xml
@@ -25,6 +25,7 @@
security-analysis-api
+ mixed-security-analysis-api
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
}