From eef1ececaca99a950d8ae166a24ce74bd5ab9ec0 Mon Sep 17 00:00:00 2001 From: David Kleiven Date: Sun, 19 Nov 2023 21:12:46 +0100 Subject: [PATCH] Add simple security analysis --- src/main/ApiDataModels.kt | 18 ++++++++- src/main/ApiUtil.kt | 5 +++ src/main/App.kt | 33 +++++++++++++++ src/main/JavaSerializers.kt | 32 +++++++++++++++ src/main/Solver.kt | 40 +++++++++++++++++++ .../ContingencyListContainer.kt | 10 ++++- .../SecurityAnalysisParametersContainer.kt | 29 ++++++++++++++ src/test/ApiDataModelsTest.kt | 13 ++++++ src/test/AppTest.kt | 18 +++++++++ .../testDataFactory/SecurityRunFactory.kt | 33 +++++++++++++++ 10 files changed, 229 insertions(+), 2 deletions(-) create mode 100644 src/main/JavaSerializers.kt create mode 100644 src/main/formItemHandlers/SecurityAnalysisParametersContainer.kt create mode 100644 src/test/testDataFactory/SecurityRunFactory.kt diff --git a/src/main/ApiDataModels.kt b/src/main/ApiDataModels.kt index 578ae9f..aaed82b 100644 --- a/src/main/ApiDataModels.kt +++ b/src/main/ApiDataModels.kt @@ -1,7 +1,16 @@ package com.github.statnett.loadflowservice +import com.fasterxml.jackson.databind.ObjectMapper import com.powsybl.iidm.network.Network +import com.powsybl.security.SecurityAnalysisResult +import com.powsybl.security.json.SecurityAnalysisJsonModule +import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder /** * Class for holding properties from the PowsbyBl bus class that are @@ -58,4 +67,11 @@ fun branchPropertiesFromNetwork(network: Network): List { terminal2 = TerminalProperties(line.terminal2.p, line.terminal2.q) ) }.toList() -} \ No newline at end of file +} + +@Serializable +data class LoadFlowServiceSecurityAnalysisResult( + @Serializable(with = SecurityAnalysisResultSerializer::class) + val securityAnalysisResult: SecurityAnalysisResult, + val report: String +) \ No newline at end of file diff --git a/src/main/ApiUtil.kt b/src/main/ApiUtil.kt index 47f9873..6d0bec9 100644 --- a/src/main/ApiUtil.kt +++ b/src/main/ApiUtil.kt @@ -160,18 +160,23 @@ fun modelObjectNames(name: String, network: Network): List { substation -> { substationNames(network) } + voltageLevel -> { voltageLevelNames(network) } + generators -> { generatorNames(network) } + loads -> { loadNames(network) } + branches -> { branchNames(network) } + else -> { val allowed = listOf(substation, voltageLevel, generators, loads, branches) throw UnknownRouteException("Unknown object type $name. Must be one of $allowed") diff --git a/src/main/App.kt b/src/main/App.kt index d7f486c..4964e62 100644 --- a/src/main/App.kt +++ b/src/main/App.kt @@ -1,6 +1,10 @@ package com.github.statnett.loadflowservice import com.github.statnett.loadflowservice.formItemHandlers.* +import com.powsybl.security.action.Action +import com.powsybl.security.interceptors.SecurityAnalysisInterceptor +import com.powsybl.security.monitor.StateMonitor +import com.powsybl.security.strategy.OperatorStrategy import io.ktor.http.* import io.ktor.serialization.kotlinx.json.* import io.ktor.server.application.* @@ -101,6 +105,35 @@ fun Application.module() { ) call.respondText(result, ContentType.Application.Json, HttpStatusCode.OK) } + + post("/security-analysis") { + val loadParamCnt = LoadParameterContainer() + val securityParamsCnt = SecurityAnalysisParametersContainer() + val contingencyCnt = ContingencyListContainer() + val itemHandler = MultiFormItemLoaders(listOf(loadParamCnt, securityParamsCnt, contingencyCnt)) + + val files = multiPartDataHandler(call.receiveMultipart(), itemHandler::formItemHandler) + + securityParamsCnt.parameters.setLoadFlowParameters(loadParamCnt.parameters) + val network = networkFromFirstFile(files) + + // Initialize preliminary empty things in first version + val intersceptors: List = listOf() + val operatorStrategies: List = listOf() + val actions: List = listOf() + val monitors: List = listOf() + + val result = runSecurityAnalysis( + network, + securityParamsCnt.parameters, + contingencyCnt, + intersceptors, + operatorStrategies, + actions, + monitors + ) + call.respond(result) + } swaggerUI(path = "openapi", swaggerFile = "openapi/documentation.yaml") } } diff --git a/src/main/JavaSerializers.kt b/src/main/JavaSerializers.kt new file mode 100644 index 0000000..95a896c --- /dev/null +++ b/src/main/JavaSerializers.kt @@ -0,0 +1,32 @@ +package com.github.statnett.loadflowservice + +import com.fasterxml.jackson.databind.ObjectMapper +import com.powsybl.security.SecurityAnalysisResult +import com.powsybl.security.json.SecurityAnalysisJsonModule +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +object SecurityAnalysisResultSerializer : KSerializer { + private val mapper = ObjectMapper() + + init { + mapper.registerModule(SecurityAnalysisJsonModule()) + } + + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("security-analysis-report", PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: SecurityAnalysisResult) { + val string = mapper.writeValueAsString(value) + encoder.encodeString(string) + } + + override fun deserialize(decoder: Decoder): SecurityAnalysisResult { + val string = decoder.decodeString() + return mapper.readValue(string, SecurityAnalysisResult::class.java) + } +} \ No newline at end of file diff --git a/src/main/Solver.kt b/src/main/Solver.kt index 354aaf7..9beef75 100644 --- a/src/main/Solver.kt +++ b/src/main/Solver.kt @@ -1,15 +1,28 @@ package com.github.statnett.loadflowservice +import com.fasterxml.jackson.databind.ObjectMapper import com.powsybl.commons.PowsyblException import com.powsybl.commons.json.JsonUtil import com.powsybl.commons.reporter.Reporter import com.powsybl.commons.reporter.ReporterModel import com.powsybl.computation.local.LocalComputationManager +import com.powsybl.contingency.ContingenciesProvider import com.powsybl.contingency.contingency.list.ContingencyList import com.powsybl.iidm.network.* import com.powsybl.loadflow.LoadFlow import com.powsybl.loadflow.LoadFlowParameters import com.powsybl.loadflow.json.JsonLoadFlowParameters +import com.powsybl.security.LimitViolationFilter +import com.powsybl.security.SecurityAnalysis +import com.powsybl.security.SecurityAnalysisParameters +import com.powsybl.security.SecurityAnalysisReport +import com.powsybl.security.SecurityAnalysisResult +import com.powsybl.security.action.Action +import com.powsybl.security.detectors.DefaultLimitViolationDetector +import com.powsybl.security.interceptors.SecurityAnalysisInterceptor +import com.powsybl.security.json.SecurityAnalysisJsonModule +import com.powsybl.security.monitor.StateMonitor +import com.powsybl.security.strategy.OperatorStrategy import com.powsybl.sensitivity.* import io.github.oshai.kotlinlogging.KotlinLogging import java.io.ByteArrayOutputStream @@ -141,4 +154,31 @@ fun runSensitivityAnalysis( jsonGenerator.writeEndObject() jsonGenerator.close() return writer.toString() +} + +fun runSecurityAnalysis( + network: Network, + params: SecurityAnalysisParameters, + contingencies: ContingenciesProvider, + intersceptors: List, + operatorStrategies: List, + actions: List, + monitors: List +): LoadFlowServiceSecurityAnalysisResult { + val reporter = ReporterModel("security", "") + val securityReport = SecurityAnalysis.run( + network, + network.variantManager.workingVariantId, + contingencies, + params, + LocalComputationManager.getDefault(), + LimitViolationFilter.load(), + DefaultLimitViolationDetector(), + intersceptors, + operatorStrategies, + actions, + monitors, + reporter + ) + return LoadFlowServiceSecurityAnalysisResult(securityReport.result, reporterToString(reporter)) } \ No newline at end of file diff --git a/src/main/formItemHandlers/ContingencyListContainer.kt b/src/main/formItemHandlers/ContingencyListContainer.kt index ebde53a..3df64af 100644 --- a/src/main/formItemHandlers/ContingencyListContainer.kt +++ b/src/main/formItemHandlers/ContingencyListContainer.kt @@ -1,14 +1,18 @@ package com.github.statnett.loadflowservice.formItemHandlers +import com.powsybl.contingency.ContingenciesProvider +import com.powsybl.contingency.Contingency import com.powsybl.contingency.contingency.list.ContingencyList import com.powsybl.contingency.contingency.list.DefaultContingencyList import com.powsybl.contingency.json.JsonContingencyListLoader +import com.powsybl.iidm.network.Network import io.github.oshai.kotlinlogging.KotlinLogging import io.ktor.http.content.* private val logger = KotlinLogging.logger {} -class ContingencyListContainer : AutoVersionableJsonParser(), FormItemLoadable { + +class ContingencyListContainer : AutoVersionableJsonParser(), FormItemLoadable, ContingenciesProvider { var contingencies: ContingencyList = DefaultContingencyList() override fun currentVersion(): String { @@ -27,4 +31,8 @@ class ContingencyListContainer : AutoVersionableJsonParser(), FormItemLoadable { logger.info { "Received contingencies: ${part.value}" } } } + + override fun getContingencies(network: Network): List { + return contingencies.getContingencies(network) + } } \ No newline at end of file diff --git a/src/main/formItemHandlers/SecurityAnalysisParametersContainer.kt b/src/main/formItemHandlers/SecurityAnalysisParametersContainer.kt new file mode 100644 index 0000000..9851329 --- /dev/null +++ b/src/main/formItemHandlers/SecurityAnalysisParametersContainer.kt @@ -0,0 +1,29 @@ +package com.github.statnett.loadflowservice.formItemHandlers + +import com.powsybl.security.SecurityAnalysisParameters +import com.powsybl.security.json.JsonSecurityAnalysisParameters +import io.github.oshai.kotlinlogging.KotlinLogging +import io.ktor.http.content.* + +private val logger = KotlinLogging.logger {} + +class SecurityAnalysisParametersContainer : AutoVersionableJsonParser(), FormItemLoadable { + var parameters = SecurityAnalysisParameters() + + override fun currentVersion(): String { + return SecurityAnalysisParameters.VERSION + } + + fun update(jsonString: String) { + val withVersion = jsonStringWithVersion(jsonString) + this.parameters = JsonSecurityAnalysisParameters.update(this.parameters, withVersion.byteInputStream()) + } + + override fun formItemHandler(part: PartData.FormItem) { + val name = part.name ?: "" + if (name == "security-analysis-parameters") { + this.update(part.value) + logger.info { "Received security analysis parameters: ${part.value}" } + } + } +} diff --git a/src/test/ApiDataModelsTest.kt b/src/test/ApiDataModelsTest.kt index b9fa7ca..91dd8b4 100644 --- a/src/test/ApiDataModelsTest.kt +++ b/src/test/ApiDataModelsTest.kt @@ -1,5 +1,9 @@ +import com.github.statnett.loadflowservice.LoadFlowServiceSecurityAnalysisResult import com.github.statnett.loadflowservice.busPropertiesFromNetwork import com.powsybl.ieeecdf.converter.IeeeCdfNetworkFactory +import com.powsybl.security.SecurityAnalysisResult +import kotlinx.serialization.json.Json +import kotlinx.serialization.encodeToString import kotlin.test.Test import kotlin.test.assertEquals @@ -10,4 +14,13 @@ class ApiDataModelTest { val buses = busPropertiesFromNetwork(network) assertEquals(buses.count(), 14) } + + @Test + fun `serialize deserialize round trip should be give the same security analysis report`() { + val emptyReport = SecurityAnalysisResult.empty() + val result = LoadFlowServiceSecurityAnalysisResult(emptyReport, "some run report") + val serialized = Json.encodeToString(result) + val deserialized = Json.decodeFromString(serialized) + assertEquals(deserialized.report, result.report) + } } diff --git a/src/test/AppTest.kt b/src/test/AppTest.kt index d012fad..0b6767f 100644 --- a/src/test/AppTest.kt +++ b/src/test/AppTest.kt @@ -1,3 +1,4 @@ +import com.github.statnett.loadflowservice.LoadFlowServiceSecurityAnalysisResult import com.github.statnett.loadflowservice.busPropertiesFromNetwork import com.powsybl.ieeecdf.converter.IeeeCdfNetworkFactory import io.ktor.client.call.* @@ -6,6 +7,7 @@ import io.ktor.client.request.forms.* import io.ktor.client.statement.* import io.ktor.http.* import io.ktor.server.testing.* +import kotlinx.serialization.json.Json import org.junit.jupiter.api.DynamicTest import org.junit.jupiter.api.TestFactory import testDataFactory.* @@ -18,6 +20,7 @@ import kotlin.test.assertTrue class ApplicationTest { // Holds various form data variants for the sensitivity-analysis end-point val sensitivityFormData = SensitivityAnalysisFormDataContainer() + val securityFormData = SecurityAnalysisFormDataContainer() @Test fun testRoot() = @@ -366,6 +369,21 @@ class ApplicationTest { } + @Test + fun `test 200 for simple security analysis`() { + testApplication { + val response = client.submitFormWithBinaryData( + url = "/security-analysis", + formData = securityFormData.formData() + ) + assertEquals(HttpStatusCode.OK, response.status) + val result = Json.decodeFromString(response.body()) + assertTrue(result.report.isNotEmpty()) + // There are two contingencies + assertEquals(2, result.securityAnalysisResult.postContingencyResults.size) + } + } + } diff --git a/src/test/testDataFactory/SecurityRunFactory.kt b/src/test/testDataFactory/SecurityRunFactory.kt new file mode 100644 index 0000000..01a58d3 --- /dev/null +++ b/src/test/testDataFactory/SecurityRunFactory.kt @@ -0,0 +1,33 @@ +package testDataFactory + +import io.ktor.client.request.forms.* +import io.ktor.http.content.* + +fun ieee14SecurityParams(): String { + return "{\"flow-proportional-threshold\": 0.2}" +} + +fun securityParams(): List { + return formData { + append( + "security-analysis-parameters", + ieee14SecurityParams() + ) + } +} + +data class SecurityAnalysisFormDataContainer( + val network: List = formDataFromFile(ieeeCdfNetwork14CgmesFile()), + val loadParams: List = loadParams(), + val securityParams: List = securityParams(), + val contingencies: List = contingencies(), +) { + fun formData(): List { + val parts: MutableList = arrayListOf() + parts += network + parts += contingencies + //parts += securityParams + parts += loadParams + return parts + } +}