Skip to content

Commit

Permalink
Add simple security analysis
Browse files Browse the repository at this point in the history
  • Loading branch information
davidkleiven committed Nov 20, 2023
1 parent 03ab7d6 commit ecd81dd
Show file tree
Hide file tree
Showing 10 changed files with 229 additions and 2 deletions.
18 changes: 17 additions & 1 deletion src/main/ApiDataModels.kt
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -58,4 +67,11 @@ fun branchPropertiesFromNetwork(network: Network): List<LineProperties> {
terminal2 = TerminalProperties(line.terminal2.p, line.terminal2.q)
)
}.toList()
}
}

@Serializable
data class LoadFlowServiceSecurityAnalysisResult(
@Serializable(with = SecurityAnalysisResultSerializer::class)
val securityAnalysisResult: SecurityAnalysisResult,
val report: String
)
5 changes: 5 additions & 0 deletions src/main/ApiUtil.kt
Original file line number Diff line number Diff line change
Expand Up @@ -160,18 +160,23 @@ fun modelObjectNames(name: String, network: Network): List<String> {
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")
Expand Down
33 changes: 33 additions & 0 deletions src/main/App.kt
Original file line number Diff line number Diff line change
@@ -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.*
Expand Down Expand Up @@ -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<SecurityAnalysisInterceptor> = listOf()
val operatorStrategies: List<OperatorStrategy> = listOf()
val actions: List<Action> = listOf()
val monitors: List<StateMonitor> = listOf()

val result = runSecurityAnalysis(
network,
securityParamsCnt.parameters,
contingencyCnt,
intersceptors,
operatorStrategies,
actions,
monitors
)
call.respond(result)
}
swaggerUI(path = "openapi", swaggerFile = "openapi/documentation.yaml")
}
}
32 changes: 32 additions & 0 deletions src/main/JavaSerializers.kt
Original file line number Diff line number Diff line change
@@ -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<SecurityAnalysisResult> {
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)
}
}
40 changes: 40 additions & 0 deletions src/main/Solver.kt
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -141,4 +154,31 @@ fun runSensitivityAnalysis(
jsonGenerator.writeEndObject()
jsonGenerator.close()
return writer.toString()
}

fun runSecurityAnalysis(
network: Network,
params: SecurityAnalysisParameters,
contingencies: ContingenciesProvider,
intersceptors: List<SecurityAnalysisInterceptor>,
operatorStrategies: List<OperatorStrategy>,
actions: List<Action>,
monitors: List<StateMonitor>
): 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))
}
10 changes: 9 additions & 1 deletion src/main/formItemHandlers/ContingencyListContainer.kt
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -27,4 +31,8 @@ class ContingencyListContainer : AutoVersionableJsonParser(), FormItemLoadable {
logger.info { "Received contingencies: ${part.value}" }
}
}

override fun getContingencies(network: Network): List<Contingency> {
return contingencies.getContingencies(network)
}
}
29 changes: 29 additions & 0 deletions src/main/formItemHandlers/SecurityAnalysisParametersContainer.kt
Original file line number Diff line number Diff line change
@@ -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}" }
}
}
}
13 changes: 13 additions & 0 deletions src/test/ApiDataModelsTest.kt
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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<LoadFlowServiceSecurityAnalysisResult>(serialized)
assertEquals(deserialized.report, result.report)
}
}
18 changes: 18 additions & 0 deletions src/test/AppTest.kt
Original file line number Diff line number Diff line change
@@ -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.*
Expand All @@ -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.*
Expand All @@ -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() =
Expand Down Expand Up @@ -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<LoadFlowServiceSecurityAnalysisResult>(response.body())
assertTrue(result.report.isNotEmpty())
// There are two contingencies
assertEquals(2, result.securityAnalysisResult.postContingencyResults.size)
}
}


}

Expand Down
33 changes: 33 additions & 0 deletions src/test/testDataFactory/SecurityRunFactory.kt
Original file line number Diff line number Diff line change
@@ -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<PartData> {
return formData {
append(
"security-analysis-parameters",
ieee14SecurityParams()
)
}
}

data class SecurityAnalysisFormDataContainer(
val network: List<PartData> = formDataFromFile(ieeeCdfNetwork14CgmesFile()),
val loadParams: List<PartData> = loadParams(),
val securityParams: List<PartData> = securityParams(),
val contingencies: List<PartData> = contingencies(),
) {
fun formData(): List<PartData> {
val parts: MutableList<PartData> = arrayListOf()
parts += network
parts += contingencies
//parts += securityParams
parts += loadParams
return parts
}
}

0 comments on commit ecd81dd

Please sign in to comment.