From 542f33a3fa74eb1f99bc2cd34504a143782f1013 Mon Sep 17 00:00:00 2001 From: staudtMarius Date: Wed, 14 May 2025 10:52:04 +0200 Subject: [PATCH 1/7] Adding transformer tap changing to congestion mitigation. --- CHANGELOG.md | 1 + .../edu/ie3/simona/agent/grid/GridAgent.scala | 3 +- .../grid/TransformerTappingSupport.scala | 57 +++ .../CongestionManagementParams.scala | 16 +- .../agent/grid/congestion/DCMAlgorithm.scala | 42 +- .../agent/grid/congestion/VoltageRange.scala | 321 +++++++++++++ .../grid/congestion/data/AwaitingData.scala | 4 +- .../data/CongestionManagementData.scala | 11 + .../detection/CongestionDetection.scala | 39 +- .../detection/DetectionMessages.scala | 2 +- .../mitigations/MitigationProgress.scala | 48 ++ .../mitigations/MitigationSteps.scala | 13 + .../mitigations/TappingMessages.scala | 67 +++ .../mitigations/TransformerTapChange.scala | 237 ++++++++++ .../edu/ie3/simona/config/SimonaConfig.scala | 2 + .../model/control/TappingGroupModel.scala | 313 +++++++++++++ .../model/grid/TransformerTapping.scala | 145 +++++- .../grid/TransformerTappingSupportSpec.scala | 94 ++++ .../CongestedComponentsSpec.scala | 5 +- .../grid/congestion/CongestionSpec.scala | 262 +++++++++++ .../congestion/CongestionTestBaseData.scala | 37 +- .../grid/congestion/VoltageRangeSpec.scala | 421 ++++++++++++++++++ .../detection/CongestionDetectionSpec.scala | 58 +-- .../mitigation/TransformerTagChangeSpec.scala | 199 +++++++++ .../model/control/TappingGroupModelSpec.scala | 316 +++++++++++++ .../model/grid/Transformer3wModelSpec.scala | 3 +- .../model/grid/TransformerTappingSpec.scala | 61 +++ .../simona/test/common/ConfigTestData.scala | 11 + .../model/grid/GridComponentsMokka.scala | 169 +++++++ .../result/CongestedComponentsTestData.scala | 15 +- .../test/common/result/ResultMokka.scala | 42 ++ 31 files changed, 2931 insertions(+), 83 deletions(-) create mode 100644 src/main/scala/edu/ie3/simona/agent/grid/TransformerTappingSupport.scala create mode 100644 src/main/scala/edu/ie3/simona/agent/grid/congestion/VoltageRange.scala create mode 100644 src/main/scala/edu/ie3/simona/agent/grid/congestion/mitigations/MitigationProgress.scala create mode 100644 src/main/scala/edu/ie3/simona/agent/grid/congestion/mitigations/MitigationSteps.scala create mode 100644 src/main/scala/edu/ie3/simona/agent/grid/congestion/mitigations/TappingMessages.scala create mode 100644 src/main/scala/edu/ie3/simona/agent/grid/congestion/mitigations/TransformerTapChange.scala create mode 100644 src/main/scala/edu/ie3/simona/model/control/TappingGroupModel.scala create mode 100644 src/test/scala/edu/ie3/simona/agent/grid/TransformerTappingSupportSpec.scala rename src/test/scala/edu/ie3/simona/agent/grid/congestion/{detection => }/CongestedComponentsSpec.scala (97%) create mode 100644 src/test/scala/edu/ie3/simona/agent/grid/congestion/CongestionSpec.scala create mode 100644 src/test/scala/edu/ie3/simona/agent/grid/congestion/VoltageRangeSpec.scala create mode 100644 src/test/scala/edu/ie3/simona/agent/grid/congestion/mitigation/TransformerTagChangeSpec.scala create mode 100644 src/test/scala/edu/ie3/simona/model/control/TappingGroupModelSpec.scala create mode 100644 src/test/scala/edu/ie3/simona/model/grid/TransformerTappingSpec.scala create mode 100644 src/test/scala/edu/ie3/simona/test/common/model/grid/GridComponentsMokka.scala create mode 100644 src/test/scala/edu/ie3/simona/test/common/result/ResultMokka.scala diff --git a/CHANGELOG.md b/CHANGELOG.md index ba0037cbcf..9108ea087f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added congestion detection [#1186](https://github.com/ie3-institute/simona/issues/1186) +- 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/GridAgent.scala b/src/main/scala/edu/ie3/simona/agent/grid/GridAgent.scala index 5d8f46ef36..47d36de34f 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/GridAgent.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/GridAgent.scala @@ -195,6 +195,7 @@ object GridAgent extends DBFSAlgorithm with DCMAlgorithm { ), CongestionManagementParams( cfg.congestionManagement.enableDetection, + cfg.congestionManagement.enableTransformerTapChange, cfg.congestionManagement.timeout, ), SimonaActorNaming.actorName(ctx.self), @@ -353,7 +354,7 @@ object GridAgent extends DBFSAlgorithm with DCMAlgorithm { askMsgBuilder: ActorRef[GridAgent.Request] => Request, resMsgBuilder: Vector[(ActorRef[GridAgent.Request], T)] => InternalReply, ctx: ActorContext[GridAgent.Request], - )(implicit timeout: FiniteDuration): Unit = { + )(using timeout: FiniteDuration): Unit = { if (inferiorGridRefs.nonEmpty) { // creating implicit vals implicit val ec: ExecutionContext = ctx.executionContext diff --git a/src/main/scala/edu/ie3/simona/agent/grid/TransformerTappingSupport.scala b/src/main/scala/edu/ie3/simona/agent/grid/TransformerTappingSupport.scala new file mode 100644 index 0000000000..ff0ffb920f --- /dev/null +++ b/src/main/scala/edu/ie3/simona/agent/grid/TransformerTappingSupport.scala @@ -0,0 +1,57 @@ +/* + * © 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 + +import edu.ie3.simona.model.grid.TransformerTapping +import edu.ie3.util.quantities.QuantityUtils.asPu +import tech.units.indriya.ComparableQuantity + +import javax.measure.quantity.Dimensionless + +object TransformerTappingSupport { + + /** Method to get the tapping options. + * + * @param tappings + * All [[TransformerTapping]] models. + * @return + * The possible voltage increase and decrease. + */ + def getTappingOptions(tappings: Set[TransformerTapping]): ( + ComparableQuantity[Dimensionless], + ComparableQuantity[Dimensionless], + ) = { + // allow tapping only if all transformers support tapping + if (tappings.forall(_.hasAutoTap)) { + + val tappingRanges = tappings.map { tapping => + val currentPos = tapping.currentTapPos + val deltaV = tapping.deltaV.divide(-100) + val increase = deltaV.multiply(tapping.tapMin - currentPos) + val decrease = deltaV.multiply(tapping.tapMax - currentPos) + + (increase, decrease) + }.toSeq + + if (tappings.size == 1) { + tappingRanges(0) + } else { + // check for possible increase and decrease that can be applied to all transformers + + // TODO: Enhance this, to support transformer combinations with different tap deltas + ( + tappingRanges.map(_._1).minOption.getOrElse(0.asPu), + tappingRanges.map(_._2).maxOption.getOrElse(0.asPu), + ) + } + } else { + // no tapping possible + (0.asPu, 0.asPu) + } + } + +} 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..7be91fef00 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 @@ -11,14 +11,22 @@ 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, -) +) { + + def anyMitigationEnabled: Boolean = enableTransformerTapChange + +} 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 dff5b8fc59..cbb5af3fc9 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,6 +18,7 @@ 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.event.ResultEvent.PowerFlowResultEvent import org.apache.pekko.actor.typed.Behavior import org.apache.pekko.actor.typed.scaladsl.{ActorContext, StashBuffer} @@ -26,7 +27,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. * @param gridAgentBaseData @@ -49,7 +50,7 @@ trait DCMAlgorithm extends CongestionDetection { currentTick: Long, results: Option[PowerFlowResultEvent], ctx: ActorContext[Request], - )(implicit + )(using constantData: GridAgentConstantData, buffer: StashBuffer[Request], ): Behavior[Request] = { @@ -68,6 +69,35 @@ trait DCMAlgorithm extends CongestionDetection { ) } + private[grid] def doCongestionMitigation( + stateData: CongestionManagementData, + ctx: ActorContext[Request], + )(using + constantData: GridAgentConstantData, + buffer: StashBuffer[Request], + ): Behavior[Request] = { + // first we find an option for the next mitigation step + val (stepOption, updatedProgress) = + stateData.mitigationProgress.getNextStepsAndUpdate + + // we update the state data with the updated progress + val updatedStateData = stateData.copy(mitigationProgress = updatedProgress) + + stepOption match { + case Some(transformerTapChange: TransformerTapChange) => + GridAgent.updateTransformerTapping( + stateData, + AwaitingData(stateData.inferiorGridRefs.keySet), + ) + + case _ => + // we have no more mitigation steps + // we finish the mitigation + finishCongestionManagement(stateData, ctx) + } + + } + /** Method for finishing the congestion management. This method will return to * the [[GridAgent.idle()]] state afterward. * @param stateData @@ -84,10 +114,16 @@ trait DCMAlgorithm extends CongestionDetection { private[grid] def finishCongestionManagement( stateData: CongestionManagementData, ctx: ActorContext[Request], - )(implicit + )(using constantData: GridAgentConstantData, buffer: StashBuffer[Request], ): Behavior[Request] = { + 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) diff --git a/src/main/scala/edu/ie3/simona/agent/grid/congestion/VoltageRange.scala b/src/main/scala/edu/ie3/simona/agent/grid/congestion/VoltageRange.scala new file mode 100644 index 0000000000..ea22c2b70d --- /dev/null +++ b/src/main/scala/edu/ie3/simona/agent/grid/congestion/VoltageRange.scala @@ -0,0 +1,321 @@ +/* + * © 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.result.connector.LineResult +import edu.ie3.simona.agent.grid.GridAgent +import edu.ie3.simona.agent.grid.TransformerTappingSupport.getTappingOptions +import edu.ie3.simona.event.ResultEvent.PowerFlowResultEvent +import edu.ie3.simona.exceptions.CriticalFailureException +import edu.ie3.simona.model.grid.GridModel.GridComponents +import edu.ie3.simona.model.grid.{ + TransformerTapping, + TransformerTappingModel, + VoltageLimits, +} +import edu.ie3.util.quantities.QuantityUtils.asPu +import edu.ie3.util.scala.quantities.QuantityConversionUtils.{ + CurrentToSimona, + DimensionlessToSimona, +} +import org.apache.pekko.actor.typed.ActorRef +import tech.units.indriya.ComparableQuantity + +import java.util.UUID +import javax.measure.quantity.Dimensionless + +/** Object that contains information about possible voltage changes.

If the + * delta plus is negative -> upper voltage violation

If the delta minus is + * positive -> lower voltage violation

If both above cases happen at the + * same time the suggestion is set to the delta plus, because having a too high + * voltage is more severe. + * + * @param deltaPlus + * Maximal possible voltage increase. + * @param deltaMinus + * Maximal possible voltage decrease. + * @param suggestion + * For voltage change. + */ +final case class VoltageRange( + deltaPlus: ComparableQuantity[Dimensionless], + deltaMinus: ComparableQuantity[Dimensionless], + suggestion: ComparableQuantity[Dimensionless], +) { + + /** Method to update this voltage range with line voltage delta. + * + * @param deltaV + * To consider. + * @return + * A new [[VoltageRange]]. + */ + def updateWithLineDelta( + deltaV: ComparableQuantity[Dimensionless] + ): VoltageRange = { + val (plus, minus) = ( + deltaV.isGreaterThan(deltaPlus), + deltaV.isGreaterThan(deltaMinus), + ) match { + case (true, true) => + (deltaPlus, deltaPlus) + case (false, true) => + (deltaPlus, deltaV) + case (true, false) => + (deltaPlus, deltaPlus) + case (false, false) => + (deltaPlus, deltaMinus) + } + + VoltageRange(plus, minus) + } + + /** Method to update this voltage range with inferior voltage ranges. + * + * @param inferiorData + * Map: inferior grid to [[VoltageRange]] and [[TransformerTappingModel]]. + * @return + * A new [[VoltageRange]]. + */ + def updateWithInferiorRanges( + inferiorData: Map[ActorRef[ + GridAgent.Request + ], (VoltageRange, Set[TransformerTapping])] + ): VoltageRange = { + inferiorData.foldLeft(this) { case (range, (_, (infRange, tappings))) => + // get tapping options + val (possiblePlus, possibleMinus) = getTappingOptions(tappings) + + val increase = range.deltaPlus + .add(possibleMinus) + .isLessThanOrEqualTo(infRange.deltaPlus) + val decrease = range.deltaMinus + .add(possiblePlus) + .isGreaterThanOrEqualTo(infRange.deltaMinus) + + (increase, decrease) match { + case (true, true) => + VoltageRange(range.deltaPlus, range.deltaMinus) + case (true, false) => + VoltageRange( + range.deltaPlus, + infRange.deltaMinus.subtract(possiblePlus), + ) + case (false, true) => + VoltageRange( + infRange.deltaPlus.subtract(possibleMinus), + range.deltaMinus, + ) + case (false, false) => + VoltageRange(infRange.deltaPlus, infRange.deltaMinus) + } + } + } +} + +object VoltageRange { + + def apply( + deltaPlus: ComparableQuantity[Dimensionless], + deltaMinus: ComparableQuantity[Dimensionless], + ): VoltageRange = { + + val plus = deltaPlus.getValue.doubleValue() + val minus = deltaMinus.getValue.doubleValue() + + val value = if (plus > minus) { + // we could have a voltage violation of one limit + (plus + minus) / 2 + } else { + (plus > 0, minus < 0) match { + case (true, false) => + // we have a voltage violation of the lower limit + // since the upper limit is fine, we can increase the voltage a bit + plus + case (false, true) => + // we have a voltage violation of the upper limit + // since the lower limit is fine, we can decrease the voltage a bit + minus + case _ => + // we have a voltage violation of both limits, we can't fix this + 0 + } + } + + val factor = 1e3 + + val suggestion = if (value < 0) { + (value * factor).floor / factor + } else { + (value * factor).ceil / factor + } + + // check if tapping is required + if (plus < 0 || minus > 0) { + VoltageRange( + deltaPlus, + deltaMinus, + suggestion.asPu, + ) + } else { + // the voltage in this range is fine, set the suggested voltage change to zero + VoltageRange( + deltaPlus, + deltaMinus, + 0.asPu, + ) + } + } + + /** Method to calculate the possible range of voltage changes. + * + * @param powerFlowResultEvent + * Results from simulating the grid. + * @param voltageLimits + * Voltage limits. + * @param gridComponents + * All components of the grid. + * @param inferiorData + * Map: inferior grid to [[VoltageRange]] and [[TransformerTappingModel]]. + * @return + * A [[VoltageRange]]. + */ + def apply( + powerFlowResultEvent: PowerFlowResultEvent, + voltageLimits: VoltageLimits, + gridComponents: GridComponents, + inferiorData: Map[ActorRef[ + GridAgent.Request + ], (VoltageRange, Set[TransformerTapping])], + subnetNo: Int, + ): VoltageRange = { + // filter nodes in subnet + val nodesInSubnet = + gridComponents.nodes.filter(_.subnet == subnetNo).map(_.uuid) + + // calculate voltage range + val nodeResMap = powerFlowResultEvent.nodeResults + .filter(res => nodesInSubnet.contains(res.getInputModel)) + .map(res => res.getInputModel -> res.getvMag()) + .toMap + val minVoltage = nodeResMap + .minByOption(_._2) + .getOrElse(throw new CriticalFailureException(s"No node result found!")) + val maxVoltage = nodeResMap + .maxByOption(_._2) + .getOrElse(throw new CriticalFailureException(s"No node result found!")) + + // build initial range + val range = VoltageRange( + voltageLimits.vMax.subtract(maxVoltage._2), + voltageLimits.vMin.subtract(minVoltage._2), + ) + + // updating the voltage range prevent or cure line congestions + val deltaV = calculatePossibleVoltageDeltaForLines( + nodeResMap, + powerFlowResultEvent.lineResults, + gridComponents, + ) + val updatedRange = range.updateWithLineDelta(deltaV) + + if (inferiorData.isEmpty) { + // if there are no inferior grids, return the voltage range + updatedRange + } else { + // if there are inferior grids, update the voltage range + updatedRange.updateWithInferiorRanges(inferiorData) + } + } + + /** Method to calculate a voltage delta for the given line currents.

- If + * there is a line congestion, increasing the voltage by the returned delta + * should mitigate them.

- If there is no line congestion, the returned + * voltage shows the possible voltage decrease.

- Formula: V * I = (V + + * deltaV) * (I + deltaI). + * + * @param nodeResults + * Node voltages. + * @param lineResults + * Line currents. + * @param gridComponents + * Information of components. + * @return + * A voltage delta. + */ + def calculatePossibleVoltageDeltaForLines( + nodeResults: Map[UUID, ComparableQuantity[Dimensionless]], + lineResults: Iterable[LineResult], + gridComponents: GridComponents, + ): ComparableQuantity[Dimensionless] = { + val lineMap = gridComponents.lines.map(line => line.uuid -> line).toMap + + // calculate the voltage change that ensures there is no line congestion + val voltageChanges = + lineResults.map(res => res.getInputModel -> res).map { case (uuid, res) => + val line = lineMap(uuid) + + // calculate the voltage and the current change at the end of the line that sees the highest current + val (voltage, deltaI) = + if (res.getiAMag().isGreaterThan(res.getiBMag())) { + ( + nodeResults(line.nodeAUuid).toSquants, + line.iNom - res.getiAMag().toSquants, + ) + } else { + ( + nodeResults(line.nodeBUuid).toSquants, + line.iNom - res.getiBMag().toSquants, + ) + } + + // calculate the voltage change + (voltage * deltaI) / line.iNom * -1 + } + + // determine the actual possible voltage change + val change = voltageChanges.maxOption.getOrElse( + throw new CriticalFailureException(s"No line result found!") + ) + + // change < 0 => tapping down possible + // change > 0 => tapping up is necessary + change.asPu + } + + /** Combines the given [[VoltageRange]]s and adds the given offset. + * @param ranges + * Given ranges. + * @param offset + * To use. + * @return + * A new [[VoltageRange]]. + */ + def combineAndUpdate( + ranges: Iterable[VoltageRange], + offset: ComparableQuantity[Dimensionless], + ): VoltageRange = { + // finds the minimal increase + val minPlus = ranges.minByOption(_.deltaPlus).map(_.deltaPlus) + + // finds the maximal decrease + val maxMinus = ranges.maxByOption(_.deltaMinus).map(_.deltaMinus) + + (minPlus, maxMinus) match { + case (Some(plus), Some(minus)) if offset.isEquivalentTo(0.asPu) => + VoltageRange(plus, minus) + case (Some(plus), Some(minus)) => + VoltageRange( + plus.subtract(offset), + minus.subtract(offset), + offset.multiply(-1), + ) + case _ => + VoltageRange(0.asPu, 0.asPu) + } + } +} 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 4f1fdfd191..9d9c47661c 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.Request], T)] + receivedData: Seq[(ActorRef[GridAgent.Request], 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 2434204ce0..84da562500 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 @@ -7,12 +7,14 @@ package edu.ie3.simona.agent.grid.congestion.data import edu.ie3.datamodel.models.result.CongestionResult +import edu.ie3.simona.agent.grid.congestion.CongestionManagementParams import edu.ie3.datamodel.models.result.CongestionResult.InputModelType import edu.ie3.simona.agent.grid.GridAgent import edu.ie3.simona.agent.grid.GridAgentData.{ GridAgentBaseData, GridAgentDataInternal, } +import edu.ie3.simona.agent.grid.congestion.mitigations.MitigationProgress import edu.ie3.simona.agent.grid.congestion.{CongestedComponents, Congestions} import edu.ie3.simona.event.ResultEvent.PowerFlowResultEvent import edu.ie3.util.quantities.QuantityUtils.asPercent @@ -34,6 +36,10 @@ import scala.concurrent.duration.FiniteDuration * Result of the previous power flow calculation. * @param congestions * The found congestions. + * @param congestedComponents + * The components that have congestion. + * @param mitigationProgress + * The progress of the congestion mitigation. */ final case class CongestionManagementData( gridAgentBaseData: GridAgentBaseData, @@ -42,6 +48,7 @@ final case class CongestionManagementData( powerFlowResults: PowerFlowResultEvent, congestions: Congestions, congestedComponents: CongestedComponents, + mitigationProgress: MitigationProgress, ) extends GridAgentDataInternal { /** Builds a [[CongestionResult]] from the power flow results. @@ -123,6 +130,9 @@ final case class CongestionManagementData( def superiorGridRefs: Map[ActorRef[GridAgent.Request], Seq[UUID]] = gridAgentBaseData.superiorGridRefs + def congestionManagementParams: CongestionManagementParams = + gridAgentBaseData.congestionManagementParams + def timeout: FiniteDuration = gridAgentBaseData.congestionManagementParams.timeout } @@ -150,6 +160,7 @@ object CongestionManagementData { powerFlowResults, Congestions(congestedComponents), congestedComponents, + MitigationProgress(), ) } 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 b46652f02f..5df2950072 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 @@ -43,7 +43,7 @@ trait CongestionDetection { private[grid] def checkForCongestion( stateData: CongestionManagementData, awaitingData: AwaitingData[Congestions], - )(implicit + )(using constantData: GridAgentConstantData, buffer: StashBuffer[GridAgent.Request], ): Behavior[GridAgent.Request] = Behaviors.receivePartial { @@ -51,10 +51,10 @@ trait CongestionDetection { // request congestion check if we have inferior grids askInferior( stateData.inferiorGridRefs, - CongestionCheckRequest, - ReceivedCongestions, + CongestionCheckRequest.apply, + ReceivedCongestions.apply, ctx, - )(stateData.timeout) + )(using stateData.timeout) Behaviors.same @@ -93,7 +93,7 @@ trait CongestionDetection { awaitingData: AwaitingData[Congestions], congestionRequest: CongestionCheckRequest, ctx: ActorContext[GridAgent.Request], - )(implicit + )(using constantData: GridAgentConstantData, buffer: StashBuffer[GridAgent.Request], ): Behavior[GridAgent.Request] = { @@ -128,9 +128,9 @@ trait CongestionDetection { private def processReceivedData( stateData: CongestionManagementData, awaitingData: AwaitingData[Congestions], - congestions: Vector[(ActorRef[GridAgent.Request], Congestions)], + congestions: Seq[(ActorRef[GridAgent.Request], Congestions)], ctx: ActorContext[GridAgent.Request], - )(implicit + )(using constantData: GridAgentConstantData, buffer: StashBuffer[GridAgent.Request], ): Behavior[GridAgent.Request] = { @@ -140,10 +140,11 @@ trait CongestionDetection { if (stateData.gridAgentBaseData.isSuperior) { // if we are the superior grid, we find the next behavior - val congestions = stateData.congestions.combine(updatedData.values) + val combinedCongestions = + stateData.congestions.combine(updatedData.values) // checking for any congestion in the complete grid - if (!congestions.hasCongestion) { + if (!combinedCongestions.hasCongestion) { ctx.log.info( s"No congestions found. Finishing the congestion management." ) @@ -152,18 +153,22 @@ trait CongestionDetection { checkForCongestion(stateData, updatedData) } else { ctx.log.debug( - s"Congestion overall: $congestions" + s"Congestion overall: $combinedCongestions" ) - val timestamp = - constantData.simStartTime.plusSeconds(stateData.currentTick) + val updatedStateData = stateData.copy(congestions = combinedCongestions) - ctx.log.info( - s"There were some congestions that could not be resolved for timestamp: $timestamp." - ) + if (stateData.congestionManagementParams.anyMitigationEnabled) { + // the mitigation is enabled + // goto mitigation behavior + GridAgent.doCongestionMitigation(updatedStateData, ctx) - ctx.self ! FinishStep - checkForCongestion(stateData, updatedData) + } else { + // no mitigation enabled + // just finish the step + ctx.self ! FinishStep + checkForCongestion(updatedStateData, updatedData) + } } } 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 6223651bf0..ecc9eed0b6 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.Request], Congestions)] + values: Seq[(ActorRef[GridAgent.Request], Congestions)] ) extends InternalReply } diff --git a/src/main/scala/edu/ie3/simona/agent/grid/congestion/mitigations/MitigationProgress.scala b/src/main/scala/edu/ie3/simona/agent/grid/congestion/mitigations/MitigationProgress.scala new file mode 100644 index 0000000000..fdaefaa3c6 --- /dev/null +++ b/src/main/scala/edu/ie3/simona/agent/grid/congestion/mitigations/MitigationProgress.scala @@ -0,0 +1,48 @@ +/* + * © 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.congestion.mitigations.MitigationSteps.* + +case class MitigationProgress( + hasUsedTransformerTapChange: Boolean = false, + currentIteration: Int = 0, + maxNrOfOptimizationIterations: Int = 1, +) { + + def getNextStepsAndUpdate + : (Option[MitigationSteps.Value], MitigationProgress) = { + val maxOptimizationIterationReached = + currentIteration == maxNrOfOptimizationIterations + + hasUsedTransformerTapChange match { + case true if maxOptimizationIterationReached => + // we have reached the maximal amount of iterations + // we reset the progress and return no next step + (None, reset) + + case true => + ( + Some(TransformerTapChange), + copy(currentIteration = currentIteration + 1), + ) + + case false => + ( + Some(TransformerTapChange), + copy(hasUsedTransformerTapChange = true), + ) + + } + } + + def reset: MitigationProgress = copy( + hasUsedTransformerTapChange = false, + currentIteration = 0, + ) + +} 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..e9230382f1 --- /dev/null +++ b/src/main/scala/edu/ie3/simona/agent/grid/congestion/mitigations/MitigationSteps.scala @@ -0,0 +1,13 @@ +/* + * © 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 = 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..7e2121faea --- /dev/null +++ b/src/main/scala/edu/ie3/simona/agent/grid/congestion/mitigations/TappingMessages.scala @@ -0,0 +1,67 @@ +/* + * © 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 tech.units.indriya.ComparableQuantity + +import javax.measure.quantity.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.Request], + 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.Request], + 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.Request], (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: ComparableQuantity[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..696a194baf --- /dev/null +++ b/src/main/scala/edu/ie3/simona/agent/grid/congestion/mitigations/TransformerTapChange.scala @@ -0,0 +1,237 @@ +/* + * © 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.util.quantities.QuantityUtils.asPu +import org.apache.pekko.actor.typed.scaladsl.{ + ActorContext, + Behaviors, + StashBuffer, +} +import org.apache.pekko.actor.typed.{ActorRef, Behavior} +import tech.units.indriya.ComparableQuantity + +import javax.measure.quantity.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.Request], + ): Behavior[GridAgent.Request] = 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 + finishCongestionManagement(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.Request], + )(using + constantData: GridAgentConstantData, + buffer: StashBuffer[GridAgent.Request], + ): Behavior[GridAgent.Request] = { + val sender = request.sender + val subgrid = request.subgrid + + // check if waiting for inferior data is needed + if (awaitingData.notDone) { + 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.lvNodeUuid) + ) + 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.Request], (VoltageRange, Set[TransformerTapping])) + ], + ctx: ActorContext[GridAgent.Request], + )(using + constantData: GridAgentConstantData, + buffer: StashBuffer[GridAgent.Request], + ): Behavior[GridAgent.Request] = { + // updating the state data with received data from inferior grids + val updatedData = awaitingData.handleReceivingData(voltageRange) + + if (stateData.gridAgentBaseData.isSuperior) { + // there should be no voltage change in the superior grid, + // because the slack grid should always have 1 pu + + ctx.self ! VoltageDeltaResponse(0.asPu) + updateTransformerTapping(stateData, updatedData) + } else { + // un-stash all messages + buffer.unstashAll(updateTransformerTapping(stateData, updatedData)) + } + } + + private def handleUpdatedDataFromSuperior( + stateData: CongestionManagementData, + awaitingData: AwaitingData[(VoltageRange, Set[TransformerTapping])], + delta: ComparableQuantity[Dimensionless], + ctx: ActorContext[GridAgent.Request], + )(using + constantData: GridAgentConstantData, + buffer: StashBuffer[GridAgent.Request], + ): Behavior[GridAgent.Request] = { + // 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.debug( + s"Grid ${stateData.gridAgentBaseData.gridEnv.gridModel.subnetNo}, received delta: $delta" + ) + + if (stateData.inferiorGridRefs.nonEmpty) { + // 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.Request], 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, ctx.log) + group.refs.foreach(_ ! VoltageDeltaResponse(deltaV.add(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 733afcf159..94925dc624 100644 --- a/src/main/scala/edu/ie3/simona/config/SimonaConfig.scala +++ b/src/main/scala/edu/ie3/simona/config/SimonaConfig.scala @@ -126,8 +126,10 @@ object SimonaConfig { time: Simona.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 new file mode 100644 index 0000000000..7f3c7a75c7 --- /dev/null +++ b/src/main/scala/edu/ie3/simona/model/control/TappingGroupModel.scala @@ -0,0 +1,313 @@ +/* + * © 2024. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.model.control + +import edu.ie3.datamodel.models.input.connector.ConnectorPort +import edu.ie3.simona.agent.grid.congestion.VoltageRange +import edu.ie3.simona.agent.grid.GridAgent +import edu.ie3.simona.exceptions.GridInconsistencyException +import edu.ie3.simona.model.grid.Transformer3wPowerFlowCase.PowerFlowCaseA +import edu.ie3.simona.model.grid.{ + Transformer3wModel, + TransformerModel, + TransformerTapping, +} +import edu.ie3.util.quantities.QuantityUtils.asPu +import org.apache.pekko.actor.typed.ActorRef +import org.slf4j.Logger +import tech.units.indriya.ComparableQuantity + +import javax.measure.quantity.Dimensionless + +/** A group of [[TransformerTapping]] with all associated [[ActorRef]]s. + * @param refs + * a set of [[ActorRef]]s + * @param tappings + * a set of [[TransformerTapping]]s + */ +final case class TappingGroupModel( + tappings: Set[TransformerTapping], + refs: Set[ActorRef[GridAgent.Request]], + hasAutoTap: Boolean, +) { + + def updateTapPositions( + delta: ComparableQuantity[Dimensionless], + refMap: Map[ActorRef[GridAgent.Request], VoltageRange], + log: Logger, + ): ComparableQuantity[Dimensionless] = if (hasAutoTap) { + // get all possible voltage ranges of the inferior grids + val inferiorRanges = refs.map(refMap) + + val suggestion = + VoltageRange.combineAndUpdate(inferiorRanges, delta) + + // calculating the tap changes for all transformers and the resulting voltage delta + val (tapChange, deltaV) = calculateTapAndVoltage(suggestion) + + // change the tap pos of all transformers + tapChange.foreach { case (tapping, tapChange) => + tapChange compare 0 match { + case 1 => + // change > 0 -> increase + tapping.incrTapPos(tapChange) + case -1 => + // change < 0 -> decrease + tapping.decrTapPos(Math.abs(tapChange)) + case 0 => + // no change, do nothing + } + } + + log.debug( + s"For inferior grids $refs, suggestion: $suggestion, delta: $deltaV" + ) + + deltaV + } else 0.asPu + + /** Method for calculating the tap pos changes for all given transformers and + * the voltage delta. + * + * @param range + * given voltage range + * @return + * a map: model to tap pos change and resulting voltage delta + */ + def calculateTapAndVoltage( + range: VoltageRange + ): (Map[TransformerTapping, Int], ComparableQuantity[Dimensionless]) = { + val noTapping = (tappings.map(t => t -> 0).toMap, 0.asPu) + val suggestion = range.suggestion + + if (suggestion.isEquivalentTo(0.asPu)) { + return noTapping + } + + // calculate a tap option for each transformer + if (tappings.forall(_.hasAutoTap)) { + + // get all possible deltas + val possibleDeltas = tappings + .map( + _.getPossibleVoltageChanges( + range.deltaPlus, + range.deltaMinus, + ConnectorPort.B, + ) + ) + + // calculates a voltage change option + val deltaOption = if (possibleDeltas.exists(_.isEmpty)) { + // there is a transformer that cannot be tapped + None + } else { + // the actual delta that can be used for all transformers + Some(findCommonDelta(suggestion, possibleDeltas)) + } + + deltaOption match { + case Some(delta) => + // calculate the voltage and tap position change for all models + val deltas = tappings + .map(model => model -> model.computeDeltas(delta, ConnectorPort.B)) + .toMap + + // mapping the data + val taps = deltas.map { case (tapping, (tap, _)) => tapping -> tap } + val actualDelta = + deltas.map { case (_, (_, delta)) => delta }.toSeq(0) + + (taps, actualDelta) + case None => + noTapping + } + } else { + // return no tappings if there is at least one transformer that cannot be taped + noTapping + } + } + + /** Method for finding a common delta that can be applied to all transformers. + * @param suggestion + * the given suggestion + * @param possibleDeltas + * the possible deltas for each transformer + * @return + * either a common delta or zero + */ + def findCommonDelta( + suggestion: ComparableQuantity[Dimensionless], + possibleDeltas: Set[List[ComparableQuantity[Dimensionless]]], + ): ComparableQuantity[Dimensionless] = { + val expectedSize = possibleDeltas.size + // reduce and filter the possible options + val filteredOptions: Set[ComparableQuantity[Dimensionless]] = + getReducedOptions(suggestion, possibleDeltas) + .groupBy(identity) + .filter { case (_, seq) => seq.size == expectedSize } + .keySet + + // find the best suitable delta + filteredOptions.size match { + case 0 => 0.asPu + case 1 => filteredOptions.headOption.getOrElse(0.asPu) + case _ if filteredOptions.exists(_.isEquivalentTo(suggestion)) => + suggestion + case _ => // get the minimal and maximal option + val minOption = + filteredOptions.filter(_.isLessThan(suggestion)).lastOption + val maxOption = filteredOptions.find(_.isGreaterThan(suggestion)) + + (minOption, maxOption) match { + case (Some(min), Some(max)) => + val minDiff = + Math.abs(suggestion.subtract(min).getValue.doubleValue()) + val maxDiff = + Math.abs(suggestion.subtract(max).getValue.doubleValue()) + + // find the difference that is smaller + if (minDiff < maxDiff) { + min + } else max + + case (Some(min), _) => min // only minimal option found + case (_, Some(max)) => max // only maximal option found + case _ => 0.asPu // no option found -> therefore no change + } + } + } + + /** Method to reduce the possible deltas. + * @param suggestion + * to compare to + * @param possibleDeltas + * the possible deltas + * @return + * a reduced sequence + */ + private def getReducedOptions( + suggestion: ComparableQuantity[Dimensionless], + possibleDeltas: Set[List[ComparableQuantity[Dimensionless]]], + ): Seq[ComparableQuantity[Dimensionless]] = possibleDeltas.toSeq.flatMap { + deltas => + if (deltas.exists(_.isEquivalentTo(suggestion))) { + List(suggestion) + } else { + val minOption = + deltas.filter(_.isLessThan(suggestion)).sorted.lastOption + val maxOption = deltas.sorted.find(_.isGreaterThan(suggestion)) + + // check possible deltas + (minOption, maxOption) match { + case (Some(min), Some(max)) => List(min, max) + case (Some(min), _) => List(min) + case (_, Some(max)) => List(max) + case _ => List() + } + } + } + +} + +object TappingGroupModel { + + def apply( + tappings: Set[TransformerTapping], + refs: Set[ActorRef[GridAgent.Request]], + ): TappingGroupModel = + TappingGroupModel( + tappings, + refs, + tappings.forall(_.hasAutoTap), + ) + + /** Method for building [[TappingGroupModel]]s by grouping transformers with + * their [[ActorRef]]s. A group consist of all transformers connecting + * another grid with this grid and the [[ActorRef]] of the other grid. + * + *

If the other grid is connected by a port of [[Transformer3wModel]], + * only the model with [[PowerFlowCaseA]] is inside the returned map, due to + * the way the tapping works. Because the tapping also effects the other port + * of the [[Transformer3wModel]], the [[ActorRef]] of that grid needs to be + * in the same group and also all of its other connecting transformers, + * + *

Examples:

- grid 0 -> grid 1: [[TransformerModel]]

- grid 0 -> + * grid 1: [[Transformer3wModel]] port B

- grid 0 -> grid 2: + * [[Transformer3wModel]] port C

- grid 0 -> grid 3: [[TransformerModel]] + *

- grid 0 -> grid 4: two [[TransformerModel]] + * + *

Result:

- Group 1: one [[TransformerModel]] and one + * [[Transformer3wModel]] to [[ActorRef]]s of grid 1 and 2

- Group 2: one + * [[TransformerModel]] to [[ActorRef]] of grid 3

- Group 3: two + * [[TransformerModel]] to [[ActorRef]] of grid 4 + * + * @param receivedData + * map: actor ref to connecting transformers + * @param transformer3ws + * set of [[Transformer3wModel]] with [[PowerFlowCaseA]] + * @return + * a set of [[TappingGroupModel]]s + */ + def buildModels( + receivedData: Map[ActorRef[GridAgent.Request], Set[TransformerTapping]], + transformer3ws: Set[Transformer3wModel], + ): Set[TappingGroupModel] = { + val transformer3wMap = transformer3ws.map(t => t.uuid -> t).toMap + + // builds all groups + receivedData + .foldLeft( + Map.empty[Set[TransformerTapping], Set[ActorRef[GridAgent.Request]]] + ) { case (combined, (ref, tappings)) => + // get all transformer models + val updated: Set[TransformerTapping] = tappings.map { + case transformerModel: TransformerModel => + transformerModel + case transformer3wModel: Transformer3wModel => + // in case of a three winding transformer, we need the model of the port A + transformer3wMap.getOrElse( + transformer3wModel.uuid, + throw new GridInconsistencyException( + s"No three winding transformer found." + ), + ) + case unsupported => + throw new IllegalArgumentException( + s"The transformer type ${unsupported.getClass} is not supported." + ) + }.toSet + + // find a group that already contains one of the given transformer models + val keyOption = combined.keySet.find { keys => + updated.exists(key => keys.contains(key)) + } + + // if a key is found, add the current transformer models and the ref to that group + // else add a new group + keyOption + .map { key => + val refs = combined(key) + val updatedMap = combined.removed(key) + + val newKey = key ++ updated + val newValue = refs ++ Set(ref) + + updatedMap ++ Map(newKey -> newValue) + } + .getOrElse { + combined ++ Map(updated -> Set(ref)) + } + } + .map { case (tappingModels, refs) => + // building tapping groups + TappingGroupModel(tappingModels, refs) + } + .toSet + } + +} diff --git a/src/main/scala/edu/ie3/simona/model/grid/TransformerTapping.scala b/src/main/scala/edu/ie3/simona/model/grid/TransformerTapping.scala index f8730317b5..fcc8c8d9f1 100644 --- a/src/main/scala/edu/ie3/simona/model/grid/TransformerTapping.scala +++ b/src/main/scala/edu/ie3/simona/model/grid/TransformerTapping.scala @@ -6,7 +6,11 @@ package edu.ie3.simona.model.grid +import edu.ie3.datamodel.models.input.connector.ConnectorPort import edu.ie3.util.quantities.PowerSystemUnits._ +import edu.ie3.util.quantities.QuantityUtils.asPu +import tech.units.indriya.ComparableQuantity + import javax.measure.Quantity import javax.measure.quantity.Dimensionless import tech.units.indriya.quantity.Quantities @@ -28,6 +32,25 @@ trait TransformerTapping { def getTapRation: Double = tapRatio + /** Returns [[TransformerTappingModel.autoTap]]. + */ + def hasAutoTap: Boolean = transformerTappingModel.autoTap + + /** Returns the maximal tap position. + */ + def tapMax: Int = transformerTappingModel.tapMax + + /** Returns the minimal tap position. + */ + def tapMin: Int = transformerTappingModel.tapMin + + /** Returns the voltage change per tap position in pu. + */ + def deltaV: ComparableQuantity[Dimensionless] = + transformerTappingModel.deltaV.getValue.doubleValue().asPu + + /** Returns the current tap position. + */ def currentTapPos: Int = transformerTappingModel.currentTapPos /** Initialize the tapping model. Should be called after creating the @@ -61,12 +84,16 @@ trait TransformerTapping { tapRatio = transformerTappingModel.decrTapPos(deltaTap) /** Determine the amount of tap positions to increase oder decrease in order - * to meet the desired change in voltage magnitude. For details on the - * implementation see [[TransformerTappingModel.computeDeltaTap()]] + * to meet the desired change in voltage magnitude at the given transformer + * side. For details on the implementation see + * [[TransformerTappingModel.computeDeltaTap()]]. This method considers the + * side at which the change is requested. * * @param vChangeRequest * desired change in voltage magnitude (> 0 --> increase voltage, < 0 --> * decrease voltage) + * @param tapSide + * the side of the transformer at which the given voltage change is desired * @param deadBand * as a portion of the transformer voltage ratio per tap, it defaults to 75 * % of the deltaV of a tap @@ -76,8 +103,118 @@ trait TransformerTapping { */ def computeDeltaTap( vChangeRequest: Quantity[Dimensionless], + tapSide: ConnectorPort = ConnectorPort.A, + deadBand: Quantity[Dimensionless] = Quantities.getQuantity(0.75, PU), + ): Int = { + if (isSameSide(tapSide)) { + transformerTappingModel.computeDeltaTap(vChangeRequest, deadBand) + } else { + transformerTappingModel.computeDeltaTap( + vChangeRequest.multiply(-1), + deadBand, + ) + } + } + + /** Determines all possible voltage deltas that can be achieved by tapping. + * This method considers the side at which the change is requested. + * + * @param maxIncrease + * maximum allowed voltage increase + * @param maxDecrease + * maximal allowed voltage decrease + * @param tapSide + * side of the tapping + * @return + * a list of possible voltage deltas + */ + def getPossibleVoltageChanges( + maxIncrease: ComparableQuantity[Dimensionless], + maxDecrease: ComparableQuantity[Dimensionless], + tapSide: ConnectorPort = ConnectorPort.A, + ): List[ComparableQuantity[Dimensionless]] = { + val plus = tapMax - currentTapPos + val minus = tapMin - currentTapPos + + val range = + Range.inclusive(minus, plus).map(deltaV.multiply(_).divide(100)).toList + + val values = if (isSameSide(tapSide)) { + range + } else { + range.map(_.multiply(-1)).sortBy(_.getValue.doubleValue()) + } + + ( + maxIncrease.isLessThan(maxDecrease), + maxIncrease.isLessThan(0.asPu), + ) match { + case (true, true) => + // maximal increase is less then maximal allowed decrease -> only max decrease as possible change + values.filter(_.isEquivalentTo(maxDecrease)) + case (true, _) => + // maximal decrease is greater then maximal allowed increase -> only max increase as possible change + values.filter(_.isEquivalentTo(maxIncrease)) + case _ => + // find all values between the maximal allowed increase and decrease + values.filter(value => + value.isLessThanOrEqualTo(maxIncrease) && value + .isGreaterThanOrEqualTo( + maxDecrease + ) + ) + } + } + + /** Determine the amount of tap positions to increase oder decrease in order + * to meet the desired change in voltage magnitude at the given transformer + * side. For details on the implementation see + * [[TransformerTappingModel.computeDeltaTap()]] and the resulting voltage + * delta. This method considers the side at which the change is requested. + * + * @param vChangeRequest + * desired change in voltage magnitude (> 0 --> increase voltage, < 0 --> + * decrease voltage) + * @param tapSide + * the side of the transformer at which the given voltage change is desired + * @param deadBand + * as a portion of the transformer voltage ratio per tap, it defaults to 75 + * % of the deltaV of a tap + * @return + * the needed in- or decrease of the transformer tap position to reach the + * desired change in voltage magnitude or zero if not possible and the + * resulting voltage delta + */ + def computeDeltas( + vChangeRequest: Quantity[Dimensionless], + tapSide: ConnectorPort = ConnectorPort.A, deadBand: Quantity[Dimensionless] = Quantities.getQuantity(0.75, PU), - ): Int = - transformerTappingModel.computeDeltaTap(vChangeRequest, deadBand) + ): (Int, ComparableQuantity[Dimensionless]) = { + val taps = computeDeltaTap(vChangeRequest, tapSide, deadBand) + val deltaV = + transformerTappingModel.deltaV.to(PU).getValue.doubleValue() * taps + + if (isSameSide(tapSide)) { + (taps, deltaV.asPu) + } else { + (taps, deltaV.asPu.multiply(-1)) + } + } + /** Method to check if a given port matches the port of this model. + * @param tapSide + * to check. + * @return + * true if both ports are either on the higher or lower side + */ + private def isSameSide(tapSide: ConnectorPort): Boolean = + (transformerTappingModel.tapSide, tapSide) match { + case (ConnectorPort.A, ConnectorPort.A) => true // both on higher side + case (ConnectorPort.A, _) => false // both on different sides + case (ConnectorPort.B, ConnectorPort.A) | + (ConnectorPort.C, ConnectorPort.A) => + false // both on different sides + case (ConnectorPort.B, _) | (ConnectorPort.C, _) => + true // both on lower side + } } diff --git a/src/test/scala/edu/ie3/simona/agent/grid/TransformerTappingSupportSpec.scala b/src/test/scala/edu/ie3/simona/agent/grid/TransformerTappingSupportSpec.scala new file mode 100644 index 0000000000..08a708d7ac --- /dev/null +++ b/src/test/scala/edu/ie3/simona/agent/grid/TransformerTappingSupportSpec.scala @@ -0,0 +1,94 @@ +/* + * © 2024. 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 + +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.TransformerTappingSupport.getTappingOptions +import edu.ie3.simona.agent.grid.congestion.VoltageRange.calculatePossibleVoltageDeltaForLines +import edu.ie3.simona.agent.grid.congestion.{Congestions, VoltageRange} +import edu.ie3.simona.event.ResultEvent.PowerFlowResultEvent +import edu.ie3.simona.model.grid.GridModel.GridComponents +import edu.ie3.simona.model.grid.{ + GridModel, + RefSystem, + TransformerTapping, + 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.asPu +import org.apache.pekko.actor.testkit.typed.scaladsl.{ + ScalaTestWithActorTestKit, + TestProbe, +} +import squants.electro.Kilovolts +import squants.energy.Kilowatts +import tech.units.indriya.quantity.Quantities + +import java.time.ZonedDateTime + +class TransformerTappingSupportSpec + extends ScalaTestWithActorTestKit + with UnitSpec + with GridComponentsMokka + with ResultMokka + with SubGridGateMokka + with DbfsTestGrid + with ConfigTestData { + + val voltageTolerance = 1e-3 + + protected val inferior1: TestProbe[GridAgent.Request] = + TestProbe[GridAgent.Request]("inferior1") + protected val inferior2: TestProbe[GridAgent.Request] = + TestProbe[GridAgent.Request]("inferior2") + + "TransformerTappingSupportSpec" should { + + "get tapping options correctly" in { + val tappingModel1: TransformerTapping = mockTransformerTapping( + autoTap = true, + currentTapPos = 3, + tapMax = 4, + deltaV = 1.asPu, + ) + + val tappingModel2: TransformerTapping = mockTransformerTapping( + autoTap = true, + currentTapPos = 1, + tapMax = 3, + tapMin = -2, + deltaV = 1.asPu, + ) + + val cases = Table( + ("tappings", "expectedPlus", "expectedMinus"), + (Set(tappingModel1), 0.08.asPu, -0.01.asPu), + (Set(tappingModel2), 0.03.asPu, -0.02.asPu), + (Set(tappingModel1, tappingModel2), 0.03.asPu, -0.01.asPu), + ) + + forAll(cases) { (tappings, expectedPlus, expectedMinus) => + val (actualPlus, actualMinus) = getTappingOptions(tappings) + + actualPlus shouldBe expectedPlus + actualMinus shouldBe expectedMinus + } + } + } +} diff --git a/src/test/scala/edu/ie3/simona/agent/grid/congestion/detection/CongestedComponentsSpec.scala b/src/test/scala/edu/ie3/simona/agent/grid/congestion/CongestedComponentsSpec.scala similarity index 97% rename from src/test/scala/edu/ie3/simona/agent/grid/congestion/detection/CongestedComponentsSpec.scala rename to src/test/scala/edu/ie3/simona/agent/grid/congestion/CongestedComponentsSpec.scala index 0f5e74cb1f..ba1b6cc82d 100644 --- a/src/test/scala/edu/ie3/simona/agent/grid/congestion/detection/CongestedComponentsSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/grid/congestion/CongestedComponentsSpec.scala @@ -4,15 +4,18 @@ * Research group Distribution grid planning and operation */ -package edu.ie3.simona.agent.grid.congestion.detection +package edu.ie3.simona.agent.grid.congestion import edu.ie3.simona.agent.grid.congestion.CongestedComponents import edu.ie3.simona.event.ResultEvent.PowerFlowResultEvent import edu.ie3.simona.test.common.UnitSpec import edu.ie3.simona.test.common.result.CongestedComponentsTestData +import edu.ie3.util.TimeUtil import edu.ie3.util.scala.quantities.{ApparentPower, Kilovoltamperes} import squants.electro.{Amperes, Kilovolts} +import java.time.ZonedDateTime + class CongestedComponentsSpec extends UnitSpec with CongestedComponentsTestData { 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..60c6009b66 --- /dev/null +++ b/src/test/scala/edu/ie3/simona/agent/grid/congestion/CongestionSpec.scala @@ -0,0 +1,262 @@ +/* + * © 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.VoltageRange.calculatePossibleVoltageDeltaForLines +import edu.ie3.simona.agent.grid.congestion.{Congestions, VoltageRange} +import edu.ie3.simona.event.ResultEvent.PowerFlowResultEvent +import edu.ie3.simona.model.grid.GridModel.GridComponents +import edu.ie3.simona.model.grid.{ + GridModel, + RefSystem, + TransformerTapping, + 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.{asPu, asDegreeGeom} +import org.apache.pekko.actor.testkit.typed.scaladsl.{ + ScalaTestWithActorTestKit, + TestProbe, +} +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 a5306e9453..3a01dc512e 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 @@ -7,14 +7,17 @@ package edu.ie3.simona.agent.grid.congestion import com.typesafe.config.ConfigFactory +import edu.ie3.util.quantities.QuantityUtils.asPu import edu.ie3.simona.agent.EnvironmentRefs -import edu.ie3.simona.agent.grid.GridAgent +import edu.ie3.simona.agent.grid.{GridAgent, GridEnvironment} import edu.ie3.simona.agent.grid.GridAgentData.{ GridAgentBaseData, GridAgentConstantData, } 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.services.{ LoadProfileMessage, ServiceMessage, @@ -31,6 +34,8 @@ import org.apache.pekko.actor.testkit.typed.scaladsl.{ import org.apache.pekko.actor.typed.scaladsl.{Behaviors, StashBuffer} import org.apache.pekko.actor.typed.{ActorRef, Behavior} import org.mockito.Mockito.when +import squants.electro.Kilovolts +import squants.energy.Megawatts import java.time.ZonedDateTime import scala.concurrent.duration.DurationInt @@ -50,9 +55,8 @@ trait CongestionTestBaseData .resolve() ) - val startTime: ZonedDateTime = TimeUtil.withDefaults.toZonedDateTime( - config.simona.time.startDateTime - ) + protected val refSystem: RefSystem = + RefSystem(Megawatts(600), Kilovolts(110d)) protected val scheduler: TestProbe[SchedulerMessage] = TestProbe("scheduler") protected val runtimeEvents: TestProbe[RuntimeEvent] = TestProbe( @@ -81,7 +85,7 @@ trait CongestionTestBaseData "resultListener" ) - protected implicit val constantData: GridAgentConstantData = + protected given constantData: GridAgentConstantData = GridAgentConstantData( environmentRefs, simonaConfig, @@ -91,25 +95,17 @@ trait CongestionTestBaseData mock[ActorRef[Activation]], ) - def spawnWithBuffer( - factory: StashBuffer[GridAgent.Request] => Behavior[GridAgent.Request], - capacity: Int = 10, - ): ActorRef[GridAgent.Request] = - testKit.spawn( - Behaviors.withStash(capacity) { buffer => - factory(buffer) - } - ) - def gridAgentBaseData( inferiorRefs: Set[ActorRef[GridAgent.Request]] = 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, ) @@ -117,6 +113,17 @@ trait CongestionTestBaseData when(data.congestionManagementParams).thenReturn(cmParams) when(data.inferiorGridRefs).thenReturn(map) + if (gridModel.nonEmpty) { + val gridEnv = mock[GridEnvironment] + when(gridEnv.gridModel).thenReturn( + gridModel.getOrElse( + throw new CriticalFailureException("No grid model found!") + ) + ) + + when(data.gridEnv).thenReturn(gridEnv) + } + data } diff --git a/src/test/scala/edu/ie3/simona/agent/grid/congestion/VoltageRangeSpec.scala b/src/test/scala/edu/ie3/simona/agent/grid/congestion/VoltageRangeSpec.scala new file mode 100644 index 0000000000..6b34023ae0 --- /dev/null +++ b/src/test/scala/edu/ie3/simona/agent/grid/congestion/VoltageRangeSpec.scala @@ -0,0 +1,421 @@ +/* + * © 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.result.NodeResult +import edu.ie3.datamodel.models.result.connector.LineResult +import edu.ie3.simona.agent.grid.GridAgent +import edu.ie3.simona.agent.grid.congestion.VoltageRange +import edu.ie3.simona.agent.grid.congestion.VoltageRange.calculatePossibleVoltageDeltaForLines +import edu.ie3.simona.event.ResultEvent.PowerFlowResultEvent +import edu.ie3.simona.model.grid.GridModel.GridComponents +import edu.ie3.simona.model.grid.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.QuantityUtils.{asAmpere, asPu} +import org.apache.pekko.actor.testkit.typed.scaladsl.{ + ScalaTestWithActorTestKit, + TestProbe, +} +import tech.units.indriya.ComparableQuantity + +import javax.measure.quantity.Dimensionless + +class VoltageRangeSpec + extends ScalaTestWithActorTestKit + with UnitSpec + with GridComponentsMokka + with ResultMokka + with SubGridGateMokka + with DbfsTestGrid + with ConfigTestData { + + given puTolerance: ComparableQuantity[Dimensionless] = 1e-6.asPu + + protected val inferior1: TestProbe[GridAgent.Request] = + TestProbe[GridAgent.Request]("inferior1") + protected val inferior2: TestProbe[GridAgent.Request] = + TestProbe[GridAgent.Request]("inferior2") + + "A VoltageRange" should { + + "calculate the suggestion correctly" in { + val cases = Table( + ("deltaPlus", "deltaMinus", "expected"), + (0.05.asPu, -0.03.asPu, 0.asPu), // no voltage limit violation + ( + -0.01.asPu, + -0.02.asPu, + -0.015.asPu, + ), // upper voltage limit violation (both are negative), decreasing voltage + ( + 0.02.asPu, + 0.01.asPu, + 0.015.asPu, + ), // lower voltage limit violation (both are positive), increasing voltage + ( + 0.01.asPu, + 0.02.asPu, + 0.01.asPu, + ), // violation of both lower limit, upper > 0, increase voltage to the upper limit + ( + -0.02.asPu, + -0.01.asPu, + -0.01.asPu, + ), // violation of both upper limit, lower < 0, decrease voltage to the lower limit + ( + -0.01.asPu, + 0.01.asPu, + 0.asPu, + ), // violation of both voltage limits (upper negative, lower positive), do nothing + ) + + forAll(cases) { (deltaPlus, deltaMinus, expected) => + val suggestion = VoltageRange( + deltaPlus, + deltaMinus, + ).suggestion + + suggestion should equalWithTolerance(expected) + } + } + + "calculates the possible voltage delta for lines correctly" in { + val node1 = nodeModel() + val node2 = nodeModel() + val node3 = nodeModel() + + val line12 = lineModel(node1.uuid, node2.uuid) + val line13 = lineModel(node1.uuid, node3.uuid) + + val gridComponents = GridComponents( + Seq(node1, node2, node3), + Set(line12, line13), + Set.empty, + Set.empty, + Set.empty, + ) + + val cases = Table( + ("results", "deltaV"), + ( + buildPowerFlowResultEvent( + Set( + mockNodeResult(node1.uuid, 0.93.asPu), + mockNodeResult(node2.uuid, 0.95.asPu), + mockNodeResult(node3.uuid, 0.95.asPu), + ), + Set( + mockLineResult(line12.uuid, 5.asAmpere, 5.asAmpere), + mockLineResult(line13.uuid, 11.asAmpere, 10.9.asAmpere), + ), + ), + 0.093.asPu, // min voltage increase to resolve line congestion + ), + ( + buildPowerFlowResultEvent( + Set( + mockNodeResult(node1.uuid, 0.93.asPu), + mockNodeResult(node2.uuid, 0.95.asPu), + mockNodeResult(node3.uuid, 0.95.asPu), + ), + Set( + mockLineResult(line12.uuid, 9.3.asAmpere, 9.2.asAmpere), + mockLineResult(line13.uuid, 8.asAmpere, 8.asAmpere), + ), + ), + -0.0651.asPu, // max voltage decrease until line congestion occur + ), + ) + + forAll(cases) { (results, deltaV) => + val nodeResults = results.nodeResults + .map(res => res.getInputModel -> res.getvMag()) + .toMap + + val actualDeltaV = calculatePossibleVoltageDeltaForLines( + nodeResults, + results.lineResults, + gridComponents, + ) + + actualDeltaV should equalWithTolerance(deltaV) + } + } + + "calculate the voltage range for a lowest grid correctly" in { + val node1 = nodeModel() + val node2 = nodeModel() + val node3 = nodeModel() + val node4 = nodeModel() + + val line12 = lineModel(node1.uuid, node2.uuid) + val line13 = lineModel(node1.uuid, node3.uuid) + val line34 = lineModel(node3.uuid, node4.uuid) + + val gridComponents = GridComponents( + Seq(node1, node2, node3, node4), + Set(line12, line13, line34), + Set.empty, + Set.empty, + Set.empty, + ) + + val powerFlowResult = buildPowerFlowResultEvent( + Set( + mockNodeResult(node1.uuid, 0.93.asPu), + mockNodeResult(node2.uuid, 0.95.asPu), + mockNodeResult(node3.uuid, 1.05.asPu), + mockNodeResult(node4.uuid, 0.97.asPu), + ), + Set( + mockLineResult(line12.uuid, 5.asAmpere, 5.asAmpere), + mockLineResult(line13.uuid, 8.asAmpere, 8.asAmpere), + mockLineResult(line34.uuid, 7.asAmpere, 7.asAmpere), + ), + ) + + val range = VoltageRange( + powerFlowResult, + VoltageLimits(0.9, 1.1), + gridComponents, + Map.empty, + subnetNo = 1, + ) + + range.deltaPlus should equalWithTolerance(0.05.asPu) + range.deltaMinus should equalWithTolerance(-0.03.asPu) + range.suggestion should equalWithTolerance(0.asPu) + } + + "calculates the voltage range for a middle grid correctly" in { + val node1 = nodeModel() + val node2 = nodeModel() + val node3 = nodeModel() + val node4 = nodeModel() + + val line12 = lineModel(node1.uuid, node2.uuid) + val line13 = lineModel(node1.uuid, node3.uuid) + val line34 = lineModel(node3.uuid, node4.uuid) + + val gridComponents = GridComponents( + Seq(node1, node2, node3, node4), + Set(line12, line13, line34), + Set.empty, + Set.empty, + Set.empty, + ) + + val tappingModel = mockTransformerTapping( + autoTap = true, + tapMax = 3, + tapMin = -3, + deltaV = 1.asPu, + ) + + val powerFlowResult = buildPowerFlowResultEvent( + Set( + mockNodeResult(node1.uuid, 0.93.asPu), + mockNodeResult(node2.uuid, 0.95.asPu), + mockNodeResult(node3.uuid, 1.05.asPu), + mockNodeResult(node4.uuid, 0.97.asPu), + ), + Set( + mockLineResult(line12.uuid, 5.asAmpere, 5.asAmpere), + mockLineResult(line13.uuid, 8.asAmpere, 8.asAmpere), + mockLineResult(line34.uuid, 7.asAmpere, 7.asAmpere), + ), + ) + + // the voltage range of the given grid is limited by the voltage range + // of the inferior grids and the possible transformer tapping + val range = VoltageRange( + powerFlowResult, + VoltageLimits(0.9, 1.1), + gridComponents, + Map( + inferior1.ref -> (VoltageRange(0.1.asPu, 0.01.asPu), Set( + tappingModel + )), + inferior2.ref -> (VoltageRange(0.01.asPu, -0.04.asPu), Set( + tappingModel + )), + ), + subnetNo = 1, + ) + + range.deltaPlus should equalWithTolerance(0.04.asPu) + range.deltaMinus should equalWithTolerance(-0.02.asPu) + range.suggestion should equalWithTolerance(0.asPu) + } + + "be updated with a line voltage delta correctly" in { + val range1 = VoltageRange(0.05.asPu, -0.05.asPu) + val cases1 = Table( + ("deltaV", "plus", "minus"), + (0.01.asPu, 0.05.asPu, 0.01.asPu), + (0.06.asPu, 0.05.asPu, 0.05.asPu), + (-0.01.asPu, 0.05.asPu, -0.01.asPu), + (-0.04.asPu, 0.05.asPu, -0.04.asPu), + (-0.06.asPu, 0.05.asPu, -0.05.asPu), + ) + + forAll(cases1) { (deltaV, plus, minus) => + val updated = range1.updateWithLineDelta(deltaV) + updated.deltaPlus should equalWithTolerance(plus) + updated.deltaMinus should equalWithTolerance(minus) + } + + val range2 = VoltageRange(-0.01.asPu, -0.05.asPu) + val cases2 = Table( + ("deltaV", "plus", "minus"), + (0.01.asPu, -0.01.asPu, -0.01.asPu), + (0.06.asPu, -0.01.asPu, -0.01.asPu), + (-0.01.asPu, -0.01.asPu, -0.01.asPu), + (-0.04.asPu, -0.01.asPu, -0.04.asPu), + (-0.06.asPu, -0.01.asPu, -0.05.asPu), + ) + + forAll(cases2) { (deltaV, plus, minus) => + val updated = range2.updateWithLineDelta(deltaV) + updated.deltaPlus should equalWithTolerance(plus) + updated.deltaMinus should equalWithTolerance(minus) + } + + val range3 = VoltageRange(0.05.asPu, 0.01.asPu) + val cases3 = Table( + ("deltaV", "plus", "minus"), + (0.01.asPu, 0.05.asPu, 0.01.asPu), + (0.06.asPu, 0.05.asPu, 0.05.asPu), + (-0.01.asPu, 0.05.asPu, 0.01.asPu), + (-0.04.asPu, 0.05.asPu, 0.01.asPu), + (-0.06.asPu, 0.05.asPu, 0.01.asPu), + ) + + forAll(cases3) { (deltaV, plus, minus) => + val updated = range3.updateWithLineDelta(deltaV) + updated.deltaPlus should equalWithTolerance(plus) + updated.deltaMinus should equalWithTolerance(minus) + } + + } + + "be updated with inferior voltage ranges and without tapping correctly" in { + val range = VoltageRange(0.05.asPu, -0.05.asPu) + + val tappingModel = + mockTransformerTapping( + tapMax = 10, + tapMin = -10, + deltaV = 1.asPu, + ) + + val cases = Table( + ("range1", "range2", "expected"), + ( + VoltageRange(0.02.asPu, -0.06.asPu), + VoltageRange(0.06.asPu, -0.03.asPu), + VoltageRange(0.02.asPu, -0.03.asPu), + ), + ( + VoltageRange(0.06.asPu, -0.06.asPu), + VoltageRange(0.06.asPu, -0.06.asPu), + VoltageRange(0.05.asPu, -0.05.asPu), + ), + ( + VoltageRange(0.asPu, -0.01.asPu), + VoltageRange(0.02.asPu, -0.03.asPu), + VoltageRange(0.asPu, -0.01.asPu), + ), + ( + VoltageRange(0.02.asPu, 0.01.asPu), + VoltageRange(0.04.asPu, -0.01.asPu), + VoltageRange(0.02.asPu, 0.01.asPu), + ), + ) + + forAll(cases) { (range1, range2, expected) => + val updatedRange = range.updateWithInferiorRanges( + Map( + inferior1.ref -> (range1, Set(tappingModel)), + inferior2.ref -> (range2, Set(tappingModel)), + ) + ) + + updatedRange.deltaPlus should equalWithTolerance(expected.deltaPlus) + updatedRange.deltaMinus should equalWithTolerance(expected.deltaMinus) + } + } + + "be updated with inferior voltage ranges and with tapping correctly" in { + val range = VoltageRange(0.05.asPu, -0.05.asPu) + + val tappingModel = mockTransformerTapping( + autoTap = true, + currentTapPos = 7, + tapMax = 10, + tapMin = -10, + deltaV = 1.asPu, + ) + + val cases = Table( + ("range1", "range2", "expected"), + ( + VoltageRange(0.02.asPu, -0.06.asPu), + VoltageRange(0.06.asPu, -0.03.asPu), + VoltageRange(0.05.asPu, -0.05.asPu), + ), + ( + VoltageRange(0.06.asPu, -0.06.asPu), + VoltageRange(0.06.asPu, -0.06.asPu), + VoltageRange(0.05.asPu, -0.05.asPu), + ), + ( + VoltageRange(0.asPu, -0.01.asPu), + VoltageRange(0.02.asPu, -0.03.asPu), + VoltageRange(0.03.asPu, -0.05.asPu), + ), + ( + VoltageRange(0.02.asPu, 0.01.asPu), + VoltageRange(0.04.asPu, -0.01.asPu), + VoltageRange(0.05.asPu, -0.05.asPu), + ), + ) + + forAll(cases) { (range1, range2, expected) => + val updatedRange = range.updateWithInferiorRanges( + Map( + inferior1.ref -> (range1, Set(tappingModel)), + inferior2.ref -> (range2, Set(tappingModel)), + ) + ) + + updatedRange.deltaPlus should equalWithTolerance(expected.deltaPlus) + updatedRange.deltaMinus should equalWithTolerance(expected.deltaMinus) + } + } + + def buildPowerFlowResultEvent( + nodeResults: Set[NodeResult], + lineResults: Set[LineResult], + ): PowerFlowResultEvent = { + PowerFlowResultEvent( + nodeResults, + Set.empty, + lineResults, + Set.empty, + Set.empty, + ) + } + } + +} 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 269b0e2efb..0249885fdb 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.mitigations.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.Request] = TestProbe("superiorAgent") - val inferiorAgent: TestProbe[GridAgent.Request] = TestProbe("inferiorAgent") + protected val superiorAgent: TestProbe[GridAgent.Request] = TestProbe( + "superiorAgent" + ) + protected val inferiorAgent: TestProbe[GridAgent.Request] = TestProbe( + "inferiorAgent" + ) "The congestion detection" should { + def spawnBehavior( + stateData: CongestionManagementData, + awaitingData: AwaitingData[Congestions], + capacity: Int = 10, + ): ActorRef[GridAgent.Request] = testKit.spawn( + Behaviors.withStash[GridAgent.Request](capacity) { buffer => + GridAgent.checkForCongestion( + stateData, + awaitingData, + )(using constantData, buffer) + } + ) "answer a request for congestions correctly" in { val stateData = CongestionManagementData( @@ -64,6 +82,7 @@ class CongestionDetectionSpec transformerCongestions = false, ), CongestedComponents.empty, + MitigationProgress(), ) val cases = Table( @@ -113,12 +132,7 @@ class CongestionDetectionSpec forAll(cases) { (inferiorData, expectedCongestions) => val awaitingData = AwaitingData(inferiorData) - val behavior = spawnWithBuffer( - GridAgent.checkForCongestion( - stateData, - awaitingData, - )(constantData, _) - ) + val behavior = spawnBehavior(stateData, awaitingData) behavior ! CongestionCheckRequest(superiorAgent.ref) @@ -128,7 +142,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, @@ -146,17 +160,13 @@ class CongestionDetectionSpec transformerCongestions = false, ), CongestedComponents.empty, + MitigationProgress(), ) val awaitingData: AwaitingData[Congestions] = AwaitingData(Set(inferiorAgent.ref)) - val behavior = spawnWithBuffer( - GridAgent.checkForCongestion( - stateData, - awaitingData, - )(constantData, _) - ) + val behavior = spawnBehavior(stateData, awaitingData) behavior ! CongestionCheckRequest(superiorAgent.ref) @@ -201,18 +211,14 @@ class CongestionDetectionSpec transformerCongestions = false, ), CongestedComponents.empty, + MitigationProgress(), ) val awaitingData: AwaitingData[Congestions] = AwaitingData(Set(inferiorAgent.ref)) // init behavior - val centerGridAgent = spawnWithBuffer( - GridAgent.checkForCongestion( - stateData, - awaitingData, - )(constantData, _) - ) + val centerGridAgent = spawnBehavior(stateData, awaitingData) // we will send the center grid agent a StartStep message to start the detection centerGridAgent ! StartStep @@ -269,18 +275,14 @@ class CongestionDetectionSpec transformerCongestions = false, ), CongestedComponents.empty, + MitigationProgress(), ) val awaitingData: AwaitingData[Congestions] = AwaitingData(Set.empty[ActorRef[GridAgent.Request]]) // init behavior - val superiorGridAgent = spawnWithBuffer( - GridAgent.checkForCongestion( - stateData, - awaitingData, - )(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..5a419fb9a6 --- /dev/null +++ b/src/test/scala/edu/ie3/simona/agent/grid/congestion/mitigation/TransformerTagChangeSpec.scala @@ -0,0 +1,199 @@ +/* + * © 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.mitigations.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 tech.units.indriya.ComparableQuantity + +import javax.measure.quantity.Dimensionless + +class TransformerTagChangeSpec + extends ScalaTestWithActorTestKit + with UnitSpec + with CongestionTestBaseData + with CongestedComponentsTestData + with QuantityMatchers + with GridComponentsMokka { + + protected given puTolerance: ComparableQuantity[Dimensionless] = 1e-3.asPu + + private type AwaitedData = (VoltageRange, Set[TransformerTapping]) + + protected val superiorAgent: TestProbe[GridAgent.Request] = TestProbe( + "superiorAgent" + ) + protected val inferiorAgent: TestProbe[GridAgent.Request] = TestProbe( + "inferiorAgent" + ) + + "The congestion mitigation by transformer tap change" should { + val gridModel = GridModel( + hvGridContainer, + refSystem, + voltageLimits, + startTime, + endTime, + config, + ) + + 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.Request] = testKit.spawn( + Behaviors.withStash[GridAgent.Request](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, + MitigationProgress(), + ) + + // the map is empty, since the inferior grid itself has no inferior grids + val awaitingData = AwaitingData( + Map.empty[ActorRef[GridAgent.Request], Option[AwaitedData]] + ) + + val centerAgent = spawnCenterAgent(stateData, awaitingData) + + centerAgent ! RequestVoltageOptions(superiorAgent.ref, 1) + + val (voltageRange, actualTransformers) = + superiorAgent.expectMessageType[VoltageRangeResponse].value + + voltageRange.deltaPlus should equalWithTolerance(-0.01.asPu) + voltageRange.deltaMinus should equalWithTolerance(-0.01.asPu) + voltageRange.suggestion should equalWithTolerance(-0.011.asPu) + + 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, + MitigationProgress(), + ) + + // 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(0.04.asPu, -0.05.asPu), + Set(mockedMvLvTappingModel), + ), + ) + ) + ) + + val (voltageRange, actualTransformers) = + superiorAgent.expectMessageType[VoltageRangeResponse].value + + voltageRange.deltaPlus should equalWithTolerance(0.04.asPu) + voltageRange.deltaMinus should equalWithTolerance(0.01.asPu) + voltageRange.suggestion should equalWithTolerance(0.026.asPu) + + 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 new file mode 100644 index 0000000000..5bd814aa03 --- /dev/null +++ b/src/test/scala/edu/ie3/simona/model/control/TappingGroupModelSpec.scala @@ -0,0 +1,316 @@ +/* + * © 2024. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.model.control + +import edu.ie3.simona.agent.grid.congestion.VoltageRange +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, +} +import org.apache.pekko.actor.typed.ActorRef + +class TappingGroupModelSpec + extends ScalaTestWithActorTestKit + with UnitSpec + with GridComponentsMokka { + + val voltageTolerance = 1e-3 + + val inferior1: TestProbe[GridAgent.Request] = + TestProbe[GridAgent.Request]("inferior1") + val inferior2: TestProbe[GridAgent.Request] = + TestProbe[GridAgent.Request]("inferior2") + + "TappingGroupModel" should { + + "group transformers correctly" in { + val (transformer3wA, transformer3wB, transformer3wC) = + mockTransformer3wModel() + val transformer1 = mockTransformerModel() + val ref1 = TestProbe[GridAgent.Request]("ref1").ref + val ref2 = TestProbe[GridAgent.Request]("ref2").ref + + val ref3 = TestProbe[GridAgent.Request]("ref3").ref + val transformer3 = mockTransformerModel(hasAutoTap = true) + + val ref4 = TestProbe[GridAgent.Request]("ref4").ref + val transformer4a = mockTransformerModel(hasAutoTap = true) + val transformer4b = mockTransformerModel() + + // grid 1 is connected via a transformer2w and one port of a transformer3w + // grid 2 is connected via one port of a transformer3w + // grid 3 is connected via a transformer2w + // grid 4 is connected via two transformer2ws + val receivedData + : Map[ActorRef[GridAgent.Request], Set[TransformerTapping]] = Map( + ref1 -> Set( + transformer1, + transformer3wB, + ), // connected with both transformer2w and transformer3w + ref2 -> Set(transformer3wC), // connected with a transformer3w + ref3 -> Set(transformer3), // connected with just one transformer model + ref4 -> Set( + transformer4a, + transformer4b, + ), // connected with two transformer2w + ) + + val groups = TappingGroupModel.buildModels( + receivedData, + Set(transformer3wA), + ) + + // explanation for the expected groups: + // since both grid 1 and grid 2 are connected by the same transformer3w they must be tapped by the same voltage delta + // since grid 1 is also connected by transformer 1, both transformer are building a group together + // the group contain the refs for both grids + // + // since grid 3 is only connected by a transformer2w, the group contains only this transformer and one ref + // + // since grid 4 is connected by two transformer2w, the group contains both transformers and the ref of grid 4 + groups shouldBe Set( + TappingGroupModel( + Set(transformer1, transformer3wA), + Set(ref1, ref2), + hasAutoTap = false, + ), + TappingGroupModel(Set(transformer3), Set(ref3), hasAutoTap = true), + TappingGroupModel( + Set(transformer4a, transformer4b), + Set(ref4), + hasAutoTap = false, + ), + ) + } + + "calculate the tap and voltage change for one transformer" in { + val tappingModel = dummyTappingModel() + val tapping = dummyTransformerModel(tappingModel) + + val group = TappingGroupModel( + Set(tapping), + Set.empty, + ) + + val cases = Table( + ("range", "expectedTap", "expectedDelta"), + (VoltageRange(0.025.asPu, 0.015.asPu, 0.02.asPu), -1, 0.015.asPu), + ( + VoltageRange(-0.015.asPu, -0.025.asPu, -0.02.asPu), + 1, + -0.015.asPu, + ), + (VoltageRange(0.041.asPu, 0.021.asPu, 0.031.asPu), -2, 0.03.asPu), + (VoltageRange(0.05.asPu, 0.03.asPu, 0.05.asPu), -3, 0.045.asPu), + ( + VoltageRange(0.asPu, -0.2.asPu, -0.1.asPu), + 4, + -0.06.asPu, + ), // max tap increase + ( + VoltageRange(0.2.asPu, 0.asPu, 0.1.asPu), + -6, + 0.09.asPu, + ), // max tap decrease + ( + VoltageRange(0.015.asPu, 0.03.asPu, 0.15.asPu), + -1, + 0.015.asPu, + ), + ( + VoltageRange(-0.04.asPu, -0.03.asPu, -0.03.asPu), + 2, + -0.03.asPu, + ), + ) + + forAll(cases) { (range, expectedTap, expectedDelta) => + val (actualTap, actualDelta) = + group.calculateTapAndVoltage(range) + + actualTap shouldBe Map(tapping -> expectedTap) + actualDelta should equalWithTolerance(expectedDelta) + } + } + + "calculate the tap and voltage change for multiple transformers" in { + val tappingModel1 = dummyTappingModel() + val tappingModel2 = dummyTappingModel( + deltaV = 1.2.asPercent, + tapMin = -3, + currentTapPos = 0, + ) + + val transformer11: TransformerTapping = + dummyTransformerModel(tappingModel1) + val transformer12: TransformerTapping = + dummyTransformerModel(tappingModel1) + + val transformer21: TransformerTapping = + dummyTransformerModel(tappingModel2) + val transformer22: TransformerTapping = + dummyTransformer3wModel(tappingModel2) + + val transformer31: TransformerTapping = + dummyTransformerModel(tappingModel1) + val transformer32: TransformerTapping = + dummyTransformer3wModel(tappingModel2) + + val modelCase1 = Set(transformer11, transformer12) + val modelCase2 = Set(transformer21, transformer22) + val modelCase3 = Set(transformer31, transformer32) + + val cases = Table( + ("suggestion", "models", "expectedTaps", "expectedDelta"), + ( + VoltageRange(0.1.asPu, -0.1.asPu, 0.02.asPu), + modelCase1, + Map(transformer11 -> -1, transformer12 -> -1), + 0.015.asPu, + ), + ( + VoltageRange(0.1.asPu, -0.1.asPu, 0.038.asPu), + modelCase1, + Map(transformer11 -> -3, transformer12 -> -3), + 0.045.asPu, + ), + ( + VoltageRange(0.1.asPu, -0.1.asPu, -0.06.asPu), + modelCase1, + Map(transformer11 -> 4, transformer12 -> 4), + -0.06.asPu, + ), + ( + VoltageRange(0.1.asPu, -0.1.asPu, 0.02.asPu), + modelCase2, + Map(transformer21 -> -2, transformer22 -> -2), + 0.024.asPu, + ), + ( + VoltageRange(0.1.asPu, -0.1.asPu, 0.038.asPu), + modelCase2, + Map(transformer21 -> -3, transformer22 -> -3), + 0.036.asPu, + ), + ( + VoltageRange(0.1.asPu, -0.1.asPu, -0.06.asPu), + modelCase2, + Map(transformer21 -> 5, transformer22 -> 5), + -0.06.asPu, + ), + ( + VoltageRange(0.1.asPu, -0.1.asPu, 0.02.asPu), + modelCase3, + Map(transformer31 -> 0, transformer32 -> 0), + 0.asPu, + ), + ( + VoltageRange(0.1.asPu, -0.1.asPu, 0.038.asPu), + modelCase3, + Map(transformer31 -> 0, transformer32 -> 0), + 0.asPu, + ), + ( + VoltageRange(0.1.asPu, -0.1.asPu, -0.06.asPu), + modelCase3, + Map(transformer31 -> 4, transformer32 -> 5), + -0.06.asPu, + ), + ( + VoltageRange(0.015.asPu, 0.05.asPu, 0.015.asPu), + modelCase1, + Map(transformer11 -> -1, transformer12 -> -1), + 0.015.asPu, + ), + ( + VoltageRange(-0.05.asPu, -0.03.asPu, -0.03.asPu), + modelCase1, + Map(transformer11 -> 2, transformer12 -> 2), + -0.03.asPu, + ), + ) + + forAll(cases) { (range, models, expectedTaps, expectedDelta) => + val group = TappingGroupModel( + models, + Set.empty, + ) + + val (tapChanges, delta) = group.calculateTapAndVoltage(range) + + tapChanges shouldBe expectedTaps + delta should equalWithTolerance(expectedDelta) + } + + } + + "calculate the common delta correctly" in { + val dummyGroup = + TappingGroupModel(Set.empty, Set.empty, hasAutoTap = false) + + val cases = Table( + ("suggestion", "possibleDeltas", "expected"), + (0.015.asPu, Set(List(0.03.asPu, 0.015.asPu, 0.asPu)), 0.015.asPu), + ( + 0.012.asPu, + Set(List(0.03.asPu, 0.02.asPu, 0.01.asPu, 0.asPu)), + 0.01.asPu, + ), + (0.006.asPu, Set(List(0.03.asPu, 0.015.asPu, 0.asPu)), 0.asPu), + ( + 0.03.asPu, + Set( + List(0.06.asPu, 0.03.asPu, 0.asPu), + List(0.045.asPu, 0.03.asPu, 0.015.asPu, 0.asPu), + ), + 0.03.asPu, + ), + ( + 0.03.asPu, + Set(List(0.06.asPu, 0.03.asPu), List(0.03.asPu, 0.015.asPu)), + 0.03.asPu, + ), + ( + 0.035.asPu, + Set( + List(0.06.asPu, 0.03.asPu, 0.asPu), + List(0.045.asPu, 0.03.asPu, 0.015.asPu, 0.asPu), + ), + 0.03.asPu, + ), + ( + 0.02.asPu, + Set(List(0.06.asPu, 0.03.asPu), List(0.03.asPu, 0.015.asPu)), + 0.03.asPu, + ), + ( + 0.06.asPu, + Set(List(0.06.asPu, 0.03.asPu), List(0.03.asPu, 0.015.asPu)), + 0.asPu, + ), + ( + -0.02.asPu, + Set(List(0.06.asPu, 0.03.asPu), List(0.03.asPu, 0.015.asPu)), + 0.asPu, + ), + ) + + forAll(cases) { (suggestion, possibleDeltas, expected) => + val delta = dummyGroup.findCommonDelta(suggestion, possibleDeltas) + + delta should equalWithTolerance(expected) + } + } + + } + +} diff --git a/src/test/scala/edu/ie3/simona/model/grid/Transformer3wModelSpec.scala b/src/test/scala/edu/ie3/simona/model/grid/Transformer3wModelSpec.scala index 532c7d26c6..b4ea5172a8 100644 --- a/src/test/scala/edu/ie3/simona/model/grid/Transformer3wModelSpec.scala +++ b/src/test/scala/edu/ie3/simona/model/grid/Transformer3wModelSpec.scala @@ -492,7 +492,8 @@ class Transformer3wModelSpec val deadBand = Quantities.getQuantity(deadBandVal, PU) transformerModel.updateTapPos(currentTapPos) - val actual = transformerModel.computeDeltaTap(vChange, deadBand) + val actual = + transformerModel.computeDeltaTap(vChange, deadBand = deadBand) actual should be(expected) } } diff --git a/src/test/scala/edu/ie3/simona/model/grid/TransformerTappingSpec.scala b/src/test/scala/edu/ie3/simona/model/grid/TransformerTappingSpec.scala new file mode 100644 index 0000000000..091545f43f --- /dev/null +++ b/src/test/scala/edu/ie3/simona/model/grid/TransformerTappingSpec.scala @@ -0,0 +1,61 @@ +/* + * © 2024. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.model.grid + +import edu.ie3.datamodel.models.input.connector.ConnectorPort +import edu.ie3.simona.test.common.UnitSpec +import edu.ie3.util.quantities.QuantityUtils.{asPercent, asPu} + +class TransformerTappingSpec extends UnitSpec { + + "A TransformerTapping" should { + + val dummyTransformer = DummyTransformer( + new TransformerTappingModel( + 1.0.asPercent, + 0, + 5, + -5, + 0, + true, + ConnectorPort.A, + ) + ) + + "return all possible voltage changes correctly" in { + + val cases = Table( + ("increase", "decrease", "expectedChanges"), + ( + 0.03.asPu, + -0.01.asPu, + List(-0.01.asPu, 0.asPu, 0.01.asPu, 0.02.asPu, 0.03.asPu), + ), + (0.03.asPu, 0.01.asPu, List(0.01.asPu, 0.02.asPu, 0.03.asPu)), + ( + -0.01.asPu, + -0.03.asPu, + List(-0.03.asPu, -0.02.asPu, -0.01.asPu), + ), + (0.01.asPu, 0.02.asPu, List(0.01.asPu)), + (-0.02.asPu, -0.01.asPu, List(-0.01.asPu)), + ) + + forAll(cases) { (increase, decrease, expectedChanges) => + val actual = + dummyTransformer.getPossibleVoltageChanges(increase, decrease) + + actual shouldBe expectedChanges + } + } + } + + private final case class DummyTransformer( + override protected val transformerTappingModel: TransformerTappingModel + ) extends TransformerTapping {} + +} 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 5d6babf38c..3e25d6d750 100644 --- a/src/test/scala/edu/ie3/simona/test/common/ConfigTestData.scala +++ b/src/test/scala/edu/ie3/simona/test/common/ConfigTestData.scala @@ -9,6 +9,9 @@ package edu.ie3.simona.test.common import org.apache.pekko.actor.ActorRef 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 @@ -148,5 +151,13 @@ 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) + protected val listener: Iterable[ActorRef] = Iterable.empty[ActorRef] } diff --git a/src/test/scala/edu/ie3/simona/test/common/model/grid/GridComponentsMokka.scala b/src/test/scala/edu/ie3/simona/test/common/model/grid/GridComponentsMokka.scala new file mode 100644 index 0000000000..c8c8bca26b --- /dev/null +++ b/src/test/scala/edu/ie3/simona/test/common/model/grid/GridComponentsMokka.scala @@ -0,0 +1,169 @@ +/* + * © 2024. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.test.common.model.grid + +import edu.ie3.datamodel.models.input.connector.ConnectorPort +import edu.ie3.simona.model.grid.* +import edu.ie3.simona.model.grid.Transformer3wPowerFlowCase.* +import edu.ie3.util.quantities.QuantityUtils.asPercent +import edu.ie3.util.scala.OperationInterval +import edu.ie3.util.scala.quantities.Voltamperes +import org.mockito.Mockito.when +import org.scalatestplus.mockito.MockitoSugar +import squants.{Amperes, Each} +import tech.units.indriya.ComparableQuantity + +import java.util.UUID +import javax.measure.quantity.Dimensionless + +/** Hold my cup of coffee and let me mock you some models. + */ +trait GridComponentsMokka extends MockitoSugar { + + protected def nodeModel( + uuid: UUID = UUID.randomUUID(), + subnetNo: Int = 1, + ): NodeModel = { + val node = mock[NodeModel] + when(node.uuid).thenReturn(uuid) + when(node.subnet).thenReturn(subnetNo) + node + } + + protected def lineModel( + nodeA: UUID, + nodeB: UUID, + iNom: Double = 10.0, + uuid: UUID = UUID.randomUUID(), + ): LineModel = { + val line = mock[LineModel] + when(line.uuid).thenReturn(uuid) + when(line.nodeAUuid).thenReturn(nodeA) + when(line.nodeBUuid).thenReturn(nodeB) + when(line.iNom).thenReturn(Amperes(iNom)) + + line + } + + protected def dummyTappingModel( + deltaV: ComparableQuantity[Dimensionless] = 1.5.asPercent, + currentTapPos: Int = 1, + tapMax: Int = 5, + tapMin: Int = -5, + tapNeutr: Int = 0, + autoTap: Boolean = true, + tapSide: ConnectorPort = ConnectorPort.A, + ): TransformerTappingModel = + TransformerTappingModel( + deltaV, + currentTapPos, + tapMax, + tapMin, + tapNeutr, + autoTap, + tapSide, + ) + + protected def dummyTransformerModel( + tappingModel: TransformerTappingModel + ): TransformerModel = + TransformerModel( + UUID.randomUUID(), + id = "dummy", + operationInterval = OperationInterval(0L, 1L), + hvNodeUuid = UUID.randomUUID(), + lvNodeUuid = UUID.randomUUID(), + tappingModel, + amount = 1, + voltRatioNominal = BigDecimal(110), + iNomHv = Amperes(1), + iNomLv = Amperes(10), + sRated = Voltamperes(1), + r = Each(1), + x = Each(1), + g = Each(1), + b = Each(1), + ) + + protected def dummyTransformer3wModel( + tappingModel: TransformerTappingModel + ): Transformer3wModel = + Transformer3wModel( + UUID.randomUUID(), + id = "dummy", + operationInterval = OperationInterval(0L, 1L), + hvNodeUuid = UUID.randomUUID(), + mvNodeUuid = UUID.randomUUID(), + lvNodeUuid = UUID.randomUUID(), + nodeInternalUuid = UUID.randomUUID(), + voltRatioNominal = BigDecimal(110), + tappingModel, + amount = 1, + powerFlowCase = PowerFlowCaseA, + sRated = Voltamperes(1), + r = Each(1), + x = Each(1), + g = Each(1), + b = Each(1), + ) + + protected def mockTransformerTapping( + uuid: UUID = UUID.randomUUID(), + autoTap: Boolean = false, + tapMax: Int = 5, + tapMin: Int = -5, + currentTapPos: Int = 0, + deltaV: ComparableQuantity[Dimensionless] = 1d.asPercent, + ): TransformerModel = { + val transformer = mock[TransformerModel] + when(transformer.uuid).thenReturn(uuid) + + when(transformer.hasAutoTap).thenReturn(autoTap) + when(transformer.tapMax).thenReturn(tapMax) + when(transformer.tapMin).thenReturn(tapMin) + when(transformer.currentTapPos).thenReturn(currentTapPos) + when(transformer.deltaV).thenReturn(deltaV) + + transformer + } + + protected def mockTransformerModel( + uuid: UUID = UUID.randomUUID(), + hasAutoTap: Boolean = false, + ): TransformerModel = { + val transformer = mock[TransformerModel] + when(transformer.uuid).thenReturn(uuid) + when(transformer.hasAutoTap).thenReturn(hasAutoTap) + + transformer + } + + protected def mockTransformer3wModel( + uuid: UUID = UUID.randomUUID(), + hasAutoTap: Boolean = false, + ): (Transformer3wModel, Transformer3wModel, Transformer3wModel) = { + val transformerA = mock[Transformer3wModel] + val transformerB = mock[Transformer3wModel] + val transformerC = mock[Transformer3wModel] + when(transformerA.uuid).thenReturn(uuid) + when(transformerB.uuid).thenReturn(uuid) + when(transformerC.uuid).thenReturn(uuid) + + when(transformerA.hasAutoTap).thenReturn(hasAutoTap) + when(transformerB.hasAutoTap).thenReturn(hasAutoTap) + when(transformerC.hasAutoTap).thenReturn(hasAutoTap) + + when(transformerA.powerFlowCase).thenReturn(PowerFlowCaseA) + when(transformerB.powerFlowCase).thenReturn( + Transformer3wPowerFlowCase.PowerFlowCaseB + ) + when(transformerC.powerFlowCase).thenReturn(PowerFlowCaseC) + + (transformerA, transformerB, transformerC) + } + +} 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 386707d2fc..5324d1109f 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,12 +34,6 @@ 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( @@ -189,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, diff --git a/src/test/scala/edu/ie3/simona/test/common/result/ResultMokka.scala b/src/test/scala/edu/ie3/simona/test/common/result/ResultMokka.scala new file mode 100644 index 0000000000..6ae635f080 --- /dev/null +++ b/src/test/scala/edu/ie3/simona/test/common/result/ResultMokka.scala @@ -0,0 +1,42 @@ +/* + * © 2024. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.test.common.result + +import edu.ie3.datamodel.models.result.NodeResult +import edu.ie3.datamodel.models.result.connector.LineResult +import org.mockito.Mockito.when +import org.scalatestplus.mockito.MockitoSugar +import tech.units.indriya.ComparableQuantity + +import java.util.UUID +import javax.measure.quantity.{Dimensionless, ElectricCurrent} + +trait ResultMokka extends MockitoSugar { + + protected def mockNodeResult( + uuid: UUID, + vMag: ComparableQuantity[Dimensionless], + ): NodeResult = { + val result = mock[NodeResult] + when(result.getInputModel).thenReturn(uuid) + when(result.getvMag()).thenReturn(vMag) + + result + } + + protected def mockLineResult( + uuid: UUID, + iAMag: ComparableQuantity[ElectricCurrent], + iBMag: ComparableQuantity[ElectricCurrent], + ): LineResult = { + val result = mock[LineResult] + when(result.getInputModel).thenReturn(uuid) + when(result.getiAMag()).thenReturn(iAMag) + when(result.getiBMag()).thenReturn(iBMag) + result + } +} From 76375017a1055acdf47d22cae62a017a9fad0750 Mon Sep 17 00:00:00 2001 From: staudtMarius Date: Wed, 11 Jun 2025 17:28:27 +0200 Subject: [PATCH 2/7] Adapting to recent changes in `dev`. --- .../congestion/CongestedComponentsSpec.scala | 22 ++++++++------- .../congestion/CongestionTestBaseData.scala | 27 ++++++++++++------- .../grid/congestion/DCMAlgorithmSpec.scala | 7 ++++- .../data/CongestionManagementDataSpec.scala | 10 ++++--- .../mitigation/TransformerTagChangeSpec.scala | 9 ------- .../result/CongestedComponentsTestData.scala | 2 +- 6 files changed, 43 insertions(+), 34 deletions(-) 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 f8042b23e7..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 @@ -10,12 +10,9 @@ import edu.ie3.simona.agent.grid.congestion.CongestedComponents import edu.ie3.simona.event.ResultEvent.PowerFlowResultEvent import edu.ie3.simona.test.common.UnitSpec import edu.ie3.simona.test.common.result.CongestedComponentsTestData -import edu.ie3.util.TimeUtil import edu.ie3.util.scala.quantities.{ApparentPower, Kilovoltamperes} import squants.electro.{Amperes, Kilovolts} -import java.time.ZonedDateTime - class CongestedComponentsSpec extends UnitSpec with CongestedComponentsTestData { @@ -42,7 +39,7 @@ class CongestedComponentsSpec val congested = CongestedComponents( results, - gridModel.gridComponents, + extendedGridModel.gridComponents, defaultVoltageLimits, Kilovolts(110), 1, @@ -65,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( @@ -92,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 @@ -123,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/CongestionTestBaseData.scala b/src/test/scala/edu/ie3/simona/agent/grid/congestion/CongestionTestBaseData.scala index 8e7edb7fe4..d50a09da2e 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 @@ -92,6 +92,15 @@ trait CongestionTestBaseData "gridAgentActivation" ) + protected val gridModel: GridModel = GridModel( + hvGridContainer, + refSystem, + voltageLimits, + startTime, + endTime, + config, + ) + protected given constantData: GridAgentConstantData = GridAgentConstantData( environmentRefs, @@ -138,19 +147,19 @@ 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) - if (gridModel.nonEmpty) { - val gridEnv = mock[GridEnvironment] - when(gridEnv.gridModel).thenReturn( - gridModel.getOrElse( - throw new CriticalFailureException("No grid model found!") - ) - ) + gridModel match { + case Some(model) => + when(gridEnv.gridModel).thenReturn(model) + + case None => + val gridModelMock = mock[GridModel] + when(gridEnv.gridModel).thenReturn(gridModelMock) - when(data.gridEnv).thenReturn(gridEnv) + 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 57fbdb051f..7831e6d946 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.mitigations.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.Request]("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 @@ -75,6 +79,7 @@ class DCMAlgorithmSpec transformerCongestions = false, ), CongestedComponents.empty, + MitigationProgress(currentIteration = 1), ) // this will return the idle behavior of the grid agent 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 b6f011423b..6952e1a294 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 @@ -6,10 +6,10 @@ package edu.ie3.simona.agent.grid.congestion.data -import edu.ie3.datamodel.models.result.CongestionResult import edu.ie3.datamodel.models.result.CongestionResult.InputModelType import edu.ie3.simona.agent.grid.GridAgentData.GridAgentBaseData import edu.ie3.simona.agent.grid.GridEnvironment +import edu.ie3.simona.agent.grid.congestion.mitigations.MitigationProgress import edu.ie3.simona.agent.grid.congestion.{CongestedComponents, Congestions} import edu.ie3.simona.event.ResultEvent.PowerFlowResultEvent import edu.ie3.simona.test.common.UnitSpec @@ -27,7 +27,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) @@ -36,9 +36,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( @@ -63,6 +64,7 @@ class CongestionManagementDataSpec ), Iterable.empty, ), + MitigationProgress(currentIteration = 1), ) val results = stateData 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 index 5a419fb9a6..f277f84328 100644 --- 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 @@ -60,15 +60,6 @@ class TransformerTagChangeSpec ) "The congestion mitigation by transformer tap change" should { - val gridModel = GridModel( - hvGridContainer, - refSystem, - voltageLimits, - startTime, - endTime, - config, - ) - val transformers: Set[TransformerTapping] = gridModel.gridComponents.transformers.map { (transformerTapping: TransformerTapping) => transformerTapping 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 96328bd662..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 @@ -74,7 +74,7 @@ trait CongestedComponentsTestData false, ) - protected val gridModel: GridModel = { + protected val extendedGridModel: GridModel = { val refSystem = RefSystem(Megawatts(600), Kilovolts(110)) val model = GridModel( From 1796e47aae7e319df9944c2e7b846d72b2de6bd8 Mon Sep 17 00:00:00 2001 From: staudtMarius Date: Wed, 11 Jun 2025 17:55:30 +0200 Subject: [PATCH 3/7] Adapting to recent changes in `dev`. --- .../agent/grid/congestion/DCMAlgorithm.scala | 17 +++++----- .../agent/grid/congestion/VoltageRange.scala | 4 +-- .../mitigations/TappingMessages.scala | 6 ++-- .../mitigations/TransformerTapChange.scala | 26 ++++++++-------- .../model/control/TappingGroupModel.scala | 10 +++--- .../grid/TransformerTappingSupportSpec.scala | 31 +++---------------- .../grid/congestion/VoltageRangeSpec.scala | 8 ++--- .../detection/CongestionDetectionSpec.scala | 4 +-- .../mitigation/TransformerTagChangeSpec.scala | 10 +++--- .../model/control/TappingGroupModelSpec.scala | 18 +++++------ 10 files changed, 58 insertions(+), 76 deletions(-) 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 a32a385496..9b1e1b550b 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.grid.congestion.mitigations.{ + MitigationSteps, + TransformerTapChange, +} import edu.ie3.simona.event.ResultEvent.PowerFlowResultEvent import org.apache.pekko.actor.typed.Behavior import org.apache.pekko.actor.typed.scaladsl.{ActorContext, StashBuffer} @@ -72,11 +75,11 @@ trait DCMAlgorithm extends CongestionDetection with TransformerTapChange { private[grid] def doCongestionMitigation( stateData: CongestionManagementData, - ctx: ActorContext[Request], + ctx: ActorContext[Message], )(using constantData: GridAgentConstantData, - buffer: StashBuffer[Request], - ): Behavior[Request] = { + buffer: StashBuffer[Message], + ): Behavior[Message] = { // first we find an option for the next mitigation step val (stepOption, updatedProgress) = stateData.mitigationProgress.getNextStepsAndUpdate @@ -85,16 +88,16 @@ trait DCMAlgorithm extends CongestionDetection with TransformerTapChange { val updatedStateData = stateData.copy(mitigationProgress = updatedProgress) stepOption match { - case Some(transformerTapChange: TransformerTapChange) => + case Some(MitigationSteps.TransformerTapChange) => GridAgent.updateTransformerTapping( - stateData, + updatedStateData, AwaitingData(stateData.inferiorGridRefs.keySet), ) case _ => // we have no more mitigation steps // we finish the mitigation - finishCongestionManagement(stateData, ctx) + finishCongestionManagement(updatedStateData, ctx) } } diff --git a/src/main/scala/edu/ie3/simona/agent/grid/congestion/VoltageRange.scala b/src/main/scala/edu/ie3/simona/agent/grid/congestion/VoltageRange.scala index ea22c2b70d..76cc92a26f 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/congestion/VoltageRange.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/congestion/VoltageRange.scala @@ -83,7 +83,7 @@ final case class VoltageRange( */ def updateWithInferiorRanges( inferiorData: Map[ActorRef[ - GridAgent.Request + GridAgent.Message ], (VoltageRange, Set[TransformerTapping])] ): VoltageRange = { inferiorData.foldLeft(this) { case (range, (_, (infRange, tappings))) => @@ -189,7 +189,7 @@ object VoltageRange { voltageLimits: VoltageLimits, gridComponents: GridComponents, inferiorData: Map[ActorRef[ - GridAgent.Request + GridAgent.Message ], (VoltageRange, Set[TransformerTapping])], subnetNo: Int, ): VoltageRange = { 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 index 7e2121faea..0ee723f137 100644 --- 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 @@ -28,7 +28,7 @@ object TappingMessages { * Subgrid of the sender. */ final case class RequestVoltageOptions( - sender: ActorRef[GridAgent.Request], + sender: ActorRef[GridAgent.Message], subgrid: Int, ) extends InternalRequest @@ -40,7 +40,7 @@ object TappingMessages { * superior grid. */ final case class VoltageRangeResponse( - override val sender: ActorRef[GridAgent.Request], + override val sender: ActorRef[GridAgent.Message], override val value: (VoltageRange, Set[TransformerTapping]), ) extends InternalReplyWithSender[(VoltageRange, Set[TransformerTapping])] @@ -51,7 +51,7 @@ object TappingMessages { */ final case class ReceivedVoltageRange( values: Seq[ - (ActorRef[GridAgent.Request], (VoltageRange, Set[TransformerTapping])) + (ActorRef[GridAgent.Message], (VoltageRange, Set[TransformerTapping])) ] ) 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 index 696a194baf..c529298890 100644 --- 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 @@ -56,8 +56,8 @@ trait TransformerTapChange { awaitingData: AwaitingData[(VoltageRange, Set[TransformerTapping])], )(using constantData: GridAgentConstantData, - buffer: StashBuffer[GridAgent.Request], - ): Behavior[GridAgent.Request] = Behaviors.receivePartial { + buffer: StashBuffer[GridAgent.Message], + ): Behavior[GridAgent.Message] = Behaviors.receivePartial { case (ctx, StartStep) => val subnet = stateData.gridAgentBaseData.gridEnv.gridModel.subnetNo @@ -98,11 +98,11 @@ trait TransformerTapChange { stateData: CongestionManagementData, awaitingData: AwaitingData[(VoltageRange, Set[TransformerTapping])], request: RequestVoltageOptions, - ctx: ActorContext[GridAgent.Request], + ctx: ActorContext[GridAgent.Message], )(using constantData: GridAgentConstantData, - buffer: StashBuffer[GridAgent.Request], - ): Behavior[GridAgent.Request] = { + buffer: StashBuffer[GridAgent.Message], + ): Behavior[GridAgent.Message] = { val sender = request.sender val subgrid = request.subgrid @@ -164,13 +164,13 @@ trait TransformerTapChange { stateData: CongestionManagementData, awaitingData: AwaitingData[(VoltageRange, Set[TransformerTapping])], voltageRange: Seq[ - (ActorRef[GridAgent.Request], (VoltageRange, Set[TransformerTapping])) + (ActorRef[GridAgent.Message], (VoltageRange, Set[TransformerTapping])) ], - ctx: ActorContext[GridAgent.Request], + ctx: ActorContext[GridAgent.Message], )(using constantData: GridAgentConstantData, - buffer: StashBuffer[GridAgent.Request], - ): Behavior[GridAgent.Request] = { + buffer: StashBuffer[GridAgent.Message], + ): Behavior[GridAgent.Message] = { // updating the state data with received data from inferior grids val updatedData = awaitingData.handleReceivingData(voltageRange) @@ -190,11 +190,11 @@ trait TransformerTapChange { stateData: CongestionManagementData, awaitingData: AwaitingData[(VoltageRange, Set[TransformerTapping])], delta: ComparableQuantity[Dimensionless], - ctx: ActorContext[GridAgent.Request], + ctx: ActorContext[GridAgent.Message], )(using constantData: GridAgentConstantData, - buffer: StashBuffer[GridAgent.Request], - ): Behavior[GridAgent.Request] = { + 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.debug( @@ -211,7 +211,7 @@ trait TransformerTapChange { } val actorRefToTappingModels - : Map[ActorRef[GridAgent.Request], Set[TransformerTapping]] = + : Map[ActorRef[GridAgent.Message], Set[TransformerTapping]] = receivedData.map { case (ref, (_, tappings)) => ref -> tappings } // groups all tapping models 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 7f3c7a75c7..4f38f886ec 100644 --- a/src/main/scala/edu/ie3/simona/model/control/TappingGroupModel.scala +++ b/src/main/scala/edu/ie3/simona/model/control/TappingGroupModel.scala @@ -31,13 +31,13 @@ import javax.measure.quantity.Dimensionless */ final case class TappingGroupModel( tappings: Set[TransformerTapping], - refs: Set[ActorRef[GridAgent.Request]], + refs: Set[ActorRef[GridAgent.Message]], hasAutoTap: Boolean, ) { def updateTapPositions( delta: ComparableQuantity[Dimensionless], - refMap: Map[ActorRef[GridAgent.Request], VoltageRange], + refMap: Map[ActorRef[GridAgent.Message], VoltageRange], log: Logger, ): ComparableQuantity[Dimensionless] = if (hasAutoTap) { // get all possible voltage ranges of the inferior grids @@ -218,7 +218,7 @@ object TappingGroupModel { def apply( tappings: Set[TransformerTapping], - refs: Set[ActorRef[GridAgent.Request]], + refs: Set[ActorRef[GridAgent.Message]], ): TappingGroupModel = TappingGroupModel( tappings, @@ -254,7 +254,7 @@ object TappingGroupModel { * a set of [[TappingGroupModel]]s */ def buildModels( - receivedData: Map[ActorRef[GridAgent.Request], Set[TransformerTapping]], + receivedData: Map[ActorRef[GridAgent.Message], Set[TransformerTapping]], transformer3ws: Set[Transformer3wModel], ): Set[TappingGroupModel] = { val transformer3wMap = transformer3ws.map(t => t.uuid -> t).toMap @@ -262,7 +262,7 @@ object TappingGroupModel { // builds all groups receivedData .foldLeft( - Map.empty[Set[TransformerTapping], Set[ActorRef[GridAgent.Request]]] + Map.empty[Set[TransformerTapping], Set[ActorRef[GridAgent.Message]]] ) { case (combined, (ref, tappings)) => // get all transformer models val updated: Set[TransformerTapping] = tappings.map { 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 08a708d7ac..4428dafb62 100644 --- a/src/test/scala/edu/ie3/simona/agent/grid/TransformerTappingSupportSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/grid/TransformerTappingSupportSpec.scala @@ -6,23 +6,8 @@ package edu.ie3.simona.agent.grid -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.TransformerTappingSupport.getTappingOptions -import edu.ie3.simona.agent.grid.congestion.VoltageRange.calculatePossibleVoltageDeltaForLines -import edu.ie3.simona.agent.grid.congestion.{Congestions, VoltageRange} -import edu.ie3.simona.event.ResultEvent.PowerFlowResultEvent -import edu.ie3.simona.model.grid.GridModel.GridComponents -import edu.ie3.simona.model.grid.{ - GridModel, - RefSystem, - TransformerTapping, - VoltageLimits, -} +import edu.ie3.simona.model.grid.TransformerTapping import edu.ie3.simona.test.common.model.grid.{ DbfsTestGrid, GridComponentsMokka, @@ -30,17 +15,11 @@ 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.PowerSystemUnits.PU import edu.ie3.util.quantities.QuantityUtils.asPu import org.apache.pekko.actor.testkit.typed.scaladsl.{ ScalaTestWithActorTestKit, TestProbe, } -import squants.electro.Kilovolts -import squants.energy.Kilowatts -import tech.units.indriya.quantity.Quantities - -import java.time.ZonedDateTime class TransformerTappingSupportSpec extends ScalaTestWithActorTestKit @@ -53,10 +32,10 @@ class TransformerTappingSupportSpec val voltageTolerance = 1e-3 - protected val inferior1: TestProbe[GridAgent.Request] = - TestProbe[GridAgent.Request]("inferior1") - protected val inferior2: TestProbe[GridAgent.Request] = - TestProbe[GridAgent.Request]("inferior2") + protected val inferior1: TestProbe[GridAgent.Message] = + TestProbe[GridAgent.Message]("inferior1") + protected val inferior2: TestProbe[GridAgent.Message] = + TestProbe[GridAgent.Message]("inferior2") "TransformerTappingSupportSpec" should { diff --git a/src/test/scala/edu/ie3/simona/agent/grid/congestion/VoltageRangeSpec.scala b/src/test/scala/edu/ie3/simona/agent/grid/congestion/VoltageRangeSpec.scala index 6b34023ae0..45811a112d 100644 --- a/src/test/scala/edu/ie3/simona/agent/grid/congestion/VoltageRangeSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/grid/congestion/VoltageRangeSpec.scala @@ -41,10 +41,10 @@ class VoltageRangeSpec given puTolerance: ComparableQuantity[Dimensionless] = 1e-6.asPu - protected val inferior1: TestProbe[GridAgent.Request] = - TestProbe[GridAgent.Request]("inferior1") - protected val inferior2: TestProbe[GridAgent.Request] = - TestProbe[GridAgent.Request]("inferior2") + protected val inferior1: TestProbe[GridAgent.Message] = + TestProbe[GridAgent.Message]("inferior1") + protected val inferior2: TestProbe[GridAgent.Message] = + TestProbe[GridAgent.Message]("inferior2") "A VoltageRange" should { 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 a7d270e0b5..b1cbc45ff9 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 @@ -55,8 +55,8 @@ class CongestionDetectionSpec stateData: CongestionManagementData, awaitingData: AwaitingData[Congestions], capacity: Int = 10, - ): ActorRef[GridAgent.Request] = testKit.spawn( - Behaviors.withStash[GridAgent.Request](capacity) { buffer => + ): ActorRef[GridAgent.Message] = testKit.spawn( + Behaviors.withStash[GridAgent.Message](capacity) { buffer => GridAgent.checkForCongestion( stateData, awaitingData, 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 index f277f84328..aac820dbf8 100644 --- 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 @@ -52,10 +52,10 @@ class TransformerTagChangeSpec private type AwaitedData = (VoltageRange, Set[TransformerTapping]) - protected val superiorAgent: TestProbe[GridAgent.Request] = TestProbe( + protected val superiorAgent: TestProbe[GridAgent.Message] = TestProbe( "superiorAgent" ) - protected val inferiorAgent: TestProbe[GridAgent.Request] = TestProbe( + protected val inferiorAgent: TestProbe[GridAgent.Message] = TestProbe( "inferiorAgent" ) @@ -69,8 +69,8 @@ class TransformerTagChangeSpec stateData: CongestionManagementData, awaitingData: AwaitingData[(VoltageRange, Set[TransformerTapping])], capacity: Int = 10, - ): ActorRef[GridAgent.Request] = testKit.spawn( - Behaviors.withStash[GridAgent.Request](capacity) { buffer => + ): ActorRef[GridAgent.Message] = testKit.spawn( + Behaviors.withStash[GridAgent.Message](capacity) { buffer => GridAgent.updateTransformerTapping( stateData, awaitingData, @@ -108,7 +108,7 @@ class TransformerTagChangeSpec // the map is empty, since the inferior grid itself has no inferior grids val awaitingData = AwaitingData( - Map.empty[ActorRef[GridAgent.Request], Option[AwaitedData]] + Map.empty[ActorRef[GridAgent.Message], Option[AwaitedData]] ) val centerAgent = spawnCenterAgent(stateData, awaitingData) 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 5bd814aa03..47781f043a 100644 --- a/src/test/scala/edu/ie3/simona/model/control/TappingGroupModelSpec.scala +++ b/src/test/scala/edu/ie3/simona/model/control/TappingGroupModelSpec.scala @@ -25,10 +25,10 @@ class TappingGroupModelSpec val voltageTolerance = 1e-3 - val inferior1: TestProbe[GridAgent.Request] = - TestProbe[GridAgent.Request]("inferior1") - val inferior2: TestProbe[GridAgent.Request] = - TestProbe[GridAgent.Request]("inferior2") + val inferior1: TestProbe[GridAgent.Message] = + TestProbe[GridAgent.Message]("inferior1") + val inferior2: TestProbe[GridAgent.Message] = + TestProbe[GridAgent.Message]("inferior2") "TappingGroupModel" should { @@ -36,13 +36,13 @@ class TappingGroupModelSpec val (transformer3wA, transformer3wB, transformer3wC) = mockTransformer3wModel() val transformer1 = mockTransformerModel() - val ref1 = TestProbe[GridAgent.Request]("ref1").ref - val ref2 = TestProbe[GridAgent.Request]("ref2").ref + val ref1 = TestProbe[GridAgent.Message]("ref1").ref + val ref2 = TestProbe[GridAgent.Message]("ref2").ref - val ref3 = TestProbe[GridAgent.Request]("ref3").ref + val ref3 = TestProbe[GridAgent.Message]("ref3").ref val transformer3 = mockTransformerModel(hasAutoTap = true) - val ref4 = TestProbe[GridAgent.Request]("ref4").ref + val ref4 = TestProbe[GridAgent.Message]("ref4").ref val transformer4a = mockTransformerModel(hasAutoTap = true) val transformer4b = mockTransformerModel() @@ -51,7 +51,7 @@ class TappingGroupModelSpec // grid 3 is connected via a transformer2w // grid 4 is connected via two transformer2ws val receivedData - : Map[ActorRef[GridAgent.Request], Set[TransformerTapping]] = Map( + : Map[ActorRef[GridAgent.Message], Set[TransformerTapping]] = Map( ref1 -> Set( transformer1, transformer3wB, From e2dce335dbe4d9e10c80ef26bbaf4bf40e0d6d44 Mon Sep 17 00:00:00 2001 From: staudtMarius Date: Mon, 30 Jun 2025 11:26:35 +0200 Subject: [PATCH 4/7] Adapting to changes in `dev`. --- .../agent/grid/congestion/CongestionTestBaseData.scala | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) 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 fd30222935..7b42444856 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 @@ -7,9 +7,7 @@ package edu.ie3.simona.agent.grid.congestion import com.typesafe.config.ConfigFactory -import edu.ie3.util.quantities.QuantityUtils.asPu import edu.ie3.simona.agent.EnvironmentRefs -import edu.ie3.simona.agent.grid.{GridAgent, GridEnvironment} import edu.ie3.simona.agent.grid.GridAgentData.{ GridAgentBaseData, GridAgentConstantData, @@ -19,17 +17,13 @@ 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.services.{ - LoadProfileMessage, - ServiceMessage, - WeatherMessage, -} import edu.ie3.simona.ontology.messages.{Activation, SchedulerMessage} import edu.ie3.simona.service.load.LoadProfileService 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, From 926490fd06f5187587b084832fe1717bc1f01d7c Mon Sep 17 00:00:00 2001 From: staudtMarius Date: Wed, 3 Sep 2025 14:06:06 +0200 Subject: [PATCH 5/7] fmt --- .../agent/grid/TransformerTappingSupport.scala | 4 ++-- .../simona/agent/grid/congestion/VoltageRange.scala | 10 +++++----- .../congestion/detection/CongestionDetection.scala | 2 +- .../mitigations/TransformerTapChange.scala | 6 +++--- .../ie3/simona/model/control/TappingGroupModel.scala | 12 ++++++------ .../ie3/simona/model/grid/TransformerTapping.scala | 6 +++--- 6 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/agent/grid/TransformerTappingSupport.scala b/src/main/scala/edu/ie3/simona/agent/grid/TransformerTappingSupport.scala index ff0ffb920f..fe8f44a288 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/TransformerTappingSupport.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/TransformerTappingSupport.scala @@ -26,7 +26,7 @@ object TransformerTappingSupport { ComparableQuantity[Dimensionless], ) = { // allow tapping only if all transformers support tapping - if (tappings.forall(_.hasAutoTap)) { + if tappings.forall(_.hasAutoTap) then { val tappingRanges = tappings.map { tapping => val currentPos = tapping.currentTapPos @@ -37,7 +37,7 @@ object TransformerTappingSupport { (increase, decrease) }.toSeq - if (tappings.size == 1) { + if tappings.size == 1 then { tappingRanges(0) } else { // check for possible increase and decrease that can be applied to all transformers diff --git a/src/main/scala/edu/ie3/simona/agent/grid/congestion/VoltageRange.scala b/src/main/scala/edu/ie3/simona/agent/grid/congestion/VoltageRange.scala index 76cc92a26f..e7f368c091 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/congestion/VoltageRange.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/congestion/VoltageRange.scala @@ -127,7 +127,7 @@ object VoltageRange { val plus = deltaPlus.getValue.doubleValue() val minus = deltaMinus.getValue.doubleValue() - val value = if (plus > minus) { + val value = if plus > minus then { // we could have a voltage violation of one limit (plus + minus) / 2 } else { @@ -148,14 +148,14 @@ object VoltageRange { val factor = 1e3 - val suggestion = if (value < 0) { + val suggestion = if value < 0 then { (value * factor).floor / factor } else { (value * factor).ceil / factor } // check if tapping is required - if (plus < 0 || minus > 0) { + if plus < 0 || minus > 0 then { VoltageRange( deltaPlus, deltaMinus, @@ -223,7 +223,7 @@ object VoltageRange { ) val updatedRange = range.updateWithLineDelta(deltaV) - if (inferiorData.isEmpty) { + if inferiorData.isEmpty then { // if there are no inferior grids, return the voltage range updatedRange } else { @@ -261,7 +261,7 @@ object VoltageRange { // calculate the voltage and the current change at the end of the line that sees the highest current val (voltage, deltaI) = - if (res.getiAMag().isGreaterThan(res.getiBMag())) { + if res.getiAMag().isGreaterThan(res.getiBMag()) then { ( nodeResults(line.nodeAUuid).toSquants, line.iNom - res.getiAMag().toSquants, 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 e147f15b55..4f39632b30 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 @@ -157,7 +157,7 @@ trait CongestionDetection { val updatedStateData = stateData.copy(congestions = updatedCongestions) - if (stateData.congestionManagementParams.anyMitigationEnabled) { + if stateData.congestionManagementParams.anyMitigationEnabled then { // the mitigation is enabled // goto mitigation behavior GridAgent.doCongestionMitigation(updatedStateData, ctx) 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 index c529298890..31b081f66a 100644 --- 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 @@ -107,7 +107,7 @@ trait TransformerTapChange { val subgrid = request.subgrid // check if waiting for inferior data is needed - if (awaitingData.notDone) { + if awaitingData.notDone then { ctx.log.debug( s"Received request for voltage range before all data from inferior grids were received. Stashing away." ) @@ -174,7 +174,7 @@ trait TransformerTapChange { // updating the state data with received data from inferior grids val updatedData = awaitingData.handleReceivingData(voltageRange) - if (stateData.gridAgentBaseData.isSuperior) { + if stateData.gridAgentBaseData.isSuperior then { // there should be no voltage change in the superior grid, // because the slack grid should always have 1 pu @@ -201,7 +201,7 @@ trait TransformerTapChange { s"Grid ${stateData.gridAgentBaseData.gridEnv.gridModel.subnetNo}, received delta: $delta" ) - if (stateData.inferiorGridRefs.nonEmpty) { + if stateData.inferiorGridRefs.nonEmpty then { // we calculate a voltage delta for all inferior grids val receivedData = awaitingData.mappedValues 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 4f38f886ec..d480b2b9b3 100644 --- a/src/main/scala/edu/ie3/simona/model/control/TappingGroupModel.scala +++ b/src/main/scala/edu/ie3/simona/model/control/TappingGroupModel.scala @@ -39,7 +39,7 @@ final case class TappingGroupModel( delta: ComparableQuantity[Dimensionless], refMap: Map[ActorRef[GridAgent.Message], VoltageRange], log: Logger, - ): ComparableQuantity[Dimensionless] = if (hasAutoTap) { + ): ComparableQuantity[Dimensionless] = if hasAutoTap then { // get all possible voltage ranges of the inferior grids val inferiorRanges = refs.map(refMap) @@ -84,12 +84,12 @@ final case class TappingGroupModel( val noTapping = (tappings.map(t => t -> 0).toMap, 0.asPu) val suggestion = range.suggestion - if (suggestion.isEquivalentTo(0.asPu)) { + if suggestion.isEquivalentTo(0.asPu) then { return noTapping } // calculate a tap option for each transformer - if (tappings.forall(_.hasAutoTap)) { + if tappings.forall(_.hasAutoTap) then { // get all possible deltas val possibleDeltas = tappings @@ -102,7 +102,7 @@ final case class TappingGroupModel( ) // calculates a voltage change option - val deltaOption = if (possibleDeltas.exists(_.isEmpty)) { + val deltaOption = if possibleDeltas.exists(_.isEmpty) then { // there is a transformer that cannot be tapped None } else { @@ -171,7 +171,7 @@ final case class TappingGroupModel( Math.abs(suggestion.subtract(max).getValue.doubleValue()) // find the difference that is smaller - if (minDiff < maxDiff) { + if minDiff < maxDiff then { min } else max @@ -195,7 +195,7 @@ final case class TappingGroupModel( possibleDeltas: Set[List[ComparableQuantity[Dimensionless]]], ): Seq[ComparableQuantity[Dimensionless]] = possibleDeltas.toSeq.flatMap { deltas => - if (deltas.exists(_.isEquivalentTo(suggestion))) { + if deltas.exists(_.isEquivalentTo(suggestion)) then { List(suggestion) } else { val minOption = diff --git a/src/main/scala/edu/ie3/simona/model/grid/TransformerTapping.scala b/src/main/scala/edu/ie3/simona/model/grid/TransformerTapping.scala index a8b0dd4aa1..f1c50ecff9 100644 --- a/src/main/scala/edu/ie3/simona/model/grid/TransformerTapping.scala +++ b/src/main/scala/edu/ie3/simona/model/grid/TransformerTapping.scala @@ -106,7 +106,7 @@ trait TransformerTapping { tapSide: ConnectorPort = ConnectorPort.A, deadBand: Quantity[Dimensionless] = Quantities.getQuantity(0.75, PU), ): Int = { - if (isSameSide(tapSide)) { + if isSameSide(tapSide) then { transformerTappingModel.computeDeltaTap(vChangeRequest, deadBand) } else { transformerTappingModel.computeDeltaTap( @@ -139,7 +139,7 @@ trait TransformerTapping { val range = Range.inclusive(minus, plus).map(deltaV.multiply(_).divide(100)).toList - val values = if (isSameSide(tapSide)) { + val values = if isSameSide(tapSide) then { range } else { range.map(_.multiply(-1)).sortBy(_.getValue.doubleValue()) @@ -194,7 +194,7 @@ trait TransformerTapping { val deltaV = transformerTappingModel.deltaV.to(PU).getValue.doubleValue() * taps - if (isSameSide(tapSide)) { + if isSameSide(tapSide) then { (taps, deltaV.asPu) } else { (taps, deltaV.asPu.multiply(-1)) From f6d644b2093b1b6c96bfaa6f717d4dad0bfc5833 Mon Sep 17 00:00:00 2001 From: staudtMarius Date: Wed, 3 Sep 2025 14:43:28 +0200 Subject: [PATCH 6/7] Fixing some issues. --- .../ie3/simona/agent/grid/DBFSAlgorithm.scala | 4 ++ .../CongestionManagementMessages.scala | 4 ++ .../CongestionManagementParams.scala | 34 +++++++++++++ .../agent/grid/congestion/DCMAlgorithm.scala | 51 ++++++++++--------- .../agent/grid/congestion/VoltageRange.scala | 5 +- .../data/CongestionManagementData.scala | 39 +++++++++++--- .../detection/CongestionDetection.scala | 46 +++++++++++++---- .../mitigations/MitigationProgress.scala | 48 ----------------- .../mitigations/MitigationSteps.scala | 3 +- .../mitigations/TransformerTapChange.scala | 6 +-- .../model/control/TappingGroupModel.scala | 5 +- .../grid/congestion/DCMAlgorithmSpec.scala | 3 +- .../data/CongestionManagementDataSpec.scala | 2 - .../detection/CongestionDetectionSpec.scala | 6 +-- .../mitigation/TransformerTagChangeSpec.scala | 4 +- 15 files changed, 149 insertions(+), 111 deletions(-) delete mode 100644 src/main/scala/edu/ie3/simona/agent/grid/congestion/mitigations/MitigationProgress.scala 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/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 7be91fef00..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,6 +6,10 @@ 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 @@ -25,8 +29,38 @@ 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 9b1e1b550b..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,11 +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.{ - MitigationSteps, - TransformerTapChange, -} +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} @@ -73,33 +72,37 @@ trait DCMAlgorithm extends CongestionDetection with TransformerTapChange { ) } - private[grid] def doCongestionMitigation( + private[grid] def doPowerFlow( stateData: CongestionManagementData, ctx: ActorContext[Message], )(using constantData: GridAgentConstantData, buffer: StashBuffer[Message], ): Behavior[Message] = { - // first we find an option for the next mitigation step - val (stepOption, updatedProgress) = - stateData.mitigationProgress.getNextStepsAndUpdate - - // we update the state data with the updated progress - val updatedStateData = stateData.copy(mitigationProgress = updatedProgress) - - stepOption match { - case Some(MitigationSteps.TransformerTapChange) => - GridAgent.updateTransformerTapping( - updatedStateData, - AwaitingData(stateData.inferiorGridRefs.keySet), - ) - - case _ => - // we have no more mitigation steps - // we finish the mitigation - finishCongestionManagement(updatedStateData, ctx) + 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 @@ -134,7 +137,7 @@ trait DCMAlgorithm extends CongestionDetection with TransformerTapChange { // 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/VoltageRange.scala b/src/main/scala/edu/ie3/simona/agent/grid/congestion/VoltageRange.scala index e7f368c091..38cd2cf74f 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/congestion/VoltageRange.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/congestion/VoltageRange.scala @@ -18,10 +18,7 @@ import edu.ie3.simona.model.grid.{ VoltageLimits, } import edu.ie3.util.quantities.QuantityUtils.asPu -import edu.ie3.util.scala.quantities.QuantityConversionUtils.{ - CurrentToSimona, - DimensionlessToSimona, -} +import edu.ie3.util.scala.quantities.QuantityConversionUtils.toSquants import org.apache.pekko.actor.typed.ActorRef import tech.units.indriya.ComparableQuantity 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 cdb3a6b306..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 @@ -7,15 +7,19 @@ package edu.ie3.simona.agent.grid.congestion.data import edu.ie3.datamodel.models.result.CongestionResult -import edu.ie3.simona.agent.grid.congestion.CongestionManagementParams import edu.ie3.datamodel.models.result.CongestionResult.InputModelType import edu.ie3.simona.agent.grid.GridAgent import edu.ie3.simona.agent.grid.GridAgentData.{ GridAgentBaseData, GridAgentDataInternal, } -import edu.ie3.simona.agent.grid.congestion.mitigations.MitigationProgress -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 @@ -40,8 +44,6 @@ import scala.concurrent.duration.FiniteDuration * The found congestions. * @param congestedComponents * The components that have congestion. - * @param mitigationProgress - * The progress of the congestion mitigation. */ final case class CongestionManagementData( gridAgentBaseData: GridAgentBaseData, @@ -50,9 +52,33 @@ final case class CongestionManagementData( powerFlowResults: PowerFlowResultEvent, congestions: Congestions, congestedComponents: CongestedComponents, - mitigationProgress: MitigationProgress, ) 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. @@ -164,7 +190,6 @@ object CongestionManagementData { powerFlowResults, Congestions(congestedComponents), congestedComponents, - MitigationProgress(), ) } 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 4f39632b30..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, @@ -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( @@ -155,19 +182,16 @@ trait CongestionDetection { s"Congestion overall: $updatedCongestions" ) - val updatedStateData = stateData.copy(congestions = updatedCongestions) + // first we find an option for the next mitigation step + val (step, updatedProgress) = stateData.getNextMitigationMeasure - if stateData.congestionManagementParams.anyMitigationEnabled then { - // the mitigation is enabled - // goto mitigation behavior - GridAgent.doCongestionMitigation(updatedStateData, ctx) + ctx.self ! NextStep(step) - } else { - // no mitigation enabled - // just finish the step - ctx.self ! FinishStep - checkForCongestion(updatedStateData, updatedData) - } + val updatedStateData = updatedProgress.copy( + congestions = updatedCongestions + ) + + checkForCongestion(updatedStateData, awaitingData) } } else { diff --git a/src/main/scala/edu/ie3/simona/agent/grid/congestion/mitigations/MitigationProgress.scala b/src/main/scala/edu/ie3/simona/agent/grid/congestion/mitigations/MitigationProgress.scala deleted file mode 100644 index fdaefaa3c6..0000000000 --- a/src/main/scala/edu/ie3/simona/agent/grid/congestion/mitigations/MitigationProgress.scala +++ /dev/null @@ -1,48 +0,0 @@ -/* - * © 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.congestion.mitigations.MitigationSteps.* - -case class MitigationProgress( - hasUsedTransformerTapChange: Boolean = false, - currentIteration: Int = 0, - maxNrOfOptimizationIterations: Int = 1, -) { - - def getNextStepsAndUpdate - : (Option[MitigationSteps.Value], MitigationProgress) = { - val maxOptimizationIterationReached = - currentIteration == maxNrOfOptimizationIterations - - hasUsedTransformerTapChange match { - case true if maxOptimizationIterationReached => - // we have reached the maximal amount of iterations - // we reset the progress and return no next step - (None, reset) - - case true => - ( - Some(TransformerTapChange), - copy(currentIteration = currentIteration + 1), - ) - - case false => - ( - Some(TransformerTapChange), - copy(hasUsedTransformerTapChange = true), - ) - - } - } - - def reset: MitigationProgress = copy( - hasUsedTransformerTapChange = false, - currentIteration = 0, - ) - -} 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 index e9230382f1..88a1c7c6db 100644 --- 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 @@ -9,5 +9,6 @@ package edu.ie3.simona.agent.grid.congestion.mitigations /** Enumeration with all congestion management steps. */ object MitigationSteps extends Enumeration { - val TransformerTapChange, TopologyChange, FlexibilityDeployment = Value + val TransformerTapChange, TopologyChange, FlexibilityDeployment, NoMeasure = + Value } 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 index 31b081f66a..a07362d3a6 100644 --- 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 @@ -87,7 +87,7 @@ trait TransformerTapChange { ) // directly finish congestion management, since we don't have any steps - finishCongestionManagement(stateData, ctx) + GridAgent.doPowerFlow(stateData, ctx) case (ctx, msg) => unsupported(msg, ctx.log) @@ -124,7 +124,7 @@ trait TransformerTapChange { val nodesInSuperiorGrid = gridComponents.nodes.filter(_.subnet == subgrid).map(_.uuid) val transformers = gridComponents.transformers.filter(t => - nodesInSuperiorGrid.contains(t.lvNodeUuid) + nodesInSuperiorGrid.contains(t.hvNodeUuid) ) val transformers3w = gridComponents.transformers3w.filter { t => t.powerFlowCase match { @@ -197,7 +197,7 @@ trait TransformerTapChange { ): 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.debug( + ctx.log.warn( s"Grid ${stateData.gridAgentBaseData.gridEnv.gridModel.subnetNo}, received delta: $delta" ) 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 d480b2b9b3..e07a804037 100644 --- a/src/main/scala/edu/ie3/simona/model/control/TappingGroupModel.scala +++ b/src/main/scala/edu/ie3/simona/model/control/TappingGroupModel.scala @@ -120,7 +120,10 @@ final case class TappingGroupModel( // mapping the data val taps = deltas.map { case (tapping, (tap, _)) => tapping -> tap } val actualDelta = - deltas.map { case (_, (_, delta)) => delta }.toSeq(0) + deltas + .map { case (_, (_, delta)) => delta } + .headOption + .getOrElse(0.asPu) (taps, actualDelta) case None => 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 f3cb1d8478..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,7 +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.mitigations.MitigationProgress +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 @@ -79,7 +79,6 @@ class DCMAlgorithmSpec transformerCongestions = false, ), CongestedComponents.empty, - MitigationProgress(currentIteration = 1), ) // this will return the idle behavior of the grid agent 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 6952e1a294..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 @@ -9,7 +9,6 @@ package edu.ie3.simona.agent.grid.congestion.data import edu.ie3.datamodel.models.result.CongestionResult.InputModelType import edu.ie3.simona.agent.grid.GridAgentData.GridAgentBaseData import edu.ie3.simona.agent.grid.GridEnvironment -import edu.ie3.simona.agent.grid.congestion.mitigations.MitigationProgress import edu.ie3.simona.agent.grid.congestion.{CongestedComponents, Congestions} import edu.ie3.simona.event.ResultEvent.PowerFlowResultEvent import edu.ie3.simona.test.common.UnitSpec @@ -64,7 +63,6 @@ class CongestionManagementDataSpec ), Iterable.empty, ), - MitigationProgress(currentIteration = 1), ) val results = stateData 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 b1cbc45ff9..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,7 +20,7 @@ import edu.ie3.simona.agent.grid.congestion.detection.DetectionMessages.{ CongestionResponse, ReceivedCongestions, } -import edu.ie3.simona.agent.grid.congestion.mitigations.MitigationProgress +import edu.ie3.simona.agent.grid.congestion.CongestionManagementParams.MitigationProgress import edu.ie3.simona.agent.grid.congestion.{ CongestedComponents, CongestionTestBaseData, @@ -82,7 +82,6 @@ class CongestionDetectionSpec transformerCongestions = false, ), CongestedComponents.empty, - MitigationProgress(), ) val cases = Table( @@ -160,7 +159,6 @@ class CongestionDetectionSpec transformerCongestions = false, ), CongestedComponents.empty, - MitigationProgress(), ) val awaitingData: AwaitingData[Congestions] = @@ -211,7 +209,6 @@ class CongestionDetectionSpec transformerCongestions = false, ), CongestedComponents.empty, - MitigationProgress(), ) val awaitingData: AwaitingData[Congestions] = @@ -275,7 +272,6 @@ class CongestionDetectionSpec transformerCongestions = false, ), CongestedComponents.empty, - MitigationProgress(), ) val awaitingData: AwaitingData[Congestions] = 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 index aac820dbf8..122cfae878 100644 --- 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 @@ -11,7 +11,7 @@ import edu.ie3.simona.agent.grid.congestion.data.{ AwaitingData, CongestionManagementData, } -import edu.ie3.simona.agent.grid.congestion.mitigations.MitigationProgress +import edu.ie3.simona.agent.grid.congestion.CongestionManagementParams.MitigationProgress import edu.ie3.simona.agent.grid.congestion.mitigations.TappingMessages.{ ReceivedVoltageRange, RequestVoltageOptions, @@ -103,7 +103,6 @@ class TransformerTagChangeSpec transformerCongestions = false, ), CongestedComponents.empty, - MitigationProgress(), ) // the map is empty, since the inferior grid itself has no inferior grids @@ -148,7 +147,6 @@ class TransformerTagChangeSpec transformerCongestions = false, ), CongestedComponents.empty, - MitigationProgress(), ) // the grid, that receives the request, is a center grid, which has to wait for its inferior data From 7d8fa371c53651aec8bd282a9eae039963436af9 Mon Sep 17 00:00:00 2001 From: staudtMarius Date: Thu, 30 Oct 2025 08:52:47 +0100 Subject: [PATCH 7/7] Fixing issues after merging dev. --- .../agent/grid/congestion/VoltageRange.scala | 4 ---- .../mitigations/TappingMessages.scala | 6 ++---- .../mitigations/TransformerTapChange.scala | 15 +++++++------- .../model/control/TappingGroupModel.scala | 14 ------------- .../grid/congestion/CongestionSpec.scala | 18 ++++------------- .../mitigation/TransformerTagChangeSpec.scala | 20 +++++++++---------- 6 files changed, 23 insertions(+), 54 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/agent/grid/congestion/VoltageRange.scala b/src/main/scala/edu/ie3/simona/agent/grid/congestion/VoltageRange.scala index 241019d3f0..4bd51f0cc4 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/congestion/VoltageRange.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/congestion/VoltageRange.scala @@ -23,7 +23,6 @@ import org.apache.pekko.actor.typed.ActorRef import squants.{Dimensionless, Each} import java.util.UUID -import javax.measure.quantity.Dimensionless /** Object that contains information about possible voltage changes.

If the * delta plus is negative -> upper voltage violation

If the delta minus is @@ -126,9 +125,6 @@ object VoltageRange { val plus = deltaPlus.toEach val minus = deltaMinus.toEach - val plus = deltaPlus.getValue.doubleValue() - val minus = deltaMinus.getValue.doubleValue() - val value = if plus > minus then { // we could have a voltage violation of one limit (plus + minus) / 2 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 index 0ee723f137..9d1a37187c 100644 --- 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 @@ -15,9 +15,7 @@ import edu.ie3.simona.agent.grid.GridAgent.{ import edu.ie3.simona.agent.grid.congestion.VoltageRange import edu.ie3.simona.model.grid.TransformerTapping import org.apache.pekko.actor.typed.ActorRef -import tech.units.indriya.ComparableQuantity - -import javax.measure.quantity.Dimensionless +import squants.Dimensionless object TappingMessages { @@ -61,7 +59,7 @@ object TappingMessages { * Voltage change. */ final case class VoltageDeltaResponse( - delta: ComparableQuantity[Dimensionless] + 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 index a07362d3a6..3030b73342 100644 --- 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 @@ -26,16 +26,17 @@ import edu.ie3.simona.model.grid.Transformer3wPowerFlowCase.{ 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 tech.units.indriya.ComparableQuantity - -import javax.measure.quantity.Dimensionless +import squants.Dimensionless trait TransformerTapChange { @@ -178,7 +179,7 @@ trait TransformerTapChange { // there should be no voltage change in the superior grid, // because the slack grid should always have 1 pu - ctx.self ! VoltageDeltaResponse(0.asPu) + ctx.self ! VoltageDeltaResponse(zeroPU) updateTransformerTapping(stateData, updatedData) } else { // un-stash all messages @@ -189,7 +190,7 @@ trait TransformerTapChange { private def handleUpdatedDataFromSuperior( stateData: CongestionManagementData, awaitingData: AwaitingData[(VoltageRange, Set[TransformerTapping])], - delta: ComparableQuantity[Dimensionless], + delta: Dimensionless, ctx: ActorContext[GridAgent.Message], )(using constantData: GridAgentConstantData, @@ -225,8 +226,8 @@ trait TransformerTapChange { stateData.gridAgentBaseData.gridEnv.gridModel.gridComponents.transformers3w, ) .foreach { group => - val deltaV = group.updateTapPositions(delta, refMap, ctx.log) - group.refs.foreach(_ ! VoltageDeltaResponse(deltaV.add(delta))) + val deltaV = group.updateTapPositions(delta, refMap) + group.refs.foreach(_ ! VoltageDeltaResponse(deltaV + delta)) } } 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 e746ff4b4a..c4dc50a099 100644 --- a/src/main/scala/edu/ie3/simona/model/control/TappingGroupModel.scala +++ b/src/main/scala/edu/ie3/simona/model/control/TappingGroupModel.scala @@ -7,7 +7,6 @@ package edu.ie3.simona.model.control import edu.ie3.datamodel.models.input.connector.ConnectorPort -import edu.ie3.simona.agent.grid.congestion.VoltageRange import edu.ie3.simona.agent.grid.GridAgent import edu.ie3.simona.agent.grid.congestion.VoltageRange import edu.ie3.simona.exceptions.GridInconsistencyException @@ -17,18 +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 - -import javax.measure.quantity.Dimensionless /** A group of [[TransformerTapping]] with all associated [[ActorRef]]s. * @param refs @@ -80,10 +70,6 @@ final case class TappingGroupModel( } } - log.debug( - s"For inferior grids $refs, suggestion: $suggestion, delta: $deltaV" - ) - deltaV } else zeroPU 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 index 60c6009b66..8c1ee0ef9e 100644 --- a/src/test/scala/edu/ie3/simona/agent/grid/congestion/CongestionSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/grid/congestion/CongestionSpec.scala @@ -12,16 +12,9 @@ import edu.ie3.datamodel.models.result.connector.{ LineResult, Transformer2WResult, } -import edu.ie3.simona.agent.grid.congestion.VoltageRange.calculatePossibleVoltageDeltaForLines -import edu.ie3.simona.agent.grid.congestion.{Congestions, VoltageRange} +import edu.ie3.simona.agent.grid.congestion.Congestions import edu.ie3.simona.event.ResultEvent.PowerFlowResultEvent -import edu.ie3.simona.model.grid.GridModel.GridComponents -import edu.ie3.simona.model.grid.{ - GridModel, - RefSystem, - TransformerTapping, - VoltageLimits, -} +import edu.ie3.simona.model.grid.{GridModel, RefSystem, VoltageLimits} import edu.ie3.simona.test.common.model.grid.{ DbfsTestGrid, GridComponentsMokka, @@ -30,11 +23,8 @@ 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.PowerSystemUnits.PU -import edu.ie3.util.quantities.QuantityUtils.{asPu, asDegreeGeom} -import org.apache.pekko.actor.testkit.typed.scaladsl.{ - ScalaTestWithActorTestKit, - TestProbe, -} +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 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 index 122cfae878..ae8f29f038 100644 --- 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 @@ -36,9 +36,7 @@ import org.apache.pekko.actor.testkit.typed.scaladsl.{ } import org.apache.pekko.actor.typed.ActorRef import org.apache.pekko.actor.typed.scaladsl.Behaviors -import tech.units.indriya.ComparableQuantity - -import javax.measure.quantity.Dimensionless +import squants.{Dimensionless, Each} class TransformerTagChangeSpec extends ScalaTestWithActorTestKit @@ -48,7 +46,7 @@ class TransformerTagChangeSpec with QuantityMatchers with GridComponentsMokka { - protected given puTolerance: ComparableQuantity[Dimensionless] = 1e-3.asPu + protected given puTolerance: Dimensionless = Each(1e-3) private type AwaitedData = (VoltageRange, Set[TransformerTapping]) @@ -117,9 +115,9 @@ class TransformerTagChangeSpec val (voltageRange, actualTransformers) = superiorAgent.expectMessageType[VoltageRangeResponse].value - voltageRange.deltaPlus should equalWithTolerance(-0.01.asPu) - voltageRange.deltaMinus should equalWithTolerance(-0.01.asPu) - voltageRange.suggestion should equalWithTolerance(-0.011.asPu) + voltageRange.deltaPlus should approximate(Each(-0.01)) + voltageRange.deltaMinus should approximate(Each(-0.01)) + voltageRange.suggestion should approximate(Each(-0.011)) actualTransformers shouldBe transformers } @@ -165,7 +163,7 @@ class TransformerTagChangeSpec ( inferiorAgent.ref, ( - VoltageRange(0.04.asPu, -0.05.asPu), + VoltageRange(Each(0.04), Each(-0.05)), Set(mockedMvLvTappingModel), ), ) @@ -175,9 +173,9 @@ class TransformerTagChangeSpec val (voltageRange, actualTransformers) = superiorAgent.expectMessageType[VoltageRangeResponse].value - voltageRange.deltaPlus should equalWithTolerance(0.04.asPu) - voltageRange.deltaMinus should equalWithTolerance(0.01.asPu) - voltageRange.suggestion should equalWithTolerance(0.026.asPu) + voltageRange.deltaPlus should approximate(Each(0.04)) + voltageRange.deltaMinus should approximate(Each(0.01)) + voltageRange.suggestion should approximate(Each(0.026)) actualTransformers shouldBe transformers