diff --git a/src/main/App.kt b/src/main/App.kt index 7f56770..f88c2ee 100644 --- a/src/main/App.kt +++ b/src/main/App.kt @@ -111,6 +111,29 @@ fun Application.module() { call.respondText(diagram, ContentType.Image.SVG, HttpStatusCode.OK) } } + + post("/sensitivity-analysis") { + val loadParamCnt = LoadParameterContainer() + val sensParamCnt = SensitivityAnalysisParametersContainer() + val sensFactorCnt = SensitivityFactorContainer() + val contingencyCnt = ContingencyListContainer() + val itemHandler = MultiFormItemLoaders(listOf(loadParamCnt, sensParamCnt, sensFactorCnt, contingencyCnt)) + + val files = multiPartDataHandler(call.receiveMultipart(), itemHandler::formItemHandler) + if (files.isEmpty()) { + call.response.status(HttpStatusCode.UnprocessableEntity) + } else { + sensParamCnt.parameters.setLoadFlowParameters(loadParamCnt.parameters) + val network = networkFromFileContent(files[0]) + val result = runSensitivityAnalysis( + network, + sensFactorCnt.factors, + sensParamCnt.parameters, + contingencyCnt.contingencies + ) + call.respondText(result, ContentType.Application.Json, HttpStatusCode.OK) + } + } swaggerUI(path = "openapi", swaggerFile = "openapi/documentation.yaml") } } diff --git a/src/main/ContingencyListContainer.kt b/src/main/ContingencyListContainer.kt index 5c7455e..c32caf0 100644 --- a/src/main/ContingencyListContainer.kt +++ b/src/main/ContingencyListContainer.kt @@ -1,6 +1,7 @@ package com.github.statnett.loadflowservice import com.powsybl.contingency.contingency.list.ContingencyList +import com.powsybl.contingency.contingency.list.DefaultContingencyList import com.powsybl.contingency.json.JsonContingencyListLoader import io.github.oshai.kotlinlogging.KotlinLogging import io.ktor.http.content.* @@ -8,7 +9,7 @@ import io.ktor.http.content.* private val logger = KotlinLogging.logger {} class ContingencyListContainer : AutoVersionableJsonParser(), FormItemLoadable { - var contingencies: ContingencyList? = null + var contingencies: ContingencyList = DefaultContingencyList() override fun currentVersion(): String { return ContingencyList.VERSION diff --git a/src/main/MultiFormItemLoaders.kt b/src/main/MultiFormItemLoaders.kt new file mode 100644 index 0000000..4c83c04 --- /dev/null +++ b/src/main/MultiFormItemLoaders.kt @@ -0,0 +1,10 @@ +package com.github.statnett.loadflowservice + +import io.ktor.http.content.* + +class MultiFormItemLoaders(private val loaders: List) : FormItemLoadable { + + override fun formItemHandler(part: PartData.FormItem) { + this.loaders.forEach { loader -> loader.formItemHandler(part) } + } +} \ No newline at end of file diff --git a/src/main/SensitivityFactorContainer.kt b/src/main/SensitivityFactorContainer.kt index 34fcb97..e0c803d 100644 --- a/src/main/SensitivityFactorContainer.kt +++ b/src/main/SensitivityFactorContainer.kt @@ -51,5 +51,4 @@ class SensitivityFactorContainer : FormItemLoadable { logger.info { "Received sensitivity factors parameters: ${part.value}" } } } - } \ No newline at end of file diff --git a/src/main/Solver.kt b/src/main/Solver.kt index a501220..dd3b968 100644 --- a/src/main/Solver.kt +++ b/src/main/Solver.kt @@ -1,13 +1,16 @@ package com.github.statnett.loadflowservice 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.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.sensitivity.* import io.github.oshai.kotlinlogging.KotlinLogging import java.io.ByteArrayOutputStream import java.io.StringWriter @@ -90,3 +93,43 @@ fun solve( report = reporterToString(reporter) ) } + +fun runSensitivityAnalysis( + network: Network, + factors: List, + params: SensitivityAnalysisParameters, + contingenciesList: ContingencyList +): String { + val reporter = ReporterModel("sensitivity", "") + val variableSets: List = listOf() + val contingencies = contingenciesList.getContingencies(network) + val factorReader = SensitivityFactorModelReader(factors, network) + + val factory = JsonUtil.createJsonFactory() + val writer = StringWriter() + val jsonGenerator = factory.createGenerator(writer) + jsonGenerator.writeStartObject() + jsonGenerator.writeFieldName("sensitivity-results") + val resultWriter = SensitivityResultJsonWriter(jsonGenerator, contingencies) + + SensitivityAnalysis.run( + network, + network.variantManager.workingVariantId, + factorReader, + resultWriter, + contingencies, + variableSets, + params, + LocalComputationManager.getDefault(), + reporter + ) + // Close the nested array created by Powsybl + jsonGenerator.writeEndArray() + jsonGenerator.writeEndArray() + + // Add run report to the JSON + jsonGenerator.writeStringField("report", reporterToString(reporter)) + jsonGenerator.writeEndObject() + jsonGenerator.close() + return writer.toString() +} \ No newline at end of file diff --git a/src/test/AppTest.kt b/src/test/AppTest.kt index 003a56a..78ffc26 100644 --- a/src/test/AppTest.kt +++ b/src/test/AppTest.kt @@ -7,6 +7,7 @@ import io.ktor.http.* import io.ktor.server.testing.* import org.junit.jupiter.api.DynamicTest import org.junit.jupiter.api.TestFactory +import testDataFactory.* import kotlin.math.abs import kotlin.test.Test import kotlin.test.assertContains @@ -14,6 +15,9 @@ import kotlin.test.assertEquals import kotlin.test.assertTrue class ApplicationTest { + // Holds various form data variants for the sensitivity-analysis end-point + val sensitivityFormData = SensitivityAnalysisFormDataContainer() + @Test fun testRoot() = testApplication { @@ -268,6 +272,29 @@ class ApplicationTest { val num = body.split("},{").size assertEquals(2, num) } + + @TestFactory + fun `test 200 response for all valid sensitivity inputs`() = allSensitivityAnalysisConfigs().map { config -> + DynamicTest.dynamicTest("Test 200 for config $config") { + testApplication { + val response = client.submitFormWithBinaryData( + url = "/sensitivity-analysis", + formData = sensitivityFormData.formData(config) + ) + assertEquals(HttpStatusCode.OK, response.status) + val body = response.bodyAsText() + + // There are two contingencies so when we have contingencies there should be three results + // Otherwise one + val numRes = if (config.withContingencies) 3 else 1 + + val regex = Regex(""""sensitivity-results":\[\[(.*)]]""") + val match = regex.find(body)!! + val content = match.groupValues[1] + assertEquals(numRes, content.count { c -> c == '{' }) + } + } + } } diff --git a/src/test/ContingencyListContainerTest.kt b/src/test/ContingencyListContainerTest.kt index 7a28f30..a4994bb 100644 --- a/src/test/ContingencyListContainerTest.kt +++ b/src/test/ContingencyListContainerTest.kt @@ -1,5 +1,6 @@ import com.github.statnett.loadflowservice.ContingencyListContainer import com.powsybl.ieeecdf.converter.IeeeCdfNetworkFactory +import testDataFactory.basicContingencyJson import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull @@ -13,7 +14,7 @@ class ContingencyListContainerTest { assertNotNull(container.contingencies) val network = IeeeCdfNetworkFactory.create9() - val contingencies = container.contingencies!!.getContingencies(network) + val contingencies = container.contingencies.getContingencies(network) assertEquals(2, contingencies.size) } } diff --git a/src/test/SensitivityFactorContainerTest.kt b/src/test/SensitivityFactorContainerTest.kt index 724dd0f..c5f7127 100644 --- a/src/test/SensitivityFactorContainerTest.kt +++ b/src/test/SensitivityFactorContainerTest.kt @@ -1,4 +1,5 @@ import com.github.statnett.loadflowservice.SensitivityFactorContainer +import testDataFactory.sensitivityFactorList import kotlin.test.Test import kotlin.test.assertEquals @@ -8,6 +9,5 @@ class SensitivityFactorContainerTest { val container = SensitivityFactorContainer() container.update(sensitivityFactorList()) assertEquals(1, container.factors.size) - } } diff --git a/src/test/SolverTest.kt b/src/test/SolverTest.kt index 536afe0..16cd12b 100644 --- a/src/test/SolverTest.kt +++ b/src/test/SolverTest.kt @@ -4,6 +4,7 @@ import com.github.statnett.loadflowservice.networkFromFileContent import com.powsybl.loadflow.LoadFlowParameters import com.powsybl.loadflow.json.JsonLoadFlowParameters import org.junit.Test +import testDataFactory.ieeeCdfNetwork14CgmesFile import java.io.ByteArrayInputStream import kotlin.test.assertEquals diff --git a/src/test/DataFactory.kt b/src/test/testDataFactory/DataFactory.kt similarity index 99% rename from src/test/DataFactory.kt rename to src/test/testDataFactory/DataFactory.kt index 13e684c..648c486 100644 --- a/src/test/DataFactory.kt +++ b/src/test/testDataFactory/DataFactory.kt @@ -1,3 +1,5 @@ +package testDataFactory + import com.powsybl.ieeecdf.converter.IeeeCdfNetworkFactory import io.ktor.client.request.forms.* import io.ktor.http.* diff --git a/src/test/testDataFactory/SensitivityRunFactory.kt b/src/test/testDataFactory/SensitivityRunFactory.kt new file mode 100644 index 0000000..d598f33 --- /dev/null +++ b/src/test/testDataFactory/SensitivityRunFactory.kt @@ -0,0 +1,145 @@ +package testDataFactory + +import com.github.statnett.loadflowservice.AutoSerializableSensitivityFactor +import io.ktor.client.request.forms.* +import io.ktor.http.content.* +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json + +@Serializable +data class Contingencies( + val type: String, + val version: String, + val name: String, + val contingencies: List +) + +@Serializable +data class Contingency( + val id: String, + val elements: List +) + +@Serializable +data class ContingencyElement( + val id: String, + val type: String, +) + +fun ieee14BusContingencies(): Contingencies { + val generator = ContingencyElement(id = "B3-G", type = "GENERATOR") + val br1 = ContingencyElement(id = "L7-8-1", type = "BRANCH") + val br2 = ContingencyElement(id = "L7-9-1", type = "BRANCH") + + return Contingencies( + type = "default", + version = "1.0", + name = "list", + contingencies = listOf( + Contingency(id = "generatorContingency", listOf(generator)), + Contingency(id = "branchContingency", listOf(br1, br2)) + ) + ) +} + +fun ieee14SensitivityFactor(): List { + return listOf( + AutoSerializableSensitivityFactor( + functionType = "BRANCH_ACTIVE_POWER_2", + functionId = "L1-2-1", + variableType = "INJECTION_ACTIVE_POWER", + variableId = "B2-G", + variableSet = false, + contingencyContextType = "ALL" + ) + ) +} + +fun ieee14SensitivityLoadParams(): String { + return "{\"dc\": true}" +} + +fun ieee14SensitivityParams(): String { + return "{\"voltage-voltage-sensitivity-value-threshold\": 0.001}" +} + +data class SensitivityAnalysisConfig( + val withContingencies: Boolean, + val withLoadParameters: Boolean, + val withSensitivityParameters: Boolean +) + +fun allSensitivityAnalysisConfigs(): List { + val options = listOf(true, false) + return options.map { withCtg -> + options.map { withLp -> + options.map { withSensParam -> + SensitivityAnalysisConfig(withCtg, withLp, withSensParam) + } + }.flatten() + }.flatten() +} + +fun loadParams(): List { + return formData { + append( + "load-parameters", + ieee14SensitivityLoadParams() + ) + } +} + +fun sensFactors(): List { + return formData { + append( + "sensitivity-factors", + Json.encodeToString(ieee14SensitivityFactor()) + ) + } +} + +fun contingencies(): List { + return formData { + append( + "contingencies", + Json.encodeToString(ieee14BusContingencies()) + ) + } +} + +fun sensParams(): List { + return formData { + append( + "sensitivity-analysis-parameters", + ieee14SensitivityParams() + ) + } +} + +data class SensitivityAnalysisFormDataContainer( + val network: List = formDataFromFile(ieeeCdfNetwork14CgmesFile()), + val loadParams: List = loadParams(), + val sensFactors: List = sensFactors(), + val contingencies: List = contingencies(), + val sensParams: List = sensParams() +) { + fun formData(config: SensitivityAnalysisConfig): List { + val parts: MutableList = arrayListOf() + parts += network + parts += sensFactors + + if (config.withContingencies) { + parts += contingencies + } + + if (config.withSensitivityParameters) { + parts += sensParams + } + + if (config.withLoadParameters) { + parts += loadParams + } + return parts + } +} \ No newline at end of file