From 7b9bc60c3c235ef21c2fe945c96effc0b98470c0 Mon Sep 17 00:00:00 2001 From: David Kleiven Date: Tue, 26 Sep 2023 07:34:31 +0200 Subject: [PATCH] Add solve endpoint --- src/main/ApiDataModels.kt | 18 ++++++++++++++++++ src/main/ApiUtil.kt | 27 ++++++++++++++++++++++++++- src/main/Application.kt | 14 ++++++++++++++ src/main/Solver.kt | 17 +++++++++++++---- src/test/ApplicationTest.kt | 28 ++++++++++++++++++++++++++++ 5 files changed, 99 insertions(+), 5 deletions(-) diff --git a/src/main/ApiDataModels.kt b/src/main/ApiDataModels.kt index 5632179..6dc0bd7 100644 --- a/src/main/ApiDataModels.kt +++ b/src/main/ApiDataModels.kt @@ -29,3 +29,21 @@ fun busPropertiesFromNetwork(network: Network): List { } .toList() } + +@Serializable +data class BranchProperties( + val id: String, + val isOverloaded: Boolean +) + +@Serializable +data class LoadFlowResultForApi(val isOk: Boolean, val buses: List, val branches: List) + +fun branchPropertiesFromNetwork(network: Network): List { + return network.lines.map { line -> + BranchProperties( + id = line.id, + isOverloaded = line.isOverloaded + ) + }.toList() +} \ No newline at end of file diff --git a/src/main/ApiUtil.kt b/src/main/ApiUtil.kt index e2f4afa..b8d3513 100644 --- a/src/main/ApiUtil.kt +++ b/src/main/ApiUtil.kt @@ -1,5 +1,8 @@ package com.github.statnett.loadflowservice +import com.powsybl.loadflow.LoadFlowParameters +import com.powsybl.loadflow.LoadFlowResult +import com.powsybl.loadflow.json.JsonLoadFlowParameters import io.ktor.http.content.* import java.io.ByteArrayInputStream @@ -13,10 +16,32 @@ fun busesFromRequest( class FileContent(val name: String, val bytes: ByteArray) -suspend fun multiPartDataHandler(multiPartData: MultiPartData): List { +/** + * Convenience class used to deserialize and update a load parameter instance + */ +class LoadParameterContainer() { + var parameters = LoadFlowParameters() + private var parametersModified = false + private fun update(jsonString: String) { + this.parameters = JsonLoadFlowParameters.update(this.parameters, ByteArrayInputStream(jsonString.toByteArray())) + this.parametersModified = true + } + + fun formItemHandler(part: PartData.FormItem) { + val name = part.name ?: "" + if (name == "load-parameters") { + this.update(part.value) + } + } +} + +suspend fun multiPartDataHandler(multiPartData: MultiPartData, formItemHandler: (part: PartData.FormItem) -> Unit = {}): List { val files = mutableListOf() multiPartData.forEachPart { part -> when (part) { + is PartData.FormItem -> { + formItemHandler(part) + } is PartData.FileItem -> { val name = part.originalFileName as String val content = part.streamProvider().readBytes() diff --git a/src/main/Application.kt b/src/main/Application.kt index 9ffd95a..774d13a 100644 --- a/src/main/Application.kt +++ b/src/main/Application.kt @@ -7,6 +7,7 @@ import io.ktor.server.plugins.contentnegotiation.* import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* +import java.io.ByteArrayInputStream fun main(args: Array): Unit = io.ktor.server.netty.EngineMain.main(args) @@ -34,5 +35,18 @@ fun Application.module() { get("/default-load-parameters") { call.respondText(defaultLoadFlowParameters(), ContentType.Application.Json, HttpStatusCode.OK) } + + post("/run-load-flow") { + val paramContainer = LoadParameterContainer() + val files = multiPartDataHandler(call.receiveMultipart(), paramContainer::formItemHandler) + + if (files.isEmpty()) { + call.response.status(HttpStatusCode.UnprocessableEntity) + } else { + val network = networkFromFileContent(files[0]) + val result = solve(network, paramContainer.parameters) + call.respond(result) + } + } } } diff --git a/src/main/Solver.kt b/src/main/Solver.kt index a4c55cb..06471d5 100644 --- a/src/main/Solver.kt +++ b/src/main/Solver.kt @@ -3,7 +3,9 @@ package com.github.statnett.loadflowservice import com.powsybl.iidm.network.Network import com.powsybl.loadflow.LoadFlow import com.powsybl.loadflow.LoadFlowParameters +import com.powsybl.loadflow.LoadFlowResult import com.powsybl.loadflow.json.JsonLoadFlowParameters +import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream import java.io.InputStream @@ -14,6 +16,10 @@ fun networkFromStream( return Network.read(fname, content) } +fun networkFromFileContent(content: FileContent): Network { + return networkFromStream(content.name, ByteArrayInputStream(content.bytes)) +} + fun defaultLoadFlowParameters(): String { val parameters = LoadFlowParameters() val stream = ByteArrayOutputStream() @@ -26,8 +32,11 @@ fun defaultLoadFlowParameters(): String { fun solve( network: Network, parameters: LoadFlowParameters, -) { - LoadFlow.run(network, parameters) +): LoadFlowResultForApi { + val result = LoadFlow.run(network, parameters) + return LoadFlowResultForApi( + isOk = result.isOk, + buses = busPropertiesFromNetwork(network), + branches = branchPropertiesFromNetwork(network) + ) } - - diff --git a/src/test/ApplicationTest.kt b/src/test/ApplicationTest.kt index f29b3a1..f400507 100644 --- a/src/test/ApplicationTest.kt +++ b/src/test/ApplicationTest.kt @@ -1,3 +1,4 @@ +import com.github.statnett.loadflowservice.busPropertiesFromNetwork import com.powsybl.ieeecdf.converter.IeeeCdfNetworkFactory import io.ktor.client.request.* import io.ktor.client.request.forms.formData @@ -11,6 +12,7 @@ import io.ktor.server.testing.testApplication import java.io.File import java.nio.file.Paths import java.util.Properties +import kotlin.math.abs import kotlin.test.Test import kotlin.test.assertContains import kotlin.test.assertEquals @@ -69,6 +71,32 @@ class ApplicationTest { assertTrue(body.startsWith("{")) assertTrue(body.endsWith("}")) } + + @Test + fun `test flow 14 bus network ok`() = + testApplication { + val response = client.submitFormWithBinaryData( + url = "/run-load-flow", + formData = formDataFromFile((ieeeCdfNetwork14File())) + ) + + assertEquals(response.status, HttpStatusCode.OK) + + val body: String = response.bodyAsText() + val solvedNetwork = IeeeCdfNetworkFactory.create14Solved() + val angles = busPropertiesFromNetwork(solvedNetwork).map { bus -> bus.angle }.toList() + + val regex = Regex("\"angle\":([0-9.-]+)") + val anglesFromJsonStr = regex.findAll(body).map {match -> match.groupValues[1].toDouble()}.toList() + + // It seems like the solved version from Powsybl contains rounded angles + assertTrue( + angles.zip(anglesFromJsonStr).all { + pair -> abs(pair.component1() - pair.component2()) < 0.01 + } + ) + } + } fun formDataFromFile(file: File): List {