Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Security #38

Merged
merged 2 commits into from
Nov 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
)
18 changes: 18 additions & 0 deletions src/main/ApiUtil.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package com.github.statnett.loadflowservice
import com.fasterxml.jackson.databind.ObjectMapper
import com.powsybl.iidm.network.Network
import com.powsybl.nad.NetworkAreaDiagram
import com.powsybl.security.SecurityAnalysisParameters
import com.powsybl.security.json.SecurityAnalysisJsonModule
import com.powsybl.sensitivity.SensitivityAnalysisParameters
import com.powsybl.sensitivity.json.SensitivityJsonModule
import com.powsybl.sld.SingleLineDiagram
Expand Down Expand Up @@ -116,11 +118,18 @@ fun defaultSensitivityAnalysisParameters(): String {
return mapper.writeValueAsString(SensitivityAnalysisParameters())
}

fun defaultSecurityAnalysisParameters(): String {
val mapper = ObjectMapper()
mapper.registerModule(SecurityAnalysisJsonModule())
return mapper.writeValueAsString(SecurityAnalysisParameters())
}

class UnknownRouteException(message: String) : Exception(message)

fun defaultParameterSet(name: String): String {
val loadParams = "load-params"
val sensitivityAnalysisParams = "sensitivity-analysis-params"
val securityAnalysisParams = "security-analysis-params"
return when (name) {
loadParams -> {
defaultLoadFlowParameters()
Expand All @@ -130,6 +139,10 @@ fun defaultParameterSet(name: String): String {
defaultSensitivityAnalysisParameters()
}

securityAnalysisParams -> {
defaultSecurityAnalysisParameters()
}

else -> {
val allowed = listOf(loadParams, sensitivityAnalysisParams)
throw UnknownRouteException("Unknown parameters set $name. Must be one of $allowed")
Expand All @@ -147,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)
}
}
30 changes: 30 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 @@ -354,6 +357,33 @@ class ApplicationTest {
}
}

@Test
fun `test default security analysis parameters`() {
testApplication {
val response = client.get("default-values/security-analysis-params")
assertEquals(HttpStatusCode.OK, response.status)
val body = response.bodyAsText()
assertTrue(body.startsWith("{"))
assertTrue(body.endsWith("}"))
}

}

@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
Loading
Loading