diff --git a/CHANGELOG.md b/CHANGELOG.md index f12a546143..c548c854ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Updated docker documentation [#488](https://github.com/ie3-institute/simona/issues/488) - Added support classes for transformer tap position calculation [#1543](https://github.com/ie3-institute/simona/issues/1543) - Added basic external em service [#1566](https://github.com/ie3-institute/simona/issues/1566) +- Adding transformer tap changing to congestion mitigation [#1378](https://github.com/ie3-institute/simona/issues/1378) ### Changed - Upgraded `scala2` to `scala3` [#53](https://github.com/ie3-institute/simona/issues/53) diff --git a/src/main/scala/edu/ie3/simona/agent/grid/DBFSAlgorithm.scala b/src/main/scala/edu/ie3/simona/agent/grid/DBFSAlgorithm.scala index fe2c46ec95..2e155452df 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/DBFSAlgorithm.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/DBFSAlgorithm.scala @@ -466,6 +466,10 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { ) buffer.stash(requestGridPower) Behaviors.same + + case (msg, _) => + ctx.log.warn("Msg '{}' received", msg) + Behaviors.same } } diff --git a/src/main/scala/edu/ie3/simona/agent/grid/GridAgent.scala b/src/main/scala/edu/ie3/simona/agent/grid/GridAgent.scala index bd6e9b462e..951c269935 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/GridAgent.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/GridAgent.scala @@ -187,6 +187,7 @@ object GridAgent extends DBFSAlgorithm with DCMAlgorithm { pfParams, CongestionManagementParams( cfg.congestionManagement.enableDetection, + cfg.congestionManagement.enableTransformerTapChange, cfg.congestionManagement.timeout, ), SimonaActorNaming.actorName(ctx.self), diff --git a/src/main/scala/edu/ie3/simona/agent/grid/congestion/CongestionManagementMessages.scala b/src/main/scala/edu/ie3/simona/agent/grid/congestion/CongestionManagementMessages.scala index 29612012e2..fc8bc0b5ec 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/congestion/CongestionManagementMessages.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/congestion/CongestionManagementMessages.scala @@ -7,11 +7,15 @@ package edu.ie3.simona.agent.grid.congestion import edu.ie3.simona.agent.grid.GridAgent +import edu.ie3.simona.agent.grid.congestion.mitigations.MitigationSteps /** Messages for the congestion management. */ object CongestionManagementMessages { + final case class NextStep(step: MitigationSteps.Value) + extends GridAgent.InternalRequest + /** Message that informs all actors that the current step is started. */ case object StartStep extends GridAgent.InternalRequest diff --git a/src/main/scala/edu/ie3/simona/agent/grid/congestion/CongestionManagementParams.scala b/src/main/scala/edu/ie3/simona/agent/grid/congestion/CongestionManagementParams.scala index e9407caec2..139fcfd57b 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/congestion/CongestionManagementParams.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/congestion/CongestionManagementParams.scala @@ -6,19 +6,61 @@ package edu.ie3.simona.agent.grid.congestion +import edu.ie3.simona.agent.grid.congestion.CongestionManagementParams.MitigationProgress +import edu.ie3.simona.agent.grid.congestion.mitigations.MitigationSteps +import edu.ie3.simona.agent.grid.congestion.mitigations.MitigationSteps.* + import scala.concurrent.duration.FiniteDuration /** Holds all congestion management configuration parameters used in * [[edu.ie3.simona.agent.grid]]. If the parameter [[detectionEnabled]] is set * to false, no congestion management is run and all the other parameters are - * ignored + * ignored. * * @param detectionEnabled - * defines if the congestion management is active and can be run + * Defines if the congestion management is active and can be run. + * @param enableTransformerTapChange + * Defines if changing the transformer tapping can be used for congestion + * management. * @param timeout - * used for asking other actors + * Used for asking other actors. */ final case class CongestionManagementParams( detectionEnabled: Boolean, + enableTransformerTapChange: Boolean, timeout: FiniteDuration, -) + progress: MitigationProgress = MitigationProgress(), +) { + + def anyMitigationEnabled: Boolean = enableTransformerTapChange + +} + +object CongestionManagementParams { + + case class MitigationProgress( + hasUsedTransformerTapChange: Boolean = false, + currentIteration: Int = 0, + maxNrOfOptimizationIterations: Int = 1, + ) { + + def getNextStepsAndUpdate: (MitigationSteps.Value, MitigationProgress) = { + + if hasUsedTransformerTapChange then { + // we reset the progress and return no next step + (NoMeasure, reset) + } else { + ( + TransformerTapChange, + copy(hasUsedTransformerTapChange = true), + ) + } + } + + def reset: MitigationProgress = copy( + hasUsedTransformerTapChange = false, + currentIteration = 0, + ) + + } +} diff --git a/src/main/scala/edu/ie3/simona/agent/grid/congestion/DCMAlgorithm.scala b/src/main/scala/edu/ie3/simona/agent/grid/congestion/DCMAlgorithm.scala index f57ed196db..10a69945a0 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/congestion/DCMAlgorithm.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/congestion/DCMAlgorithm.scala @@ -18,7 +18,10 @@ import edu.ie3.simona.agent.grid.congestion.data.{ CongestionManagementData, } import edu.ie3.simona.agent.grid.congestion.detection.CongestionDetection +import edu.ie3.simona.agent.grid.congestion.mitigations.TransformerTapChange +import edu.ie3.simona.agent.participant.ParticipantAgent.GridSimulationFinished import edu.ie3.simona.event.ResultEvent.PowerFlowResultEvent +import edu.ie3.simona.ontology.messages.Activation import org.apache.pekko.actor.typed.Behavior import org.apache.pekko.actor.typed.scaladsl.{ActorContext, StashBuffer} @@ -26,7 +29,7 @@ import org.apache.pekko.actor.typed.scaladsl.{ActorContext, StashBuffer} * congestion management (DCM) algorithm execution. It is considered to be the * standard behaviour of a [[GridAgent]]. */ -trait DCMAlgorithm extends CongestionDetection { +trait DCMAlgorithm extends CongestionDetection with TransformerTapChange { /** Method for starting the congestion management. * @@ -69,6 +72,39 @@ trait DCMAlgorithm extends CongestionDetection { ) } + private[grid] def doPowerFlow( + stateData: CongestionManagementData, + ctx: ActorContext[Message], + )(using + constantData: GridAgentConstantData, + buffer: StashBuffer[Message], + ): Behavior[Message] = { + val gridAgentBaseData = stateData.gridAgentBaseData + val tick = stateData.currentTick + + // inform every system participant about a new simulation for the same tick + gridAgentBaseData.gridEnv.nodeToAssetAgents.values.foreach { actors => + actors.foreach { actor => + actor ! GridSimulationFinished(tick, tick) + } + } + + // / clean copy of the gridAgentBaseData + val cleanedGridAgentBaseData = GridAgentBaseData.clean( + gridAgentBaseData, + gridAgentBaseData.superiorGridNodeUuids, + gridAgentBaseData.inferiorGridGates, + ) + + ctx.self ! Activation(tick) + + // do another power flow calculation + GridAgent.simulateGrid( + cleanedGridAgentBaseData, + tick, + ) + } + /** Method for finishing the congestion management. This method will return to * the [[GridAgent.idle()]] state afterward. * @@ -90,12 +126,18 @@ trait DCMAlgorithm extends CongestionDetection { constantData: GridAgentConstantData, buffer: StashBuffer[Message], ): Behavior[Message] = { + val timestamp = constantData.simStartTime.plusSeconds(stateData.currentTick) + + ctx.log.info( + s"There were some congestions that could not be resolved for timestamp: $timestamp." + ) + // clean up agent and go back to idle val powerFlowResults = stateData.getAllResults(constantData.simStartTime) // return to idle GridAgent.gotoIdle( - stateData.gridAgentBaseData, + stateData.resetProgress.gridAgentBaseData, stateData.currentTick + constantData.resolution, Some(powerFlowResults), ctx, diff --git a/src/main/scala/edu/ie3/simona/agent/grid/congestion/data/AwaitingData.scala b/src/main/scala/edu/ie3/simona/agent/grid/congestion/data/AwaitingData.scala index 7bba072d35..6ca35ed933 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/congestion/data/AwaitingData.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/congestion/data/AwaitingData.scala @@ -54,10 +54,10 @@ final case class AwaitingData[T]( * @param receivedData * Data that was received. * @return - * A updated copy of this data. + * An updated copy of this data. */ def handleReceivingData( - receivedData: Vector[(ActorRef[GridAgent.Message], T)] + receivedData: Seq[(ActorRef[GridAgent.Message], T)] ): AwaitingData[T] = { val mappedData = receivedData.map { case (ref, value) => ref -> Some(value) diff --git a/src/main/scala/edu/ie3/simona/agent/grid/congestion/data/CongestionManagementData.scala b/src/main/scala/edu/ie3/simona/agent/grid/congestion/data/CongestionManagementData.scala index 8811557bbc..079f930a7a 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/congestion/data/CongestionManagementData.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/congestion/data/CongestionManagementData.scala @@ -13,7 +13,13 @@ import edu.ie3.simona.agent.grid.GridAgentData.{ GridAgentBaseData, GridAgentDataInternal, } -import edu.ie3.simona.agent.grid.congestion.{CongestedComponents, Congestions} +import edu.ie3.simona.agent.grid.congestion.CongestionManagementParams.MitigationProgress +import edu.ie3.simona.agent.grid.congestion.mitigations.MitigationSteps +import edu.ie3.simona.agent.grid.congestion.{ + CongestedComponents, + CongestionManagementParams, + Congestions, +} import edu.ie3.simona.event.ResultEvent.PowerFlowResultEvent import edu.ie3.util.quantities.QuantityUtils.asPercent import org.apache.pekko.actor.typed.ActorRef @@ -36,6 +42,8 @@ import scala.concurrent.duration.FiniteDuration * Result of the previous power flow calculation. * @param congestions * The found congestions. + * @param congestedComponents + * The components that have congestion. */ final case class CongestionManagementData( gridAgentBaseData: GridAgentBaseData, @@ -46,6 +54,31 @@ final case class CongestionManagementData( congestedComponents: CongestedComponents, ) extends GridAgentDataInternal { + def getNextMitigationMeasure + : (MitigationSteps.Value, CongestionManagementData) = { + val params = gridAgentBaseData.congestionManagementParams + + val (nextStep, updatedProgress) = params.progress.getNextStepsAndUpdate + + ( + nextStep, + copy(gridAgentBaseData = + gridAgentBaseData.copy(congestionManagementParams = + params.copy(progress = updatedProgress) + ) + ), + ) + } + + def resetProgress: CongestionManagementData = { + val params = gridAgentBaseData.congestionManagementParams + val updatedParams = params.copy(progress = params.progress.reset) + + copy(gridAgentBaseData = + gridAgentBaseData.copy(congestionManagementParams = updatedParams) + ) + } + /** Builds a [[CongestionResult]] from the power flow results. * @param startTime * Of the simulation. @@ -127,6 +160,9 @@ final case class CongestionManagementData( def superiorGridRefs: Map[ActorRef[GridAgent.Message], Seq[UUID]] = gridAgentBaseData.superiorGridRefs + def congestionManagementParams: CongestionManagementParams = + gridAgentBaseData.congestionManagementParams + def timeout: FiniteDuration = gridAgentBaseData.congestionManagementParams.timeout } diff --git a/src/main/scala/edu/ie3/simona/agent/grid/congestion/detection/CongestionDetection.scala b/src/main/scala/edu/ie3/simona/agent/grid/congestion/detection/CongestionDetection.scala index 34468b75ea..8353bc377f 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/congestion/detection/CongestionDetection.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/congestion/detection/CongestionDetection.scala @@ -19,7 +19,15 @@ import edu.ie3.simona.agent.grid.congestion.data.{ AwaitingData, CongestionManagementData, } +import edu.ie3.simona.agent.grid.congestion.CongestionManagementMessages.* +import edu.ie3.simona.agent.grid.congestion.detection.DetectionMessages.* +import edu.ie3.simona.agent.grid.congestion.data.{ + AwaitingData, + CongestionManagementData, +} import edu.ie3.simona.agent.grid.congestion.Congestions +import edu.ie3.simona.agent.grid.congestion.mitigations.MitigationSteps +import edu.ie3.simona.agent.grid.congestion.mitigations.MitigationSteps.* import org.apache.pekko.actor.typed.{ActorRef, Behavior} import org.apache.pekko.actor.typed.scaladsl.{ ActorContext, @@ -43,7 +51,7 @@ trait CongestionDetection { private[grid] def checkForCongestion( stateData: CongestionManagementData, awaitingData: AwaitingData[Congestions], - )(implicit + )(using constantData: GridAgentConstantData, buffer: StashBuffer[GridAgent.Message], ): Behavior[GridAgent.Message] = Behaviors.receivePartial { @@ -74,6 +82,25 @@ trait CongestionDetection { ctx, ) + case (ctx, nextStep @ NextStep(step)) => + stateData.inferiorGridRefs.keys.foreach(_ ! nextStep) + + step match { + case TransformerTapChange => + // use transformer tap change + ctx.self ! StartStep + + GridAgent.updateTransformerTapping( + stateData, + AwaitingData(stateData.inferiorGridRefs.keySet), + ) + + case _ => + // no mitigation measure found + ctx.self ! FinishStep + Behaviors.same + } + case (ctx, FinishStep) => // inform my inferior grids about the end of the congestion management stateData.inferiorGridRefs.keys.foreach( @@ -128,9 +155,9 @@ trait CongestionDetection { private def processReceivedData( stateData: CongestionManagementData, awaitingData: AwaitingData[Congestions], - congestions: Vector[(ActorRef[GridAgent.Message], Congestions)], + congestions: Seq[(ActorRef[GridAgent.Message], Congestions)], ctx: ActorContext[GridAgent.Message], - )(implicit + )(using constantData: GridAgentConstantData, buffer: StashBuffer[GridAgent.Message], ): Behavior[GridAgent.Message] = { @@ -155,15 +182,16 @@ trait CongestionDetection { s"Congestion overall: $updatedCongestions" ) - val timestamp = - constantData.simStartTime.plusSeconds(stateData.currentTick) + // first we find an option for the next mitigation step + val (step, updatedProgress) = stateData.getNextMitigationMeasure - ctx.log.info( - s"There were some congestions that could not be resolved for timestamp: $timestamp." + ctx.self ! NextStep(step) + + val updatedStateData = updatedProgress.copy( + congestions = updatedCongestions ) - ctx.self ! FinishStep - checkForCongestion(stateData, updatedData) + checkForCongestion(updatedStateData, awaitingData) } } else { diff --git a/src/main/scala/edu/ie3/simona/agent/grid/congestion/detection/DetectionMessages.scala b/src/main/scala/edu/ie3/simona/agent/grid/congestion/detection/DetectionMessages.scala index df3f31590b..7f5327da5f 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/congestion/detection/DetectionMessages.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/congestion/detection/DetectionMessages.scala @@ -41,6 +41,6 @@ object DetectionMessages { * vector of congestion in inferior grids */ final case class ReceivedCongestions( - values: Vector[(ActorRef[GridAgent.Message], Congestions)] + values: Seq[(ActorRef[GridAgent.Message], Congestions)] ) extends InternalReply } diff --git a/src/main/scala/edu/ie3/simona/agent/grid/congestion/mitigations/MitigationSteps.scala b/src/main/scala/edu/ie3/simona/agent/grid/congestion/mitigations/MitigationSteps.scala new file mode 100644 index 0000000000..88a1c7c6db --- /dev/null +++ b/src/main/scala/edu/ie3/simona/agent/grid/congestion/mitigations/MitigationSteps.scala @@ -0,0 +1,14 @@ +/* + * © 2025. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.agent.grid.congestion.mitigations + +/** Enumeration with all congestion management steps. + */ +object MitigationSteps extends Enumeration { + val TransformerTapChange, TopologyChange, FlexibilityDeployment, NoMeasure = + Value +} diff --git a/src/main/scala/edu/ie3/simona/agent/grid/congestion/mitigations/TappingMessages.scala b/src/main/scala/edu/ie3/simona/agent/grid/congestion/mitigations/TappingMessages.scala new file mode 100644 index 0000000000..9d1a37187c --- /dev/null +++ b/src/main/scala/edu/ie3/simona/agent/grid/congestion/mitigations/TappingMessages.scala @@ -0,0 +1,65 @@ +/* + * © 2025. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.agent.grid.congestion.mitigations + +import edu.ie3.simona.agent.grid.GridAgent +import edu.ie3.simona.agent.grid.GridAgent.{ + InternalReply, + InternalReplyWithSender, + InternalRequest, +} +import edu.ie3.simona.agent.grid.congestion.VoltageRange +import edu.ie3.simona.model.grid.TransformerTapping +import org.apache.pekko.actor.typed.ActorRef +import squants.Dimensionless + +object TappingMessages { + + /** Request for voltage options in the inferior grid. + * @param sender + * That is asking. + * @param subgrid + * Subgrid of the sender. + */ + final case class RequestVoltageOptions( + sender: ActorRef[GridAgent.Message], + subgrid: Int, + ) extends InternalRequest + + /** Response with voltage options of the inferior grid. + * @param sender + * Inferior grid ref. + * @param value + * Consisting of the voltage range and a set of all transformers to the + * superior grid. + */ + final case class VoltageRangeResponse( + override val sender: ActorRef[GridAgent.Message], + override val value: (VoltageRange, Set[TransformerTapping]), + ) extends InternalReplyWithSender[(VoltageRange, Set[TransformerTapping])] + + /** Answer with all voltage options and corresponding transformers to the + * inferior grids. + * @param values + * Received data. + */ + final case class ReceivedVoltageRange( + values: Seq[ + (ActorRef[GridAgent.Message], (VoltageRange, Set[TransformerTapping])) + ] + ) extends InternalReply + + /** Message to an inferior grid with the voltage change after the transformers + * are tapped. + * @param delta + * Voltage change. + */ + final case class VoltageDeltaResponse( + delta: Dimensionless + ) extends InternalReply + +} diff --git a/src/main/scala/edu/ie3/simona/agent/grid/congestion/mitigations/TransformerTapChange.scala b/src/main/scala/edu/ie3/simona/agent/grid/congestion/mitigations/TransformerTapChange.scala new file mode 100644 index 0000000000..3030b73342 --- /dev/null +++ b/src/main/scala/edu/ie3/simona/agent/grid/congestion/mitigations/TransformerTapChange.scala @@ -0,0 +1,238 @@ +/* + * © 2025. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.agent.grid.congestion.mitigations + +import edu.ie3.simona.agent.grid.GridAgent +import edu.ie3.simona.agent.grid.GridAgent.{ + askInferior, + finishCongestionManagement, + unsupported, +} +import edu.ie3.simona.agent.grid.GridAgentData.GridAgentConstantData +import edu.ie3.simona.agent.grid.congestion.CongestionManagementMessages.* +import edu.ie3.simona.agent.grid.congestion.data.{ + AwaitingData, + CongestionManagementData, +} +import edu.ie3.simona.agent.grid.congestion.mitigations.TappingMessages.* +import edu.ie3.simona.agent.grid.congestion.{Congestions, VoltageRange} +import edu.ie3.simona.model.control.TappingGroupModel +import edu.ie3.simona.model.grid.Transformer3wPowerFlowCase.{ + PowerFlowCaseB, + PowerFlowCaseC, +} +import edu.ie3.simona.model.grid.TransformerTapping +import edu.ie3.simona.util.SimonaConstants +import edu.ie3.util.quantities.QuantityUtils.asPu +import edu.ie3.util.scala.quantities.DefaultQuantities.zeroPU +import edu.ie3.util.scala.quantities.{DefaultQuantities, SquantsUtils} +import org.apache.pekko.actor.typed.scaladsl.{ + ActorContext, + Behaviors, + StashBuffer, +} +import org.apache.pekko.actor.typed.{ActorRef, Behavior} +import squants.Dimensionless + +trait TransformerTapChange { + + /** Method that defines the [[Behavior]] for changing the tapping for + * transformers. + * + * @param stateData + * of the actor + * @param constantData + * constant data of the [[GridAgent]] + * @param buffer + * for stashed messages + * @return + * a [[Behavior]] + */ + private[grid] def updateTransformerTapping( + stateData: CongestionManagementData, + awaitingData: AwaitingData[(VoltageRange, Set[TransformerTapping])], + )(using + constantData: GridAgentConstantData, + buffer: StashBuffer[GridAgent.Message], + ): Behavior[GridAgent.Message] = Behaviors.receivePartial { + case (ctx, StartStep) => + val subnet = stateData.gridAgentBaseData.gridEnv.gridModel.subnetNo + + // request congestion check if we have inferior grids + askInferior( + stateData.inferiorGridRefs, + ref => RequestVoltageOptions(ref, subnet), + ReceivedVoltageRange.apply, + ctx, + )(using stateData.timeout) + + Behaviors.same + + case (ctx, voltageOptionRequest: RequestVoltageOptions) => + answerRequest(stateData, awaitingData, voltageOptionRequest, ctx) + + case (ctx, ReceivedVoltageRange(voltageRange)) => + processReceivedData(stateData, awaitingData, voltageRange, ctx) + + case (ctx, VoltageDeltaResponse(delta)) => + handleUpdatedDataFromSuperior(stateData, awaitingData, delta, ctx) + + case (ctx, FinishStep) => + // inform my inferior grids about the end of the congestion management + stateData.inferiorGridRefs.keys.foreach( + _ ! FinishStep + ) + + // directly finish congestion management, since we don't have any steps + GridAgent.doPowerFlow(stateData, ctx) + + case (ctx, msg) => + unsupported(msg, ctx.log) + Behaviors.same + } + + private def answerRequest( + stateData: CongestionManagementData, + awaitingData: AwaitingData[(VoltageRange, Set[TransformerTapping])], + request: RequestVoltageOptions, + ctx: ActorContext[GridAgent.Message], + )(using + constantData: GridAgentConstantData, + buffer: StashBuffer[GridAgent.Message], + ): Behavior[GridAgent.Message] = { + val sender = request.sender + val subgrid = request.subgrid + + // check if waiting for inferior data is needed + if awaitingData.notDone then { + ctx.log.debug( + s"Received request for voltage range before all data from inferior grids were received. Stashing away." + ) + + // stash away the message, because we need to wait for data from inferior grids + buffer.stash(request) + } else { + // calculate the voltage range for this grid + val gridEnv = stateData.gridAgentBaseData.gridEnv + val gridModel = gridEnv.gridModel + val gridComponents = gridModel.gridComponents + + // filter all transformers that are connecting this grid to the superior grid + val nodesInSuperiorGrid = + gridComponents.nodes.filter(_.subnet == subgrid).map(_.uuid) + val transformers = gridComponents.transformers.filter(t => + nodesInSuperiorGrid.contains(t.hvNodeUuid) + ) + val transformers3w = gridComponents.transformers3w.filter { t => + t.powerFlowCase match { + case PowerFlowCaseB => + nodesInSuperiorGrid.contains(t.mvNodeUuid) + case PowerFlowCaseC => + nodesInSuperiorGrid.contains(t.lvNodeUuid) + } + } + + val allTransformers: Set[TransformerTapping] = + transformers ++ transformers3w + + // calculate the voltage range with the received data + val range = VoltageRange( + stateData.powerFlowResults, + gridModel.voltageLimits, + gridModel.gridComponents, + awaitingData.mappedValues, + gridModel.subnetNo, + ) + + ctx.log.debug( + s"For Grid ${stateData.gridAgentBaseData.gridEnv.gridModel.subnetNo}, voltage range: $range" + ) + + sender ! VoltageRangeResponse( + ctx.self, + (range, allTransformers), + ) + } + + updateTransformerTapping(stateData, awaitingData) + } + + private def processReceivedData( + stateData: CongestionManagementData, + awaitingData: AwaitingData[(VoltageRange, Set[TransformerTapping])], + voltageRange: Seq[ + (ActorRef[GridAgent.Message], (VoltageRange, Set[TransformerTapping])) + ], + ctx: ActorContext[GridAgent.Message], + )(using + constantData: GridAgentConstantData, + buffer: StashBuffer[GridAgent.Message], + ): Behavior[GridAgent.Message] = { + // updating the state data with received data from inferior grids + val updatedData = awaitingData.handleReceivingData(voltageRange) + + if stateData.gridAgentBaseData.isSuperior then { + // there should be no voltage change in the superior grid, + // because the slack grid should always have 1 pu + + ctx.self ! VoltageDeltaResponse(zeroPU) + updateTransformerTapping(stateData, updatedData) + } else { + // un-stash all messages + buffer.unstashAll(updateTransformerTapping(stateData, updatedData)) + } + } + + private def handleUpdatedDataFromSuperior( + stateData: CongestionManagementData, + awaitingData: AwaitingData[(VoltageRange, Set[TransformerTapping])], + delta: Dimensionless, + ctx: ActorContext[GridAgent.Message], + )(using + constantData: GridAgentConstantData, + buffer: StashBuffer[GridAgent.Message], + ): Behavior[GridAgent.Message] = { + // if we are the superior grid to another grid, we check for transformer tapping option + // and send the new delta to the inferior grid + ctx.log.warn( + s"Grid ${stateData.gridAgentBaseData.gridEnv.gridModel.subnetNo}, received delta: $delta" + ) + + if stateData.inferiorGridRefs.nonEmpty then { + // we calculate a voltage delta for all inferior grids + val receivedData = awaitingData.mappedValues + + // map the actor ref to the possible voltage range + val refMap = receivedData.map { case (ref, (range, _)) => + ref -> range + } + + val actorRefToTappingModels + : Map[ActorRef[GridAgent.Message], Set[TransformerTapping]] = + receivedData.map { case (ref, (_, tappings)) => ref -> tappings } + + // groups all tapping models + // necessary, because to make sure the tapping is change by the same value between two grids, + // we need to know all transformers that are relevant as well as all actor refs to check their + // possible voltage ranges + + TappingGroupModel + .buildModels( + actorRefToTappingModels, + stateData.gridAgentBaseData.gridEnv.gridModel.gridComponents.transformers3w, + ) + .foreach { group => + val deltaV = group.updateTapPositions(delta, refMap) + group.refs.foreach(_ ! VoltageDeltaResponse(deltaV + delta)) + } + } + + ctx.self ! FinishStep + updateTransformerTapping(stateData, awaitingData) + } + +} diff --git a/src/main/scala/edu/ie3/simona/config/SimonaConfig.scala b/src/main/scala/edu/ie3/simona/config/SimonaConfig.scala index 83037f2346..5b1356f62f 100644 --- a/src/main/scala/edu/ie3/simona/config/SimonaConfig.scala +++ b/src/main/scala/edu/ie3/simona/config/SimonaConfig.scala @@ -115,8 +115,10 @@ object SimonaConfig { time: Simona.Time, ) derives ConfigConvert object Simona { + final case class CongestionManagement( enableDetection: Boolean = false, + enableTransformerTapChange: Boolean = false, timeout: FiniteDuration = 30.seconds, ) diff --git a/src/main/scala/edu/ie3/simona/model/control/TappingGroupModel.scala b/src/main/scala/edu/ie3/simona/model/control/TappingGroupModel.scala index cf159d4db8..c4dc50a099 100644 --- a/src/main/scala/edu/ie3/simona/model/control/TappingGroupModel.scala +++ b/src/main/scala/edu/ie3/simona/model/control/TappingGroupModel.scala @@ -16,16 +16,9 @@ import edu.ie3.simona.model.grid.{ TransformerModel, TransformerTapping, } -import edu.ie3.util.quantities.QuantityUtils.asPu -import edu.ie3.util.scala.quantities.DefaultQuantities import edu.ie3.util.scala.quantities.DefaultQuantities.zeroPU -import edu.ie3.util.scala.quantities.QuantityConversionUtils.{ - toQuantity, - toSquants, -} import org.apache.pekko.actor.typed.ActorRef import squants.{Dimensionless, Each} -import tech.units.indriya.ComparableQuantity /** A group of [[TransformerTapping]] with all associated [[ActorRef]]s. * @param refs diff --git a/src/test/scala/edu/ie3/simona/agent/grid/TransformerTappingSupportSpec.scala b/src/test/scala/edu/ie3/simona/agent/grid/TransformerTappingSupportSpec.scala index 7ec1aa7d3e..d3d18a2e86 100644 --- a/src/test/scala/edu/ie3/simona/agent/grid/TransformerTappingSupportSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/grid/TransformerTappingSupportSpec.scala @@ -15,6 +15,7 @@ import edu.ie3.simona.test.common.model.grid.{ } import edu.ie3.simona.test.common.result.ResultMokka import edu.ie3.simona.test.common.{ConfigTestData, UnitSpec} +import edu.ie3.util.quantities.QuantityUtils.asPu import org.apache.pekko.actor.testkit.typed.scaladsl.{ ScalaTestWithActorTestKit, TestProbe, diff --git a/src/test/scala/edu/ie3/simona/agent/grid/congestion/CongestedComponentsSpec.scala b/src/test/scala/edu/ie3/simona/agent/grid/congestion/CongestedComponentsSpec.scala index a1bff59ec3..e3df83c73e 100644 --- a/src/test/scala/edu/ie3/simona/agent/grid/congestion/CongestedComponentsSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/grid/congestion/CongestedComponentsSpec.scala @@ -39,7 +39,7 @@ class CongestedComponentsSpec val congested = CongestedComponents( results, - gridModel.gridComponents, + extendedGridModel.gridComponents, defaultVoltageLimits, Kilovolts(110), 1, @@ -62,13 +62,14 @@ class CongestedComponentsSpec val congested = CongestedComponents( results, - gridModel.gridComponents, + extendedGridModel.gridComponents, defaultVoltageLimits, Kilovolts(110), 1, ) - val lineMap = gridModel.gridComponents.lines.map(l => l.uuid -> l).toMap + val lineMap = + extendedGridModel.gridComponents.lines.map(l => l.uuid -> l).toMap congested.voltages shouldBe Iterable.empty congested.lines shouldBe Iterable( @@ -89,14 +90,16 @@ class CongestedComponentsSpec val congested = CongestedComponents( results, - gridModel.gridComponents, + extendedGridModel.gridComponents, defaultVoltageLimits, Kilovolts(110), 1, ) val transformerMap = - gridModel.gridComponents.transformers.map(t => t.uuid -> t).toMap + extendedGridModel.gridComponents.transformers + .map(t => t.uuid -> t) + .toMap congested.voltages shouldBe Iterable(nodeResult2) congested.lines shouldBe Iterable.empty @@ -120,14 +123,16 @@ class CongestedComponentsSpec val congested = CongestedComponents( results, - gridModel.gridComponents, + extendedGridModel.gridComponents, defaultVoltageLimits, Kilovolts(110), 1, ) val transformerMap = - gridModel.gridComponents.transformers3w.map(t => t.uuid -> t).toMap + extendedGridModel.gridComponents.transformers3w + .map(t => t.uuid -> t) + .toMap congested.voltages shouldBe Iterable(nodeResult2) congested.lines shouldBe Iterable.empty diff --git a/src/test/scala/edu/ie3/simona/agent/grid/congestion/CongestionSpec.scala b/src/test/scala/edu/ie3/simona/agent/grid/congestion/CongestionSpec.scala new file mode 100644 index 0000000000..8c1ee0ef9e --- /dev/null +++ b/src/test/scala/edu/ie3/simona/agent/grid/congestion/CongestionSpec.scala @@ -0,0 +1,252 @@ +/* + * © 2025. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.agent.grid.congestion + +import edu.ie3.datamodel.models.StandardUnits +import edu.ie3.datamodel.models.result.NodeResult +import edu.ie3.datamodel.models.result.connector.{ + LineResult, + Transformer2WResult, +} +import edu.ie3.simona.agent.grid.congestion.Congestions +import edu.ie3.simona.event.ResultEvent.PowerFlowResultEvent +import edu.ie3.simona.model.grid.{GridModel, RefSystem, VoltageLimits} +import edu.ie3.simona.test.common.model.grid.{ + DbfsTestGrid, + GridComponentsMokka, + SubGridGateMokka, +} +import edu.ie3.simona.test.common.result.ResultMokka +import edu.ie3.simona.test.common.{ConfigTestData, UnitSpec} +import edu.ie3.util.quantities.PowerSystemUnits.PU +import edu.ie3.util.quantities.QuantityUtils.{asDegreeGeom, asPu} +import org.apache.pekko.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit +import squants.electro.Kilovolts +import squants.energy.Kilowatts +import tech.units.indriya.quantity.Quantities + +import java.time.ZonedDateTime + +class CongestionSpec + extends ScalaTestWithActorTestKit + with UnitSpec + with GridComponentsMokka + with ResultMokka + with SubGridGateMokka + with DbfsTestGrid + with ConfigTestData { + + "A Congestion" should { + val startTime = ZonedDateTime.now() + + val gridModel = GridModel( + hvGridContainer, + RefSystem(Kilowatts(600), Kilovolts(110)), + VoltageLimits(0.9, 1.1), + startTime, + startTime.plusHours(2), + simonaConfig, + ) + + "find congestions correctly for empty results" in { + val emptyResults = PowerFlowResultEvent( + Seq.empty, + Seq.empty, + Seq.empty, + Seq.empty, + Seq.empty, + ) + + val congestedComponents = CongestedComponents( + emptyResults, + gridModel.gridComponents, + gridModel.voltageLimits, + gridModel.mainRefSystem.nominalVoltage, + gridModel.subnetNo, + ) + + Congestions(congestedComponents) shouldBe Congestions( + voltageCongestions = false, + lineCongestions = false, + transformerCongestions = false, + ) + } + + "find voltage congestions correctly" in { + val nodeResult1 = new NodeResult( + startTime, + node1.getUuid, + Quantities.getQuantity(1d, PU), + Quantities.getQuantity(0, StandardUnits.VOLTAGE_ANGLE), + ) + + val nodeResult2 = new NodeResult( + startTime, + node2.getUuid, + Quantities.getQuantity(0.9d, PU), + Quantities.getQuantity(0, StandardUnits.VOLTAGE_ANGLE), + ) + + val nodeResult3 = new NodeResult( + startTime, + node3.getUuid, + Quantities.getQuantity(1.1d, PU), + Quantities.getQuantity(0, StandardUnits.VOLTAGE_ANGLE), + ) + + val nodeResult4 = new NodeResult( + startTime, + node4.getUuid, + Quantities.getQuantity(0.89d, PU), + Quantities.getQuantity(0, StandardUnits.VOLTAGE_ANGLE), + ) + + val results = PowerFlowResultEvent( + Seq(nodeResult1, nodeResult2, nodeResult3, nodeResult4), + Seq.empty, + Seq.empty, + Seq.empty, + Seq.empty, + ) + + val congestedComponents = CongestedComponents( + results, + gridModel.gridComponents, + gridModel.voltageLimits, + gridModel.mainRefSystem.nominalVoltage, + gridModel.subnetNo, + ) + + Congestions(congestedComponents) shouldBe Congestions( + voltageCongestions = true, + lineCongestions = false, + transformerCongestions = false, + ) + } + + "find line congestions correctly" in { + val lineResult1to2 = new LineResult( + startTime, + line1To2.getUuid, + Quantities.getQuantity(1360d, StandardUnits.ELECTRIC_CURRENT_MAGNITUDE), + Quantities.getQuantity(0, StandardUnits.VOLTAGE_ANGLE), + Quantities.getQuantity(1360d, StandardUnits.ELECTRIC_CURRENT_MAGNITUDE), + Quantities.getQuantity(0, StandardUnits.VOLTAGE_ANGLE), + ) + + val lineResult1to3 = new LineResult( + startTime, + line1To3.getUuid, + Quantities.getQuantity(500d, StandardUnits.ELECTRIC_CURRENT_MAGNITUDE), + Quantities.getQuantity(0, StandardUnits.VOLTAGE_ANGLE), + Quantities.getQuantity(500d, StandardUnits.ELECTRIC_CURRENT_MAGNITUDE), + Quantities.getQuantity(0, StandardUnits.VOLTAGE_ANGLE), + ) + + val lineResult1to4 = new LineResult( + startTime, + line1To4.getUuid, + Quantities.getQuantity(801d, StandardUnits.ELECTRIC_CURRENT_MAGNITUDE), + Quantities.getQuantity(0, StandardUnits.VOLTAGE_ANGLE), + Quantities.getQuantity(799d, StandardUnits.ELECTRIC_CURRENT_MAGNITUDE), + Quantities.getQuantity(0, StandardUnits.VOLTAGE_ANGLE), + ) + + val lineResult2to3 = new LineResult( + startTime, + line2To3.getUuid, + Quantities.getQuantity(801d, StandardUnits.ELECTRIC_CURRENT_MAGNITUDE), + Quantities.getQuantity(0, StandardUnits.VOLTAGE_ANGLE), + Quantities.getQuantity(799d, StandardUnits.ELECTRIC_CURRENT_MAGNITUDE), + Quantities.getQuantity(0, StandardUnits.VOLTAGE_ANGLE), + ) + + val results = PowerFlowResultEvent( + Seq.empty, + Seq.empty, + Seq(lineResult1to2, lineResult1to3, lineResult1to4, lineResult2to3), + Seq.empty, + Seq.empty, + ) + + val congestedComponents = CongestedComponents( + results, + gridModel.gridComponents, + gridModel.voltageLimits, + gridModel.mainRefSystem.nominalVoltage, + gridModel.subnetNo, + ) + + Congestions(congestedComponents) shouldBe Congestions( + voltageCongestions = false, + lineCongestions = true, + transformerCongestions = false, + ) + } + + "find transformer2w congestions correctly" in { + + val nodeResult1 = new NodeResult( + startTime, + node1.getUuid, + 0.9.asPu, + 0.asDegreeGeom, + ) + + val nodeResult2 = new NodeResult( + startTime, + node2.getUuid, + 1.0.asPu, + 0.asDegreeGeom, + ) + + val transformerResult1 = new Transformer2WResult( + startTime, + transformer1.getUuid, + Quantities.getQuantity(308d, StandardUnits.ELECTRIC_CURRENT_MAGNITUDE), + Quantities.getQuantity(0, StandardUnits.VOLTAGE_ANGLE), + Quantities + .getQuantity(1064, StandardUnits.ELECTRIC_CURRENT_MAGNITUDE), + Quantities.getQuantity(0, StandardUnits.VOLTAGE_ANGLE), + 0, + ) + + val transformerResult2 = new Transformer2WResult( + startTime, + transformer2.getUuid, + Quantities.getQuantity(310d, StandardUnits.ELECTRIC_CURRENT_MAGNITUDE), + Quantities.getQuantity(0, StandardUnits.VOLTAGE_ANGLE), + Quantities.getQuantity(1071d, StandardUnits.ELECTRIC_CURRENT_MAGNITUDE), + Quantities.getQuantity(0, StandardUnits.VOLTAGE_ANGLE), + 0, + ) + + val results = PowerFlowResultEvent( + Seq(nodeResult1, nodeResult2), + Seq.empty, + Seq.empty, + Seq(transformerResult1, transformerResult2), + Seq.empty, + ) + + val congestedComponents = CongestedComponents( + results, + gridModel.gridComponents, + gridModel.voltageLimits, + gridModel.mainRefSystem.nominalVoltage, + gridModel.subnetNo, + ) + + Congestions(congestedComponents) shouldBe Congestions( + voltageCongestions = false, + lineCongestions = false, + transformerCongestions = true, + ) + } + } + +} diff --git a/src/test/scala/edu/ie3/simona/agent/grid/congestion/CongestionTestBaseData.scala b/src/test/scala/edu/ie3/simona/agent/grid/congestion/CongestionTestBaseData.scala index d6e856bce5..6e652118d5 100644 --- a/src/test/scala/edu/ie3/simona/agent/grid/congestion/CongestionTestBaseData.scala +++ b/src/test/scala/edu/ie3/simona/agent/grid/congestion/CongestionTestBaseData.scala @@ -15,6 +15,9 @@ import edu.ie3.simona.agent.grid.GridAgentData.{ import edu.ie3.simona.agent.grid.{GridAgent, GridEnvironment} import edu.ie3.simona.config.SimonaConfig import edu.ie3.simona.event.{ResultEvent, RuntimeEvent} +import edu.ie3.simona.exceptions.CriticalFailureException +import edu.ie3.simona.model.grid.{GridModel, RefSystem, VoltageLimits} +import edu.ie3.simona.ontology.messages.{Activation, SchedulerMessage} import edu.ie3.simona.model.grid.RefSystem import edu.ie3.simona.ontology.messages.SchedulerMessage import edu.ie3.simona.service.load.LoadProfileService @@ -22,6 +25,7 @@ import edu.ie3.simona.service.primary.PrimaryServiceProxy import edu.ie3.simona.service.weather.WeatherService import edu.ie3.simona.test.common.result.CongestedComponentsTestData import edu.ie3.simona.test.common.{ConfigTestData, TestSpawnerTyped} +import edu.ie3.util.quantities.QuantityUtils.asPu import org.apache.pekko.actor.testkit.typed.scaladsl.{ ActorTestKitBase, TestProbe, @@ -53,10 +57,8 @@ trait CongestionTestBaseData .resolve() ) - protected val refSystem: RefSystem = RefSystem( - Megawatts(600d), - Kilovolts(110d), - ) + protected val refSystem: RefSystem = + RefSystem(Megawatts(600), Kilovolts(110d)) protected val scheduler: TestProbe[SchedulerMessage] = TestProbe("scheduler") protected val runtimeEvents: TestProbe[RuntimeEvent] = TestProbe( @@ -88,7 +90,20 @@ trait CongestionTestBaseData "resultListener" ) - protected implicit val constantData: GridAgentConstantData = + protected val gridAgentActivation: TestProbe[Activation] = TestProbe( + "gridAgentActivation" + ) + + protected val gridModel: GridModel = GridModel( + hvGridContainer, + refSystem, + voltageLimits, + startTime, + endTime, + config, + ) + + protected given constantData: GridAgentConstantData = GridAgentConstantData( environmentRefs, simonaConfig, @@ -111,25 +126,17 @@ trait CongestionTestBaseData } } - def spawnWithBuffer( - factory: StashBuffer[GridAgent.Message] => Behavior[GridAgent.Message], - capacity: Int = 10, - ): ActorRef[GridAgent.Message] = - testKit.spawn( - Behaviors.withStash(capacity) { buffer => - factory(buffer) - } - ) - def gridAgentBaseData( inferiorRefs: Set[ActorRef[GridAgent.Message]] = Set.empty, isSuperior: Boolean = false, + gridModel: Option[GridModel] = None, ): GridAgentBaseData = { val data = mock[GridAgentBaseData] val map = inferiorRefs.map(ref => ref -> Seq.empty).toMap val cmParams = CongestionManagementParams( detectionEnabled = true, + enableTransformerTapChange = false, 30.seconds, ) @@ -141,10 +148,21 @@ trait CongestionTestBaseData val gridEnv = mock[GridEnvironment] when(data.gridEnv).thenReturn(gridEnv) - when(gridEnv.gridModel).thenReturn(gridModel) when(gridEnv.subgridGateToActorRef).thenReturn(Map.empty) when(gridEnv.nodeToAssetAgents).thenReturn(Map.empty) + gridModel match { + case Some(model) => + when(gridEnv.gridModel).thenReturn(model) + + case None => + val gridModelMock = mock[GridModel] + when(gridEnv.gridModel).thenReturn(gridModelMock) + + when(gridModelMock.voltageLimits).thenReturn(voltageLimits) + when(gridModelMock.mainRefSystem).thenReturn(refSystem) + } + data } diff --git a/src/test/scala/edu/ie3/simona/agent/grid/congestion/DCMAlgorithmSpec.scala b/src/test/scala/edu/ie3/simona/agent/grid/congestion/DCMAlgorithmSpec.scala index fde0c5b5ca..1c01a8d9ab 100644 --- a/src/test/scala/edu/ie3/simona/agent/grid/congestion/DCMAlgorithmSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/grid/congestion/DCMAlgorithmSpec.scala @@ -9,6 +9,7 @@ package edu.ie3.simona.agent.grid.congestion import edu.ie3.simona.agent.grid.GridAgent import edu.ie3.simona.agent.grid.congestion.data.CongestionManagementData import edu.ie3.simona.agent.grid.congestion.detection.DetectionMessages.CongestionCheckRequest +import edu.ie3.simona.agent.grid.congestion.CongestionManagementParams.MitigationProgress import edu.ie3.simona.event.ResultEvent.PowerFlowResultEvent import edu.ie3.simona.ontology.messages.SchedulerMessage.Completion import edu.ie3.simona.test.common.UnitSpec @@ -30,7 +31,10 @@ class DCMAlgorithmSpec "start the congestion management correctly" in { val inferiorGA = TestProbe[GridAgent.Message]("inferiorGridAgent") - val baseData = gridAgentBaseData(inferiorRefs = Set(inferiorGA.ref)) + val baseData = gridAgentBaseData( + inferiorRefs = Set(inferiorGA.ref), + gridModel = Some(gridModel), + ) // behavior, that will start the congestion management by creating the state data and checking the result for congestions // we need to spawn the behavior here, since we send a start message internally diff --git a/src/test/scala/edu/ie3/simona/agent/grid/congestion/data/CongestionManagementDataSpec.scala b/src/test/scala/edu/ie3/simona/agent/grid/congestion/data/CongestionManagementDataSpec.scala index 62a9b14c60..34bd1a3875 100644 --- a/src/test/scala/edu/ie3/simona/agent/grid/congestion/data/CongestionManagementDataSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/grid/congestion/data/CongestionManagementDataSpec.scala @@ -26,7 +26,7 @@ class CongestionManagementDataSpec val data = mock[GridAgentBaseData] val gridEnv = mock[GridEnvironment] - when(gridEnv.gridModel).thenReturn(gridModel) + when(gridEnv.gridModel).thenReturn(extendedGridModel) when(data.gridEnv).thenReturn(gridEnv) @@ -35,9 +35,10 @@ class CongestionManagementDataSpec "The CongestionManagementData" should { - val lineMap = gridModel.gridComponents.lines.map(l => l.uuid -> l).toMap + val lineMap = + extendedGridModel.gridComponents.lines.map(l => l.uuid -> l).toMap val transformerMap = - gridModel.gridComponents.transformers.map(t => t.uuid -> t).toMap + extendedGridModel.gridComponents.transformers.map(t => t.uuid -> t).toMap "return the congestion result correctly" in { val stateData = CongestionManagementData( diff --git a/src/test/scala/edu/ie3/simona/agent/grid/congestion/detection/CongestionDetectionSpec.scala b/src/test/scala/edu/ie3/simona/agent/grid/congestion/detection/CongestionDetectionSpec.scala index 86ba7bf33d..f8908dc302 100644 --- a/src/test/scala/edu/ie3/simona/agent/grid/congestion/detection/CongestionDetectionSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/grid/congestion/detection/CongestionDetectionSpec.scala @@ -20,6 +20,7 @@ import edu.ie3.simona.agent.grid.congestion.detection.DetectionMessages.{ CongestionResponse, ReceivedCongestions, } +import edu.ie3.simona.agent.grid.congestion.CongestionManagementParams.MitigationProgress import edu.ie3.simona.agent.grid.congestion.{ CongestedComponents, CongestionTestBaseData, @@ -31,7 +32,8 @@ import org.apache.pekko.actor.testkit.typed.scaladsl.{ ScalaTestWithActorTestKit, TestProbe, } -import org.apache.pekko.actor.typed.ActorRef +import org.apache.pekko.actor.typed.scaladsl.Behaviors +import org.apache.pekko.actor.typed.{ActorRef, Behavior} import scala.concurrent.duration.DurationInt import scala.language.implicitConversions @@ -41,10 +43,26 @@ class CongestionDetectionSpec with UnitSpec with CongestionTestBaseData { - val superiorAgent: TestProbe[GridAgent.Message] = TestProbe("superiorAgent") - val inferiorAgent: TestProbe[GridAgent.Message] = TestProbe("inferiorAgent") + protected val superiorAgent: TestProbe[GridAgent.Message] = TestProbe( + "superiorAgent" + ) + protected val inferiorAgent: TestProbe[GridAgent.Message] = TestProbe( + "inferiorAgent" + ) "The congestion detection" should { + def spawnBehavior( + stateData: CongestionManagementData, + awaitingData: AwaitingData[Congestions], + capacity: Int = 10, + ): ActorRef[GridAgent.Message] = testKit.spawn( + Behaviors.withStash[GridAgent.Message](capacity) { buffer => + GridAgent.checkForCongestion( + stateData, + awaitingData, + )(using constantData, buffer) + } + ) "answer a request for congestions correctly" in { val stateData = CongestionManagementData( @@ -113,12 +131,7 @@ class CongestionDetectionSpec forAll(cases) { (inferiorData, expectedCongestions) => val awaitingData = AwaitingData(inferiorData) - val behavior = spawnWithBuffer( - GridAgent.checkForCongestion( - stateData, - awaitingData, - )(using constantData, _) - ) + val behavior = spawnBehavior(stateData, awaitingData) behavior ! CongestionCheckRequest(superiorAgent.ref) @@ -128,7 +141,7 @@ class CongestionDetectionSpec } } - "wait to answer a request for congestions if inferior data is still missing" in { + "wait to answer a request for congestions, if inferior data is still missing" in { val stateData = CongestionManagementData( gridAgentBaseData(), 3600, @@ -151,12 +164,7 @@ class CongestionDetectionSpec val awaitingData: AwaitingData[Congestions] = AwaitingData(Set(inferiorAgent.ref)) - val behavior = spawnWithBuffer( - GridAgent.checkForCongestion( - stateData, - awaitingData, - )(using constantData, _) - ) + val behavior = spawnBehavior(stateData, awaitingData) behavior ! CongestionCheckRequest(superiorAgent.ref) @@ -207,12 +215,7 @@ class CongestionDetectionSpec AwaitingData(Set(inferiorAgent.ref)) // init behavior - val centerGridAgent = spawnWithBuffer( - GridAgent.checkForCongestion( - stateData, - awaitingData, - )(using constantData, _) - ) + val centerGridAgent = spawnBehavior(stateData, awaitingData) // we will send the center grid agent a StartStep message to start the detection centerGridAgent ! StartStep @@ -275,12 +278,7 @@ class CongestionDetectionSpec AwaitingData(Set.empty[ActorRef[GridAgent.Message]]) // init behavior - val superiorGridAgent = spawnWithBuffer( - GridAgent.checkForCongestion( - stateData, - awaitingData, - )(using constantData, _) - ) + val superiorGridAgent = spawnBehavior(stateData, awaitingData) // we will send the center grid agent a StartStep message to start the detection superiorGridAgent ! StartStep diff --git a/src/test/scala/edu/ie3/simona/agent/grid/congestion/mitigation/TransformerTagChangeSpec.scala b/src/test/scala/edu/ie3/simona/agent/grid/congestion/mitigation/TransformerTagChangeSpec.scala new file mode 100644 index 0000000000..ae8f29f038 --- /dev/null +++ b/src/test/scala/edu/ie3/simona/agent/grid/congestion/mitigation/TransformerTagChangeSpec.scala @@ -0,0 +1,186 @@ +/* + * © 2025. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.agent.grid.congestion.mitigation + +import edu.ie3.simona.agent.grid.GridAgent +import edu.ie3.simona.agent.grid.congestion.data.{ + AwaitingData, + CongestionManagementData, +} +import edu.ie3.simona.agent.grid.congestion.CongestionManagementParams.MitigationProgress +import edu.ie3.simona.agent.grid.congestion.mitigations.TappingMessages.{ + ReceivedVoltageRange, + RequestVoltageOptions, + VoltageRangeResponse, +} +import edu.ie3.simona.agent.grid.congestion.{ + CongestedComponents, + CongestionTestBaseData, + Congestions, + VoltageRange, +} +import edu.ie3.simona.event.ResultEvent.PowerFlowResultEvent +import edu.ie3.simona.model.grid.{GridModel, TransformerTapping} +import edu.ie3.simona.test.common.UnitSpec +import edu.ie3.simona.test.common.model.grid.GridComponentsMokka +import edu.ie3.simona.test.common.result.CongestedComponentsTestData +import edu.ie3.simona.test.matchers.QuantityMatchers +import edu.ie3.util.quantities.QuantityUtils.{asPercent, asPu} +import org.apache.pekko.actor.testkit.typed.scaladsl.{ + ScalaTestWithActorTestKit, + TestProbe, +} +import org.apache.pekko.actor.typed.ActorRef +import org.apache.pekko.actor.typed.scaladsl.Behaviors +import squants.{Dimensionless, Each} + +class TransformerTagChangeSpec + extends ScalaTestWithActorTestKit + with UnitSpec + with CongestionTestBaseData + with CongestedComponentsTestData + with QuantityMatchers + with GridComponentsMokka { + + protected given puTolerance: Dimensionless = Each(1e-3) + + private type AwaitedData = (VoltageRange, Set[TransformerTapping]) + + protected val superiorAgent: TestProbe[GridAgent.Message] = TestProbe( + "superiorAgent" + ) + protected val inferiorAgent: TestProbe[GridAgent.Message] = TestProbe( + "inferiorAgent" + ) + + "The congestion mitigation by transformer tap change" should { + val transformers: Set[TransformerTapping] = + gridModel.gridComponents.transformers.map { + (transformerTapping: TransformerTapping) => transformerTapping + } + + def spawnCenterAgent( + stateData: CongestionManagementData, + awaitingData: AwaitingData[(VoltageRange, Set[TransformerTapping])], + capacity: Int = 10, + ): ActorRef[GridAgent.Message] = testKit.spawn( + Behaviors.withStash[GridAgent.Message](capacity) { buffer => + GridAgent.updateTransformerTapping( + stateData, + awaitingData, + )(using constantData, buffer) + } + ) + + "answer a request for voltage options" in { + val stateData = CongestionManagementData( + gridAgentBaseData(gridModel = Some(gridModel)), + 3600, + 100, + PowerFlowResultEvent( + Iterable( + nodeResult1, + nodeResult2, + nodeResult3, + nodeResult4, + nodeResultA, + nodeResultB, + ), + Iterable.empty, + Iterable(lineResult12), + Iterable.empty, + Iterable.empty, + ), + Congestions( + voltageCongestions = true, + lineCongestions = false, + transformerCongestions = false, + ), + CongestedComponents.empty, + ) + + // the map is empty, since the inferior grid itself has no inferior grids + val awaitingData = AwaitingData( + Map.empty[ActorRef[GridAgent.Message], Option[AwaitedData]] + ) + + val centerAgent = spawnCenterAgent(stateData, awaitingData) + + centerAgent ! RequestVoltageOptions(superiorAgent.ref, 1) + + val (voltageRange, actualTransformers) = + superiorAgent.expectMessageType[VoltageRangeResponse].value + + voltageRange.deltaPlus should approximate(Each(-0.01)) + voltageRange.deltaMinus should approximate(Each(-0.01)) + voltageRange.suggestion should approximate(Each(-0.011)) + + actualTransformers shouldBe transformers + } + + "wait to answer a request for voltage options, if inferior data is still missing" in { + val stateData = CongestionManagementData( + gridAgentBaseData(gridModel = Some(gridModel)), + 3600, + 100, + PowerFlowResultEvent( + Iterable( + nodeResult3, + nodeResult4, + nodeResultA, + nodeResultB, + ), + Iterable.empty, + Iterable(lineResult34), + Iterable.empty, + Iterable.empty, + ), + Congestions( + voltageCongestions = true, + lineCongestions = false, + transformerCongestions = false, + ), + CongestedComponents.empty, + ) + + // the grid, that receives the request, is a center grid, which has to wait for its inferior data + val awaitingData: AwaitingData[AwaitedData] = + AwaitingData(Set(inferiorAgent.ref)) + + val centerAgent = spawnCenterAgent(stateData, awaitingData) + + centerAgent ! RequestVoltageOptions(superiorAgent.ref, 1) + + val mockedMvLvTappingModel = mockTransformerTapping() + + // the request will be stashed and answered after inferior data was received + centerAgent ! ReceivedVoltageRange( + Seq( + ( + inferiorAgent.ref, + ( + VoltageRange(Each(0.04), Each(-0.05)), + Set(mockedMvLvTappingModel), + ), + ) + ) + ) + + val (voltageRange, actualTransformers) = + superiorAgent.expectMessageType[VoltageRangeResponse].value + + voltageRange.deltaPlus should approximate(Each(0.04)) + voltageRange.deltaMinus should approximate(Each(0.01)) + voltageRange.suggestion should approximate(Each(0.026)) + + actualTransformers shouldBe transformers + + } + + } + +} diff --git a/src/test/scala/edu/ie3/simona/model/control/TappingGroupModelSpec.scala b/src/test/scala/edu/ie3/simona/model/control/TappingGroupModelSpec.scala index 7b4b1e012a..31a3b18362 100644 --- a/src/test/scala/edu/ie3/simona/model/control/TappingGroupModelSpec.scala +++ b/src/test/scala/edu/ie3/simona/model/control/TappingGroupModelSpec.scala @@ -11,6 +11,7 @@ import edu.ie3.simona.agent.grid.GridAgent import edu.ie3.simona.model.grid.TransformerTapping import edu.ie3.simona.test.common.UnitSpec import edu.ie3.simona.test.common.model.grid.GridComponentsMokka +import edu.ie3.util.quantities.QuantityUtils.{asPercent, asPu} import org.apache.pekko.actor.testkit.typed.scaladsl.{ ScalaTestWithActorTestKit, TestProbe, diff --git a/src/test/scala/edu/ie3/simona/test/common/ConfigTestData.scala b/src/test/scala/edu/ie3/simona/test/common/ConfigTestData.scala index 246548e7d2..7f38d2371b 100644 --- a/src/test/scala/edu/ie3/simona/test/common/ConfigTestData.scala +++ b/src/test/scala/edu/ie3/simona/test/common/ConfigTestData.scala @@ -8,6 +8,9 @@ package edu.ie3.simona.test.common import com.typesafe.config.{Config, ConfigFactory} import edu.ie3.simona.config.SimonaConfig +import edu.ie3.util.TimeUtil + +import java.time.ZonedDateTime /** Simple (empty) configuration data. Furthermore, it would make sense to * implement another class which reads a config and provides config based @@ -151,4 +154,11 @@ trait ConfigTestData { ) protected val simonaConfig: SimonaConfig = SimonaConfig(typesafeConfig) + protected val startTime: ZonedDateTime = + TimeUtil.withDefaults.toZonedDateTime( + simonaConfig.simona.time.startDateTime + ) + + protected val endTime: ZonedDateTime = + TimeUtil.withDefaults.toZonedDateTime(simonaConfig.simona.time.endDateTime) } diff --git a/src/test/scala/edu/ie3/simona/test/common/result/CongestedComponentsTestData.scala b/src/test/scala/edu/ie3/simona/test/common/result/CongestedComponentsTestData.scala index 1f938f4da3..dae9580dc1 100644 --- a/src/test/scala/edu/ie3/simona/test/common/result/CongestedComponentsTestData.scala +++ b/src/test/scala/edu/ie3/simona/test/common/result/CongestedComponentsTestData.scala @@ -34,11 +34,7 @@ trait CongestedComponentsTestData with NodeInputTestData with DbfsTestGrid { - val startTime: ZonedDateTime = TimeUtil.withDefaults.toZonedDateTime( - simonaConfig.simona.time.startDateTime - ) - - val endTime: ZonedDateTime = startTime.plusHours(2) + protected val voltageLimits: VoltageLimits = VoltageLimits(0.9, 1.1) val trafoType3W = new Transformer3WTypeInput( UUID.randomUUID(), @@ -78,7 +74,7 @@ trait CongestedComponentsTestData false, ) - protected val gridModel: GridModel = { + protected val extendedGridModel: GridModel = { val refSystem = RefSystem(Megawatts(600), Kilovolts(110)) val model = GridModel( @@ -187,6 +183,15 @@ trait CongestedComponentsTestData 0d.asDegreeGeom, ) + val lineResult34 = new LineResult( + startTime, + line3To4.getUuid, + 630d.asAmpere, + 0d.asDegreeGeom, + 630d.asAmpere, + 0d.asDegreeGeom, + ) + // transformer results val transformerResult1 = new Transformer2WResult( startTime,