Skip to content

Commit

Permalink
Add sensitivity analysis
Browse files Browse the repository at this point in the history
  • Loading branch information
davidkleiven committed Oct 10, 2023
1 parent ed063c3 commit edb43c5
Show file tree
Hide file tree
Showing 11 changed files with 256 additions and 4 deletions.
23 changes: 23 additions & 0 deletions src/main/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}
3 changes: 2 additions & 1 deletion src/main/ContingencyListContainer.kt
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
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.*

private val logger = KotlinLogging.logger {}

class ContingencyListContainer : AutoVersionableJsonParser(), FormItemLoadable {
var contingencies: ContingencyList? = null
var contingencies: ContingencyList = DefaultContingencyList()

override fun currentVersion(): String {
return ContingencyList.VERSION
Expand Down
10 changes: 10 additions & 0 deletions src/main/MultiFormItemLoaders.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.github.statnett.loadflowservice

import io.ktor.http.content.*

class MultiFormItemLoaders(private val loaders: List<FormItemLoadable>) : FormItemLoadable {

override fun formItemHandler(part: PartData.FormItem) {
this.loaders.forEach { loader -> loader.formItemHandler(part) }
}
}
1 change: 0 additions & 1 deletion src/main/SensitivityFactorContainer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,4 @@ class SensitivityFactorContainer : FormItemLoadable {
logger.info { "Received sensitivity factors parameters: ${part.value}" }
}
}

}
43 changes: 43 additions & 0 deletions src/main/Solver.kt
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -90,3 +93,43 @@ fun solve(
report = reporterToString(reporter)
)
}

fun runSensitivityAnalysis(
network: Network,
factors: List<SensitivityFactor>,
params: SensitivityAnalysisParameters,
contingenciesList: ContingencyList
): String {
val reporter = ReporterModel("sensitivity", "")
val variableSets: List<SensitivityVariableSet> = 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()
}
27 changes: 27 additions & 0 deletions src/test/AppTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,17 @@ 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
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 {
Expand Down Expand Up @@ -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 == '{' })
}
}
}
}


Expand Down
3 changes: 2 additions & 1 deletion src/test/ContingencyListContainerTest.kt
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)
}
}
2 changes: 1 addition & 1 deletion src/test/SensitivityFactorContainerTest.kt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import com.github.statnett.loadflowservice.SensitivityFactorContainer
import testDataFactory.sensitivityFactorList
import kotlin.test.Test
import kotlin.test.assertEquals

Expand All @@ -8,6 +9,5 @@ class SensitivityFactorContainerTest {
val container = SensitivityFactorContainer()
container.update(sensitivityFactorList())
assertEquals(1, container.factors.size)

}
}
1 change: 1 addition & 0 deletions src/test/SolverTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
package testDataFactory

import com.powsybl.ieeecdf.converter.IeeeCdfNetworkFactory
import io.ktor.client.request.forms.*
import io.ktor.http.*
Expand Down
145 changes: 145 additions & 0 deletions src/test/testDataFactory/SensitivityRunFactory.kt
Original file line number Diff line number Diff line change
@@ -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<Contingency>
)

@Serializable
data class Contingency(
val id: String,
val elements: List<ContingencyElement>
)

@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<AutoSerializableSensitivityFactor> {
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<SensitivityAnalysisConfig> {
val options = listOf(true, false)
return options.map { withCtg ->
options.map { withLp ->
options.map { withSensParam ->
SensitivityAnalysisConfig(withCtg, withLp, withSensParam)
}
}.flatten()
}.flatten()
}

fun loadParams(): List<PartData> {
return formData {
append(
"load-parameters",
ieee14SensitivityLoadParams()
)
}
}

fun sensFactors(): List<PartData> {
return formData {
append(
"sensitivity-factors",
Json.encodeToString(ieee14SensitivityFactor())
)
}
}

fun contingencies(): List<PartData> {
return formData {
append(
"contingencies",
Json.encodeToString(ieee14BusContingencies())
)
}
}

fun sensParams(): List<PartData> {
return formData {
append(
"sensitivity-analysis-parameters",
ieee14SensitivityParams()
)
}
}

data class SensitivityAnalysisFormDataContainer(
val network: List<PartData> = formDataFromFile(ieeeCdfNetwork14CgmesFile()),
val loadParams: List<PartData> = loadParams(),
val sensFactors: List<PartData> = sensFactors(),
val contingencies: List<PartData> = contingencies(),
val sensParams: List<PartData> = sensParams()
) {
fun formData(config: SensitivityAnalysisConfig): List<PartData> {
val parts: MutableList<PartData> = arrayListOf()
parts += network
parts += sensFactors

if (config.withContingencies) {
parts += contingencies
}

if (config.withSensitivityParameters) {
parts += sensParams
}

if (config.withLoadParameters) {
parts += loadParams
}
return parts
}
}

0 comments on commit edb43c5

Please sign in to comment.