-
Notifications
You must be signed in to change notification settings - Fork 56
Mixed security analysis #3871
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Mixed security analysis #3871
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| <?xml version="1.0" encoding="UTF-8"?> | ||
| <project xmlns="http://maven.apache.org/POM/4.0.0" | ||
| xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
| xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||
| <modelVersion>4.0.0</modelVersion> | ||
| <parent> | ||
| <groupId>com.powsybl</groupId> | ||
| <artifactId>powsybl-security-analysis</artifactId> | ||
| <version>7.3.0-SNAPSHOT</version> | ||
| </parent> | ||
|
|
||
| <artifactId>mixed-security-analysis-api</artifactId> | ||
| <name>Mixed-Mode Security Analysis API</name> | ||
| <description>An API for running mixed-mode (static and dynamic) security analyses.</description> | ||
|
|
||
| <properties> | ||
| <maven.compiler.source>21</maven.compiler.source> | ||
| <maven.compiler.target>21</maven.compiler.target> | ||
| <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> | ||
| </properties> | ||
|
|
||
| <dependencies> | ||
| <dependency> | ||
| <groupId>com.fasterxml.jackson.core</groupId> | ||
| <artifactId>jackson-databind</artifactId> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>com.powsybl</groupId> | ||
| <artifactId>powsybl-security-analysis-api</artifactId> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>com.powsybl</groupId> | ||
| <artifactId>powsybl-open-loadflow</artifactId> | ||
| <version>2.2.0</version> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>com.powsybl</groupId> | ||
| <artifactId>powsybl-iidm-serde</artifactId> | ||
| <version>${project.version}</version> | ||
| </dependency> | ||
| <!-- Test dependencies --> | ||
| <dependency> | ||
| <groupId>com.google.jimfs</groupId> | ||
| <artifactId>jimfs</artifactId> | ||
| <scope>test</scope> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>org.junit.jupiter</groupId> | ||
| <artifactId>junit-jupiter</artifactId> | ||
| <scope>test</scope> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>org.assertj</groupId> | ||
| <artifactId>assertj-core</artifactId> | ||
| <scope>test</scope> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>org.mockito</groupId> | ||
| <artifactId>mockito-core</artifactId> | ||
| <scope>test</scope> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>org.slf4j</groupId> | ||
| <artifactId>log4j-over-slf4j</artifactId> | ||
| <scope>test</scope> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>org.slf4j</groupId> | ||
| <artifactId>slf4j-simple</artifactId> | ||
| <scope>test</scope> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>${project.groupId}</groupId> | ||
| <artifactId>powsybl-commons-test</artifactId> | ||
| <version>${project.version}</version> | ||
| <scope>test</scope> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>${project.groupId}</groupId> | ||
| <artifactId>powsybl-config-test</artifactId> | ||
| <version>${project.version}</version> | ||
| <scope>test</scope> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>${project.groupId}</groupId> | ||
| <artifactId>powsybl-iidm-impl</artifactId> | ||
| <version>${project.version}</version> | ||
| <scope>test</scope> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>${project.groupId}</groupId> | ||
| <artifactId>powsybl-iidm-test</artifactId> | ||
| <version>${project.version}</version> | ||
| <scope>test</scope> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>${project.groupId}</groupId> | ||
| <artifactId>powsybl-tools-test</artifactId> | ||
| <version>${project.version}</version> | ||
| <scope>test</scope> | ||
| </dependency> | ||
| </dependencies> | ||
|
|
||
| </project> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 <riad.benradi at rte-france.com>} | ||
| */ | ||
| public class MixedSecurityAnalysis { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. HybridSecurityAnalysis would be better |
||
| 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<SecurityAnalysisProvider> 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<SecurityAnalysisProvider> 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<SecurityAnalysisReport> run() { | ||
| LOGGER.info("Starting mixed-mode security analysis"); | ||
| LOGGER.debug("Static simulator: {}, Dynamic simulator: {}", extension.getStaticSimulator(), extension.getDynamicSimulator()); | ||
|
|
||
| // Step 1: Get all contingencies | ||
| List<Contingency> 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<SecurityAnalysisReport> 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<String> 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<String> 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<SecurityAnalysisReport> runDynamicAnalysis(List<String> contingencyIds, | ||
| List<Contingency> allContingencies) { | ||
| LOGGER.info("Starting dynamic analysis pass for {} contingencies", contingencyIds.size()); | ||
|
|
||
| ContingenciesProvider filteredProvider = network -> | ||
|
Check warning on line 127 in security-analysis/mixed-security-analysis-api/src/main/java/com/powsybl/mixed/security/analysis/MixedSecurityAnalysis.java
|
||
| 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<String, PostContingencyResult> dynamicResultsMap = dynamicResult.getPostContingencyResults() | ||
| .stream() | ||
| .collect(Collectors.toMap(r -> r.getContingency().getId(), r -> r)); | ||
|
|
||
| // Merge: use dynamic result if available, otherwise use static | ||
| List<PostContingencyResult> 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<String, SecurityAnalysisProvider> 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; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are creating a cycling dependency! We cannot have a dependency on OLF in powsybl core