diff --git a/input/ma_thesis/ma_thesis.conf b/input/ma_thesis/ma_thesis.conf new file mode 100644 index 0000000000..3221c08e0d --- /dev/null +++ b/input/ma_thesis/ma_thesis.conf @@ -0,0 +1,200 @@ +include "../samples/common/pekko.conf" + +pekko.loglevel = "info" + +######### +# ATTENTION: Do not change this config file directly but use it as a base for your personal delta config for the +# vn_simona scenario! Delta configs can be created by including the config you want to change +# parameters from via include (e.g. include "input/samples/vn_simona/vn_simona.conf") at the +# beginning of your config file and then just override the parameters you want to change! +######### + +################################################################## +# Simulation Parameters +################################################################## +simona.simulationName = "Szenario-1" + +################################################################## +# Time Parameters +################################################################## +simona.time.startDateTime = "2016-07-24T00:00:00Z" +simona.time.endDateTime = "2016-07-31T00:00:00Z" +simona.time.schedulerReadyCheckWindow = 900 + +################################################################## +# Congestion Management Configuration +################################################################## + +simona.congestionManagement.enable = true +simona.congestionManagement.enableTransformerTapping = false +simona.congestionManagement.enableTopologyChanges = false +simona.congestionManagement.useFlexOptions = false + +################################################################## +# Grid Configuration +################################################################## + +simona.gridConfig.voltageLimits = [ + {vMin = 0.95, vMax = 1.05, voltLvls = [{id = "lv", vNom = "0.4 kV"}]}, + {vMin = 0.95, vMax = 1.05, voltLvls = [{id = "mv", vNom = "20 kV"}]}, + {vMin = 0.95, vMax = 1.05, voltLvls = [{id = "hv", vNom = "110 kV"}]}, +] + +################################################################## +# Input Parameters +################################################################## +simona.input.primary.csvParams = { + directoryPath: "input/ma_thesis/fullGrid/primary" + csvSep: "," + isHierarchic: false +} +simona.input.grid.datasource.id = "csv" +simona.input.grid.datasource.csvParams = { + directoryPath: "input/ma_thesis/fullGrid" + csvSep: "," + isHierarchic: false +} + +simona.input.weather.datasource = { + scheme = "icon" + sampleParams.use = true + coordinateSource.sampleParams.use = true + maxCoordinateDistance = 50000 +} + +################################################################## +# Output Parameters +################################################################## +simona.output.base.dir = "output/ma_thesis" +simona.output.base.addTimestampToOutputDir = false + +simona.output.sink.csv { + fileFormat = ".csv" + filePrefix = "" + fileSuffix = "" +} + +simona.output.grid = { + notifier = "grid" + nodes = true + lines = true + switches = true + transformers2w = true + transformers3w = false + congestions = true +} +simona.output.participant.defaultConfig = { + notifier = "default" + powerRequestReply = false + simulationResult = false +} +simona.output.participant.individualConfigs = [ + { + notifier = "fixedfeedin" + powerRequestReply = false + simulationResult = true + }, + { + notifier = "load" + powerRequestReply = false + simulationResult = true + } +] + +simona.output.thermal = { + defaultConfig = { + notifier = "default", + simulationResult = false + } + individualConfigs = [] +} + +################################################################## +# Runtime Configuration // todo refactor as this naming is misleading and partly unneeded +################################################################## +simona.runtime.selected_subgrids = [] +simona.runtime.selected_volt_lvls = [] + +simona.runtime.participant.load = { + defaultConfig = { + calculateMissingReactivePowerWithModel = false + uuids = ["default"] + scaling = 1.0 + modelBehaviour = "fix" + reference = "power" + } + individualConfigs = [] +} + +simona.runtime.participant.fixedFeedIn = { + defaultConfig = { + calculateMissingReactivePowerWithModel = false + uuids = ["default"] + scaling = 1.0 + } + individualConfigs = [] +} + +simona.runtime.participant.pv = { + defaultConfig = { + calculateMissingReactivePowerWithModel = false + uuids = ["default"] + scaling = 1.0 + } + individualConfigs = [] +} + +simona.runtime.participant.wec = { + defaultConfig = { + calculateMissingReactivePowerWithModel = false + uuids = ["default"] + scaling = 1.0 + } + individualConfigs = [] +} + +simona.runtime.participant.evcs = { + defaultConfig = { + calculateMissingReactivePowerWithModel = false + uuids = ["default"] + scaling = 1.0 + } + individualConfigs = [] +} + +simona.runtime.participant.hp = { + defaultConfig = { + calculateMissingReactivePowerWithModel = false + uuids = ["default"] + scaling = 1.0 + } + individualConfigs = [] +} + +# # # # # +# ATTENTION: calculateMissingReactivePowerWithModel and scaling is ignored here. +# # # # # +simona.runtime.participant.em = { + defaultConfig = { + calculateMissingReactivePowerWithModel = false + uuids = ["default"] + scaling = 1.0 + } + individualConfigs = [] +} + +################################################################## +# Event Configuration +################################################################## +simona.event.listener = [] + +################################################################## +# Power Flow Configuration +################################################################## +simona.powerflow.maxSweepPowerDeviation = 1E-5 // the maximum allowed deviation in power between two sweeps, before overall convergence is assumed +simona.powerflow.newtonraphson.epsilon = [1E-9] +simona.powerflow.newtonraphson.iterations = 50 +simona.powerflow.resolution = "3600s" +simona.powerflow.stopOnFailure = true + +simona.control.transformer = [] diff --git a/input/samples/vn_simona/vn_simona.conf b/input/samples/vn_simona/vn_simona.conf index 9d7b4e4c39..253fe69fc3 100644 --- a/input/samples/vn_simona/vn_simona.conf +++ b/input/samples/vn_simona/vn_simona.conf @@ -60,6 +60,7 @@ simona.output.grid = { switches = false transformers2w = false transformers3w = false + congestions = true } simona.output.participant.defaultConfig = { notifier = "default" @@ -199,6 +200,21 @@ simona.gridConfig.refSystems = [ {sNom = "1000 MVA", vNom = "380 kV", voltLvls = [{id = "EHV", vNom = "380 kV"}]} ] +simona.gridConfig.voltageLimits = [ + { + vMin = 0.9, + vMax = 1.1, + voltLvls = [ + {id = "lv", vNom = "0.4 kV"}, + {id = "mv", vNom = "10 kV"}, + {id = "mv", vNom = "20 kV"}, + {id = "mv", vNom = "30 kV"}, + {id = "hv", vNom = "110 kV"}, + ]}, + {vMin = 0.9, vMax = 1.118, voltLvls = [{id = "EHV", vNom = "220 kV"}]}, + {vMin = 0.9, vMax = 1.05, voltLvls = [{id = "EHV", vNom = "380 kV"}]}, +] + ################################################################## # Power Flow Configuration ################################################################## @@ -221,3 +237,12 @@ simona.control.transformer = [ vMax = 1.02 } ] + +################################################################## +# Congestion Management Configuration +################################################################## + +simona.congestionManagement.enable = false +simona.congestionManagement.enableTransformerTapping = false +simona.congestionManagement.enableTopologyChanges = false +simona.congestionManagement.useFlexOptions = false diff --git a/src/main/resources/config/config-template.conf b/src/main/resources/config/config-template.conf index 0b636a5d50..2add6c78dc 100644 --- a/src/main/resources/config/config-template.conf +++ b/src/main/resources/config/config-template.conf @@ -14,6 +14,16 @@ RefSystemConfig { gridIds: [string] # Sub grid numbers to apply to, expl.: 1,2,4..10 } +#@define +VoltageLimitsConfig { + vMin: double # minimal voltage + vMax: double # maximal voltage + #@optional + voltLvls: [VoltLvlConfig] # Voltage levels to apply to + #@optional + gridIds: [string] # Sub grid numbers to apply to, expl.: 1,2,4..10 +} + #@define abstract extends !java.io.Serializable BaseRuntimeConfig { uuids: [string] # Unique id to identify the system participant models this config applies for @@ -145,6 +155,7 @@ GridOutputConfig { switches: boolean | false transformers2w: boolean | false transformers3w: boolean | false + congestions: boolean | false } #@define @@ -360,6 +371,9 @@ simona.powerflow.stopOnFailure = boolean | false #@optional simona.gridConfig.refSystems = [RefSystemConfig] +#@optional +simona.gridConfig.voltageLimits = [VoltageLimitsConfig] + ################################################################## # Event Configuration ################################################################## @@ -372,6 +386,17 @@ simona.event.listener = [ } ] +################################################################## +# Congestion Management Configuration +################################################################## + +simona.congestionManagement.enable = boolean | false +simona.congestionManagement.enableTransformerTapping = boolean | false +simona.congestionManagement.enableTopologyChanges = boolean | false +simona.congestionManagement.maxOptimizationIterations = int | 1 +simona.congestionManagement.useFlexOptions = boolean | false +simona.congestionManagement.timeout = "duration:seconds | 30 seconds" // maximum timeout + ################################################################## # Configuration of Control Schemes ################################################################## diff --git a/src/main/scala/edu/ie3/simona/agent/grid/CongestionManagementParams.scala b/src/main/scala/edu/ie3/simona/agent/grid/CongestionManagementParams.scala new file mode 100644 index 0000000000..bcc0dc0533 --- /dev/null +++ b/src/main/scala/edu/ie3/simona/agent/grid/CongestionManagementParams.scala @@ -0,0 +1,53 @@ +/* + * © 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 java.time.Duration + +/** Holds all congestion management configuration parameters used in + * [[edu.ie3.simona.agent.grid]]. If the parameter [[enabled]] is set to false, + * no congestion management is run and all the other parameters are ignored + * + * @param enabled + * defines if the congestion management is active and can be run + * @param transformerTapping + * defines if the transformer tapping should be used for tappable + * transformers + * @param topologyChanges + * defines if switches should be used to change the topology of the grid + * @param flexOptions + * defines if available [[edu.ie3.simona.agent.em.EmAgent]] should be used to + * resolve congestions + */ +final case class CongestionManagementParams( + enabled: Boolean, + transformerTapping: Boolean, + topologyChanges: Boolean, + flexOptions: Boolean, + maxOptimizationIterations: Int, + timeout: Duration, + iteration: Int = 0, + hasRunTransformerTapping: Boolean = false, + hasUsedFlexOptions: Boolean = false, +) { + def runTransformerTapping: Boolean = + transformerTapping && !hasRunTransformerTapping && runOptimization + + def runTopologyChanges: Boolean = topologyChanges && runOptimization + + def useFlexOptions: Boolean = flexOptions && !hasUsedFlexOptions + + def clean: CongestionManagementParams = { + copy( + hasRunTransformerTapping = false, + hasUsedFlexOptions = false, + iteration = 0, + ) + } + + private def runOptimization: Boolean = iteration < maxOptimizationIterations +} diff --git a/src/main/scala/edu/ie3/simona/agent/grid/CongestionManagementSupport.scala b/src/main/scala/edu/ie3/simona/agent/grid/CongestionManagementSupport.scala new file mode 100644 index 0000000000..e3216f233d --- /dev/null +++ b/src/main/scala/edu/ie3/simona/agent/grid/CongestionManagementSupport.scala @@ -0,0 +1,642 @@ +/* + * © 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.input.connector.ConnectorPort +import edu.ie3.datamodel.models.result.connector.LineResult +import edu.ie3.simona.agent.grid.CongestionManagementSupport.{ + TappingGroup, + VoltageRange, +} +import edu.ie3.simona.event.ResultEvent.PowerFlowResultEvent +import edu.ie3.simona.exceptions.{GridInconsistencyException, ResultException} +import edu.ie3.simona.model.grid.GridModel.GridComponents +import edu.ie3.simona.model.grid.Transformer3wPowerFlowCase.PowerFlowCaseA +import edu.ie3.simona.model.grid._ +import edu.ie3.util.quantities.QuantityUtils.RichQuantityDouble +import org.apache.pekko.actor.typed.ActorRef +import squants.electro.Amperes +import tech.units.indriya.ComparableQuantity + +import java.util.UUID +import javax.measure.quantity.Dimensionless + +/** Support and helper methods for calculations done during the congestion + * management. + */ +trait CongestionManagementSupport { + + /** Method for 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 [[TappingGroup]]s + */ + def groupTappingModels( + receivedData: Map[ActorRef[GridAgent.Request], Set[TransformerTapping]], + transformer3ws: Set[Transformer3wModel], + ): Set[TappingGroup] = { + 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) => + TappingGroup(refs, tappingModels) + } + .toSet + } + + /** Method for calculating the tap pos changes for all given transformers and + * the voltage delta. + * + * @param range + * given voltage range + * @param tappings + * a set of all transformers + * @return + * a map: model to tap pos change and resulting voltage delta + */ + def calculateTapAndVoltage( + range: VoltageRange, + tappings: Seq[TransformerTapping], + ): (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)) { + val possibleDeltas = tappings.map( + _.possibleDeltas(range.deltaPlus, range.deltaMinus, ConnectorPort.B) + ) + + val deltaOption = if (possibleDeltas.exists(_.isEmpty)) { + // there is a transformer that cannot be tapped + None + } else if (possibleDeltas.exists(_.size == 1)) { + // there is a transformer that can only be tapped by one delta + val delta = possibleDeltas.flatten.toSet + + if (delta.size == 1) { + // all transformer have the same delta + Some(delta.toSeq(0)) + } else None + + } else { + // the actual delta that can be used for all transformers + val delta = findCommonDelta(suggestion, possibleDeltas) + + Some(delta) + } + + deltaOption match { + case Some(delta) => + val deltas = tappings + .map(model => model -> model.computeDeltas(delta, ConnectorPort.B)) + .toMap + + val taps = deltas.map { case (tapping, (tap, _)) => tapping -> tap } + val actualDelta = deltas.map(_._2._2).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: Seq[List[ComparableQuantity[Dimensionless]]], + ): ComparableQuantity[Dimensionless] = { + // reduce all possible deltas + val reducedOptions = possibleDeltas.map { 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() + } + } + } + + // filter the possible options + val filteredOptions: Set[ComparableQuantity[Dimensionless]] = + reducedOptions.flatten + .groupBy(identity) + .filter(_._2.size == reducedOptions.size) + .keySet + + // find the best suitable delta + filteredOptions.size match { + case 0 => 0.asPu + case 1 => filteredOptions.toSeq(0) + case _ => + if (filteredOptions.exists(_.isEquivalentTo(suggestion))) { + suggestion + } else { + + val minOption = filteredOptions + .filter(_.isLessThan(suggestion)) + .lastOption + .map(_.getValue.doubleValue()) + val maxOption = filteredOptions + .find(_.isGreaterThan(suggestion)) + .map(_.getValue.doubleValue()) + + (minOption, maxOption) match { + case (Some(min), Some(max)) => + val suggestionDouble = suggestion.getValue.doubleValue() + + if ( + Math.abs(suggestionDouble - min) < Math.abs( + suggestionDouble - max + ) + ) { + min.asPu + } else max.asPu + + case (Some(min), _) => + min.asPu + case (_, Some(max)) => + max.asPu + case _ => + 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 + */ + def calculatePossibleVoltageRange( + 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 ResultException(s"No node result found!")) + val maxVoltage = nodeResMap + .maxByOption(_._2) + .getOrElse(throw new ResultException(s"No node result found!")) + + 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) + + val (voltage, deltaI) = + if (res.getiAMag().isGreaterThan(res.getiBMag())) { + ( + nodeResults(line.nodeAUuid).getValue.doubleValue(), + line.iNom.value - res.getiAMag().getValue.doubleValue(), + ) + } else { + ( + nodeResults(line.nodeBUuid).getValue.doubleValue(), + line.iNom.value - res.getiBMag().getValue.doubleValue(), + ) + } + + (voltage * deltaI) / line.iNom.value * -1 + } + + // determine the actual possible voltage change + val change = voltageChanges.maxOption.getOrElse( + throw new ResultException(s"No line result found!") + ) + + // change < 0 => tapping down possible + // change > 0 => tapping up is necessary + change.asPu + } + +} + +object CongestionManagementSupport { + + /** A group of [[TransformerTapping]] with all associated [[ActorRef]]s. + * @param refs + * a set of [[ActorRef]]s + * @param tappingModels + * a set of [[TransformerTapping]] + */ + final case class TappingGroup( + refs: Set[ActorRef[GridAgent.Request]], + tappingModels: Set[TransformerTapping], + ) + + /** 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 the suggestion is set to the delta plus, + * because having a too high voltage is more severe + * @param deltaPlus + * maximale possible voltage increase + * @param deltaMinus + * maximale possible voltage decrease + * @param suggestion + * for voltage change + */ + final case class VoltageRange( + deltaPlus: ComparableQuantity[Dimensionless], + deltaMinus: ComparableQuantity[Dimensionless], + suggestion: ComparableQuantity[Dimensionless], + ) { + + def isInRange( + delta: ComparableQuantity[Dimensionless] + ): Boolean = + delta.isGreaterThanOrEqualTo(deltaMinus) && delta.isLessThanOrEqualTo( + deltaPlus + ) + + /** 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))) => + // allow tapping only if all transformers support tapping + val (plus, minus) = if (tappings.forall(_.hasAutoTap)) { + + // TODO: Enhance tests, to tests these changes + 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 + + val (possiblePlus, possibleMinus) = if (tappings.size == 1) { + tappingRanges(0) + } else { + // check for possible increase and decrease that can be applied to all transformers + ( + tappingRanges.map(_._1).minOption.getOrElse(0.asPu), + tappingRanges.map(_._2).maxOption.getOrElse(0.asPu), + ) + } + + val increase = range.deltaPlus + .add(possibleMinus) + .isLessThanOrEqualTo(infRange.deltaPlus) + val decrease = range.deltaMinus + .add(possiblePlus) + .isGreaterThanOrEqualTo(infRange.deltaMinus) + + (increase, decrease) match { + case (true, true) => + (range.deltaPlus, range.deltaMinus) + case (true, false) => + (range.deltaPlus, infRange.deltaMinus.subtract(possiblePlus)) + case (false, true) => + (infRange.deltaPlus.subtract(possibleMinus), range.deltaMinus) + case (false, false) => + (infRange.deltaPlus, infRange.deltaMinus) + } + } else { + // no tapping possible, just update the range + + ( + range.deltaPlus.isGreaterThanOrEqualTo(infRange.deltaPlus), + range.deltaMinus.isLessThanOrEqualTo(infRange.deltaMinus), + ) match { + case (true, true) => + (infRange.deltaPlus, infRange.deltaMinus) + case (true, false) => + (infRange.deltaPlus, range.deltaMinus) + case (false, true) => + (range.deltaPlus, infRange.deltaMinus) + case (false, false) => + (range.deltaPlus, range.deltaMinus) + } + } + + VoltageRange(plus, minus) + } + } + } + + 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 if (plus > 0 && minus > 0) { + // we have a voltage violation of the lower limit + // since the upper limit is fine, we can increase the voltage a bit + plus + } else if (plus < 0 && minus < 0) { + // we have a voltage violation of the upper limit + // since the lower limit is fine, we can decrease the voltage a bit + minus + } else 0 // we have a voltage violation of both limits, we can't fix this + + 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, + ) + } + } + + def combineSuggestions( + ranges: Iterable[VoltageRange] + ): ComparableQuantity[Dimensionless] = { + ranges.headOption match { + case Some(value) => + if (ranges.size == 1) { + value.suggestion + } else { + ranges + .foldLeft(value) { case (combined, current) => + ( + combined.deltaPlus.isGreaterThanOrEqualTo(current.deltaPlus), + combined.deltaMinus.isLessThanOrEqualTo(current.deltaMinus), + ) match { + case (true, true) => + current + case (true, false) => + combined.copy(deltaPlus = current.deltaPlus) + case (false, true) => + combined.copy(deltaMinus = current.deltaMinus) + case (false, false) => + combined + } + } + .suggestion + } + case None => + // no suggestion found => no tapping suggestion + 0.asPu + } + } + + def combine( + ranges: Iterable[VoltageRange], + offset: ComparableQuantity[Dimensionless], + ): VoltageRange = { + val minPlus = ranges.minByOption(_.deltaPlus).map(_.deltaPlus) + 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) + } + + } + } + + final case class Congestions( + voltageCongestions: Boolean, + lineCongestions: Boolean, + transformerCongestions: Boolean, + ) { + + def any: Boolean = + voltageCongestions || lineCongestions || transformerCongestions + + def assetCongestion: Boolean = lineCongestions || transformerCongestions + + def combine(options: Iterable[Congestions]): Congestions = + Congestions( + voltageCongestions || options.exists(_.voltageCongestions), + lineCongestions || options.exists(_.lineCongestions), + transformerCongestions || options.exists(_.transformerCongestions), + ) + } + + object CongestionManagementSteps extends Enumeration { + val TransformerTapping, TopologyChanges, UsingFlexibilities = Value + } + +} 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 5f21b06a39..232f4d3fe4 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/DBFSAlgorithm.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/DBFSAlgorithm.scala @@ -15,8 +15,10 @@ import edu.ie3.powerflow.model.PowerFlowResult import edu.ie3.powerflow.model.PowerFlowResult.FailedPowerFlowResult.FailedNewtonRaphsonPFResult import edu.ie3.powerflow.model.PowerFlowResult.SuccessFullPowerFlowResult.ValidNewtonRaphsonPFResult import edu.ie3.powerflow.model.enums.NodeType -import edu.ie3.simona.agent.grid.GridAgent.idle +import edu.ie3.simona.agent.grid.GridAgent.pipeToSelf import edu.ie3.simona.agent.grid.GridAgentData.{ + AwaitingData, + CongestionManagementData, GridAgentBaseData, GridAgentConstantData, PowerFlowDoneData, @@ -28,6 +30,7 @@ import edu.ie3.simona.agent.participant.ParticipantAgent.{ ParticipantMessage, RequestAssetPowerMessage, } +import edu.ie3.simona.event.ResultEvent.PowerFlowResultEvent import edu.ie3.simona.event.RuntimeEvent.PowerFlowFailed import edu.ie3.simona.exceptions.agent.DBFSAlgorithmException import edu.ie3.simona.model.grid.{NodeModel, RefSystem} @@ -52,7 +55,6 @@ import squants.Each import java.time.{Duration, ZonedDateTime} import java.util.UUID import scala.concurrent.{ExecutionContext, Future} -import scala.util.{Failure, Success} /** Trait that is normally mixed into every [[GridAgent]] to enable distributed * forward backward sweep (DBFS) algorithm execution. It is considered to be @@ -238,7 +240,7 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { /* Determine the slack node voltage under consideration of the target voltage set point */ val vTarget = gridAgentBaseData.gridEnv.gridModel.gridComponents.nodes - .find { case NodeModel(uuid, _, _, isSlack, _, _) => + .find { case NodeModel(uuid, _, _, isSlack, _, _, _) => uuid == nodeUuid && isSlack } .map(_.vTarget) @@ -462,33 +464,39 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { } } - // notify listener about the results ctx.log.debug( - "Calculate results and sending the results to the listener ..." - ) - createAndSendPowerFlowResults( - gridAgentBaseData, - currentTick.toDateTime(constantData.simStartTime), - )(ctx.log, constantData) - - // do my cleanup stuff - ctx.log.debug("Doing my cleanup stuff") - - // / clean copy of the gridAgentBaseData - val cleanedGridAgentBaseData = GridAgentBaseData.clean( - gridAgentBaseData, - gridAgentBaseData.superiorGridNodeUuids, - gridAgentBaseData.inferiorGridGates, + "Calculate results ..." ) + val results: Option[PowerFlowResultEvent] = + gridAgentBaseData.sweepValueStores.lastOption.map { + case (_, valueStore) => + createResultModels( + gridAgentBaseData.gridEnv.gridModel, + valueStore, + )(currentTick.toDateTime(constantData.simStartTime), ctx.log) + } - // / inform scheduler that we are done with the whole simulation and request new trigger for next time step - constantData.environmentRefs.scheduler ! Completion( - constantData.activationAdapter, - Some(currentTick + constantData.resolution), - ) + // check if congestion management is enabled + if (gridAgentBaseData.congestionManagementParams.enabled) { - // return to Idle - idle(cleanedGridAgentBaseData) + // get result or build empty data + val congestionManagementData = results + .map(res => + CongestionManagementData(gridAgentBaseData, currentTick, res) + ) + .getOrElse( + CongestionManagementData.empty(gridAgentBaseData, currentTick) + ) + + ctx.self ! StartStep + GridAgent.checkForCongestion( + congestionManagementData, + AwaitingData(congestionManagementData.inferiorGrids), + ) + } else { + // clean up agent and go back to idle + GridAgent.gotoIdle(gridAgentBaseData, currentTick, results, ctx) + } // handles power request that arrive to early case (requestGridPower: RequestGridPower, _) => @@ -1361,6 +1369,7 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { * @param currentTimestamp * the current time stamp */ + @deprecated private def createAndSendPowerFlowResults( gridAgentBaseData: GridAgentBaseData, currentTimestamp: ZonedDateTime, @@ -1381,23 +1390,4 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { ) } } - - /** This method uses [[ActorContext.pipeToSelf()]] to send a future message to - * itself. If the future is a [[Success]] the message is send, else a - * [[WrappedFailure]] with the thrown error is send. - * - * @param future - * future message that should be send to the agent after it was processed - * @param ctx - * [[ActorContext]] of the receiving actor - */ - private def pipeToSelf( - future: Future[GridAgent.Request], - ctx: ActorContext[GridAgent.Request], - ): Unit = { - ctx.pipeToSelf[GridAgent.Request](future) { - case Success(value) => value - case Failure(exception) => WrappedFailure(exception) - } - } } diff --git a/src/main/scala/edu/ie3/simona/agent/grid/DCMAlgorithm.scala b/src/main/scala/edu/ie3/simona/agent/grid/DCMAlgorithm.scala new file mode 100644 index 0000000000..1a9d82e248 --- /dev/null +++ b/src/main/scala/edu/ie3/simona/agent/grid/DCMAlgorithm.scala @@ -0,0 +1,544 @@ +/* + * © 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.simona.agent.grid.CongestionManagementSupport.CongestionManagementSteps._ +import edu.ie3.simona.agent.grid.CongestionManagementSupport.{ + Congestions, + TappingGroup, + VoltageRange, +} +import edu.ie3.simona.agent.grid.GridAgent.pipeToSelf +import edu.ie3.simona.agent.grid.GridAgentData.{ + AwaitingData, + CongestionManagementData, + GridAgentBaseData, + GridAgentConstantData, +} +import edu.ie3.simona.agent.grid.GridAgentMessages._ +import edu.ie3.simona.model.grid.TransformerTapping +import edu.ie3.simona.ontology.messages.Activation +import edu.ie3.util.quantities.QuantityUtils.RichQuantityDouble +import org.apache.pekko.actor.typed.scaladsl.AskPattern.Askable +import org.apache.pekko.actor.typed.scaladsl.{ + ActorContext, + Behaviors, + StashBuffer, +} +import org.apache.pekko.actor.typed.{ActorRef, Behavior, Scheduler} +import org.apache.pekko.util.Timeout + +import scala.concurrent.{ExecutionContext, Future} + +/** Trait that is normally mixed into every [[GridAgent]] to enable distributed + * congestion management (DCM) algorithm execution. It is considered to be the + * standard behaviour of a [[GridAgent]]. + */ +trait DCMAlgorithm extends CongestionManagementSupport { + + /** Method that defines the [[Behavior]] for checking if there are any + * congestion in the grid. + * @param stateData + * of the actor + * @param constantData + * constant data of the [[GridAgent]] + * @param buffer + * for stashed messages + * @return + * a [[Behavior]] + */ + private[grid] def checkForCongestion( + stateData: CongestionManagementData, + awaitingData: AwaitingData[Congestions], + )(implicit + constantData: GridAgentConstantData, + buffer: StashBuffer[GridAgent.Request], + ): Behavior[GridAgent.Request] = Behaviors.receivePartial { + case (ctx, StartStep) => + // request congestion check if we have inferior grids + askInferior( + stateData, + CongestionCheckRequest, + ReceivedCongestions, + ctx, + ) + + Behaviors.same + + case (ctx, congestionRequest @ CongestionCheckRequest(sender)) => + // check if waiting for inferior data is needed + if (awaitingData.notDone) { + ctx.log.debug( + s"Received request for congestions 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(congestionRequest) + } else { + // check if there are any congestions in the grid + val congestions = stateData.congestions + + if (congestions.any) { + ctx.log.info( + s"In the grid ${stateData.gridAgentBaseData.gridEnv.gridModel.subnetNo}, the following congestions were found: $congestions" + ) + } + + // sends the results to the superior grid + sender ! CongestionResponse( + ctx.self, + congestions.combine(awaitingData.values), + ) + } + + Behaviors.same + + case (ctx, ReceivedCongestions(congestions)) => + // updating the state data with received data from inferior grids + val updatedData = awaitingData.handleReceivingData(congestions) + + if (stateData.gridAgentBaseData.isSuperior) { + // if we are the superior grid, we find the next behavior + + val congestions = stateData.congestions.combine(updatedData.values) + + // checking for any congestion in the complete grid + if (!congestions.any) { + ctx.log.warn( + s"No congestions found. Finishing the congestion management." + ) + + ctx.self ! GotoIdle + checkForCongestion(stateData, updatedData) + } else { + ctx.log.warn( + s"Congestion overall: $congestions" + ) // TODO: Change to debug + + val steps = stateData.gridAgentBaseData.congestionManagementParams + + val msg = + if ( + (congestions.voltageCongestions || congestions.lineCongestions) && steps.runTransformerTapping + ) { + NextStepRequest(TransformerTapping) + } else if ( + congestions.assetCongestion && steps.runTopologyChanges + ) { + NextStepRequest(TopologyChanges) + } else if (congestions.any && steps.useFlexOptions) { + NextStepRequest(UsingFlexibilities) + } else { + val timestamp = + constantData.simStartTime.plusSeconds(stateData.currentTick) + + ctx.log.info( + s"There were some congestions that could not be resolved for timestamp: $timestamp." + ) + GotoIdle + } + + ctx.self ! msg + checkForCongestion(stateData, updatedData) + } + + } else { + // un-stash all messages + buffer.unstashAll(checkForCongestion(stateData, updatedData)) + } + + case (ctx, NextStepRequest(next)) => + // inform my inferior grids about the next behavior + stateData.inferiorRefs.foreach( + _ ! NextStepRequest(next) + ) + + // switching to the next behavior + ctx.self ! StartStep + + next match { + case TransformerTapping => + buffer.unstashAll( + updateTransformerTapping( + stateData, + AwaitingData(stateData.inferiorGrids), + ) + ) + case TopologyChanges => + buffer.unstashAll( + useTopologyChanges(stateData, AwaitingData(stateData.inferiorGrids)) + ) + case UsingFlexibilities => + buffer.unstashAll( + useFlexOptions(stateData, AwaitingData(stateData.inferiorGrids)) + ) + } + + case (ctx, GotoIdle) => + // inform my inferior grids about the end of the congestion management + stateData.inferiorRefs.foreach( + _ ! GotoIdle + ) + + // clean up agent and go back to idle + val powerFlowResults = stateData.powerFlowResults.copy(congestionResults = + Seq(stateData.getCongestionResult(constantData.simStartTime)) + ) + + GridAgent.gotoIdle( + stateData.gridAgentBaseData, + stateData.currentTick, + Some(powerFlowResults), + ctx, + ) + + case (ctx, msg) => + ctx.log.debug(s"Received unsupported msg: $msg. Stash away!") + buffer.stash(msg) + Behaviors.same + } + + /** 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])], + )(implicit + 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, + ref => RequestVoltageOptions(ref, subnet), + ReceivedVoltageRange, + ctx, + ) + + Behaviors.same + + case (ctx, voltageRangeRequest @ RequestVoltageOptions(sender, subnet)) => + // check if waiting for inferior data is needed + if (awaitingData.notDone) { + ctx.log.debug( + s"Received request for congestions 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(voltageRangeRequest) + } 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 == subnet).map(_.uuid) + val transformers = gridComponents.transformers.filter(t => + nodesInSuperiorGrid.contains(t.hvNodeUuid) + ) + val transformers3w = gridComponents.transformers3w.filter(t => + nodesInSuperiorGrid.contains(t.hvNodeUuid) + ) + + val allTransformers = (transformers ++ transformers3w).map( + _.asInstanceOf[TransformerTapping] + ) + + // calculate the voltage range with the received data + val range = calculatePossibleVoltageRange( + stateData.powerFlowResults, + gridModel.voltageLimits, + gridModel.gridComponents, + awaitingData.mappedValues, + gridModel.subnetNo, + ) + + ctx.log.warn( + s"For Grid ${stateData.gridAgentBaseData.gridEnv.gridModel.subnetNo}, voltage range: $range" + ) + + sender ! VoltageRangeResponse( + ctx.self, + (range, allTransformers), + ) + } + + Behaviors.same + + case (ctx, ReceivedVoltageRange(voltageRange)) => + // 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)) + } + + case (ctx, VoltageDeltaResponse(delta)) => + // if we are the superior grid to another grid, we check for transformer tapping option + // and send the new delta to the inferior grid + + ctx.log.warn( + s"Grid ${stateData.gridAgentBaseData.gridEnv.gridModel.subnetNo}, received delta: $delta" + ) // TODO: Change to debug + + if (stateData.inferiorRefs.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 + } + + // 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 + val groups = + groupTappingModels( + receivedData.map { case (ref, (_, tappings)) => ref -> tappings }, + stateData.gridAgentBaseData.gridEnv.gridModel.gridComponents.transformers3w, + ) + + groups.foreach { case TappingGroup(refs, tappingModels) => + // get all possible voltage ranges of the inferior grids + val inferiorRanges = refs.map(refMap) + + // check if all transformers support tapping + if (tappingModels.forall(_.hasAutoTap)) { + // the given transformer can be tapped, calculate the new tap pos + + val suggestion = VoltageRange.combine(inferiorRanges, delta) + + // calculating the tap changes for all transformers and the resulting voltage delta + val (tapChange, deltaV) = calculateTapAndVoltage( + suggestion, + tappingModels.toSeq, + ) + + // change the tap pos of all transformers + tapChange.foreach { case (tapping, tapChange) => + if (tapChange > 0) { + tapping.incrTapPos(tapChange) + } else if (tapChange < 0) { + tapping.decrTapPos(tapChange) + } else { + // no change, do nothing + } + } + + ctx.log.warn( + s"For inferior grids $refs, suggestion: $suggestion, delta: $deltaV" + ) // TODO: Change to debug + + // send the resulting voltage delta to all inferior grids + refs.foreach(_ ! VoltageDeltaResponse(deltaV.add(delta))) + } else { + // no tapping possible, just send the delta to the inferior grid + refs.foreach(_ ! VoltageDeltaResponse(delta)) + } + } + } + + // all work is done in this grid, therefore finish this step + // simulate grid after changing the transformer tapping + buffer.unstashAll( + clearAndGotoSimulateGrid( + stateData.cleanAfterTransformerTapping, + stateData.currentTick, + ctx, + ) + ) + + case (ctx, msg) => + ctx.log.debug(s"Received unsupported msg: $msg. Stash away!") + buffer.stash(msg) + Behaviors.same + } + + // TODO: Implement a proper behavior + private[grid] def useTopologyChanges( + stateData: CongestionManagementData, + awaitingData: AwaitingData[_], + )(implicit + constantData: GridAgentConstantData, + buffer: StashBuffer[GridAgent.Request], + ): Behavior[GridAgent.Request] = Behaviors.receivePartial { + case (ctx, StartStep) => + if (stateData.gridAgentBaseData.isSuperior) { + // for now this step is skipped + ctx.log.warn( + s"Using topology changes to resolve a congestion is not implemented yet. Skipping this step!" + ) + + ctx.self ! FinishStep + } + + Behaviors.same + + case (ctx, FinishStep) => + // inform my inferior grids about the end of this step + stateData.inferiorRefs.foreach(_ ! FinishStep) + + // simulate grid after using topology changes + clearAndGotoSimulateGrid( + stateData.cleanAfterTopologyChange, + stateData.currentTick, + ctx, + ) + + case (ctx, msg) => + ctx.log.debug(s"Received unsupported msg: $msg. Stash away!") + buffer.stash(msg) + Behaviors.same + } + + // TODO: Implement a proper behavior + private[grid] def useFlexOptions( + stateData: CongestionManagementData, + awaitingData: AwaitingData[_], + )(implicit + constantData: GridAgentConstantData, + buffer: StashBuffer[GridAgent.Request], + ): Behavior[GridAgent.Request] = Behaviors.receivePartial { + case (ctx, StartStep) => + if (stateData.gridAgentBaseData.isSuperior) { + // for now this step is skipped + ctx.log.warn( + s"Using flex options to resolve a congestion is not implemented yet. Skipping this step!" + ) + + ctx.self ! FinishStep + } + + Behaviors.same + + case (ctx, FinishStep) => + // inform my inferior grids about the end of this step + stateData.inferiorRefs.foreach(_ ! FinishStep) + + // simulate grid after finishing the congestion management + clearAndGotoSimulateGrid( + stateData.cleanAfterFlexOptions, + stateData.currentTick, + ctx, + ) + + case (ctx, msg) => + ctx.log.debug(s"Received unsupported msg: $msg. Stash away!") + buffer.stash(msg) + Behaviors.same + } + + /** Method to ask all inferior grids a [[CMRequest]]. + * + * @param stateData + * current state data + * @param askMsgBuilder + * function to build the asked message + * @param resMsgBuilder + * function to build the returned message + * @param ctx + * actor context to use + * @tparam T + * type of data + */ + private def askInferior[T]( + stateData: CongestionManagementData, + askMsgBuilder: ActorRef[GridAgent.Request] => CMRequest, + resMsgBuilder: Vector[(ActorRef[GridAgent.Request], T)] => CMResponse[T], + ctx: ActorContext[GridAgent.Request], + ): Unit = { + + if (stateData.inferiorRefs.nonEmpty) { + implicit val askTimeout: Timeout = Timeout.create( + stateData.gridAgentBaseData.congestionManagementParams.timeout + ) + implicit val ec: ExecutionContext = ctx.executionContext + implicit val scheduler: Scheduler = ctx.system.scheduler + + val future = Future + .sequence( + stateData.inferiorRefs.map { inferiorGridAgentRef => + inferiorGridAgentRef + .ask(askMsgBuilder) + .map { case response: CMReceiveResponse[T] => + (response.sender, response.value) + } + }.toVector + ) + .map(resMsgBuilder) + pipeToSelf(future, ctx) + } + + } + + /** Method to clear all data and go to the [[DBFSAlgorithm.simulateGrid]]. + * + * @param gridAgentBaseData + * to clear + * @param currentTick + * to use + * @param ctx + * actor context + * @param constantData + * constant grid agent data + * @param buffer + * for buffered messages + * @return + * a new [[Behavior]] + */ + private def clearAndGotoSimulateGrid( + gridAgentBaseData: GridAgentBaseData, + currentTick: Long, + ctx: ActorContext[GridAgent.Request], + )(implicit + constantData: GridAgentConstantData, + buffer: StashBuffer[GridAgent.Request], + ): Behavior[GridAgent.Request] = { + + val cleanedData = GridAgentBaseData.clean( + gridAgentBaseData, + gridAgentBaseData.superiorGridNodeUuids, + gridAgentBaseData.inferiorGridGates, + ) + + ctx.self ! WrappedActivation(Activation(currentTick)) + + buffer.unstashAll( + GridAgent.simulateGrid( + cleanedData.copy(congestionManagementParams = + gridAgentBaseData.congestionManagementParams + ), + currentTick, + ) + ) + } +} 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 f02d7c36b1..6abb7989ed 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/GridAgent.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/GridAgent.scala @@ -17,6 +17,7 @@ import edu.ie3.simona.agent.grid.GridAgentMessages._ import edu.ie3.simona.agent.participant.ParticipantAgent.ParticipantMessage import edu.ie3.simona.config.SimonaConfig import edu.ie3.simona.event.ResultEvent +import edu.ie3.simona.event.ResultEvent.PowerFlowResultEvent import edu.ie3.simona.exceptions.agent.GridAgentInitializationException import edu.ie3.simona.model.grid.GridModel import edu.ie3.simona.ontology.messages.Activation @@ -26,15 +27,21 @@ import edu.ie3.simona.ontology.messages.SchedulerMessage.{ } import edu.ie3.simona.util.SimonaConstants.INIT_SIM_TICK import edu.ie3.util.TimeUtil -import org.apache.pekko.actor.typed.scaladsl.{Behaviors, StashBuffer} +import org.apache.pekko.actor.typed.scaladsl.{ + ActorContext, + Behaviors, + StashBuffer, +} import org.apache.pekko.actor.typed.{ActorRef, Behavior} import java.time.ZonedDateTime import java.time.temporal.ChronoUnit import java.util.UUID +import scala.concurrent.Future import scala.language.postfixOps +import scala.util.{Failure, Success} -object GridAgent extends DBFSAlgorithm { +object GridAgent extends DBFSAlgorithm with DCMAlgorithm { /** Trait for requests made to the [[GridAgent]] */ sealed trait Request @@ -115,6 +122,8 @@ object GridAgent extends DBFSAlgorithm { ctx.log.debug("Received InitializeTrigger.") + val cfg = constantData.simonaConfig.simona + // build the assets concurrently val subGridContainer = gridAgentInitData.subGridContainer val refSystem = gridAgentInitData.refSystem @@ -127,11 +136,12 @@ object GridAgent extends DBFSAlgorithm { val gridModel = GridModel( subGridContainer, refSystem, + gridAgentInitData.voltageLimits, TimeUtil.withDefaults.toZonedDateTime( - constantData.simonaConfig.simona.time.startDateTime + cfg.time.startDateTime ), TimeUtil.withDefaults.toZonedDateTime( - constantData.simonaConfig.simona.time.endDateTime + cfg.time.endDateTime ), simonaConfig, ) @@ -142,9 +152,9 @@ object GridAgent extends DBFSAlgorithm { constantData.environmentRefs, constantData.simStartTime, TimeUtil.withDefaults - .toZonedDateTime(constantData.simonaConfig.simona.time.endDateTime), - constantData.simonaConfig.simona.runtime.participant, - constantData.simonaConfig.simona.output.participant, + .toZonedDateTime(cfg.time.endDateTime), + cfg.runtime.participant, + cfg.output.participant, constantData.resolution, constantData.listener, ctx.log, @@ -174,11 +184,19 @@ object GridAgent extends DBFSAlgorithm { gridAgentInitData.superiorGridNodeUuids, gridAgentInitData.inferiorGridGates, PowerFlowParams( - constantData.simonaConfig.simona.powerflow.maxSweepPowerDeviation, - constantData.simonaConfig.simona.powerflow.newtonraphson.epsilon.toVector.sorted, - constantData.simonaConfig.simona.powerflow.newtonraphson.iterations, - constantData.simonaConfig.simona.powerflow.sweepTimeout, - constantData.simonaConfig.simona.powerflow.stopOnFailure, + cfg.powerflow.maxSweepPowerDeviation, + cfg.powerflow.newtonraphson.epsilon.toVector.sorted, + cfg.powerflow.newtonraphson.iterations, + cfg.powerflow.sweepTimeout, + cfg.powerflow.stopOnFailure, + ), + CongestionManagementParams( + cfg.congestionManagement.enable, + cfg.congestionManagement.enableTransformerTapping, + cfg.congestionManagement.enableTopologyChanges, + cfg.congestionManagement.useFlexOptions, + cfg.congestionManagement.maxOptimizationIterations, + cfg.congestionManagement.timeout, ), SimonaActorNaming.actorName(ctx.self), ) @@ -224,6 +242,58 @@ object GridAgent extends DBFSAlgorithm { Behaviors.same } + private[grid] def gotoIdle( + gridAgentBaseData: GridAgentBaseData, + currentTick: Long, + results: Option[PowerFlowResultEvent], + ctx: ActorContext[Request], + )(implicit + constantData: GridAgentConstantData, + buffer: StashBuffer[GridAgent.Request], + ): Behavior[Request] = { + + // notify listener about the results + results.foreach(constantData.notifyListeners) + + // do my cleanup stuff + ctx.log.debug("Doing my cleanup stuff") + + // / clean copy of the gridAgentBaseData + val cleanedGridAgentBaseData = GridAgentBaseData.clean( + gridAgentBaseData, + gridAgentBaseData.superiorGridNodeUuids, + gridAgentBaseData.inferiorGridGates, + ) + + // / inform scheduler that we are done with the whole simulation and request new trigger for next time step + constantData.environmentRefs.scheduler ! Completion( + constantData.activationAdapter, + Some(currentTick + constantData.resolution), + ) + + // return to Idle + buffer.unstashAll(idle(cleanedGridAgentBaseData)) + } + + /** This method uses [[ActorContext.pipeToSelf()]] to send a future message to + * itself. If the future is a [[Success]] the message is send, else a + * [[WrappedFailure]] with the thrown error is send. + * + * @param future + * future message that should be send to the agent after it was processed + * @param ctx + * [[ActorContext]] of the receiving actor + */ + private[grid] def pipeToSelf( + future: Future[GridAgent.Request], + ctx: ActorContext[GridAgent.Request], + ): Unit = { + ctx.pipeToSelf[GridAgent.Request](future) { + case Success(value) => value + case Failure(exception) => WrappedFailure(exception) + } + } + private def failFast( gridAgentInitData: GridAgentInitData, actorName: String, diff --git a/src/main/scala/edu/ie3/simona/agent/grid/GridAgentData.scala b/src/main/scala/edu/ie3/simona/agent/grid/GridAgentData.scala index 636baca646..79f3efe61f 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/GridAgentData.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/GridAgentData.scala @@ -6,19 +6,25 @@ package edu.ie3.simona.agent.grid +import breeze.numerics.sqrt import edu.ie3.datamodel.graph.SubGridGate import edu.ie3.datamodel.models.input.container.{SubGridContainer, ThermalGrid} +import edu.ie3.datamodel.models.result.CongestionResult import edu.ie3.powerflow.model.PowerFlowResult import edu.ie3.powerflow.model.PowerFlowResult.SuccessFullPowerFlowResult.ValidNewtonRaphsonPFResult import edu.ie3.simona.agent.EnvironmentRefs +import edu.ie3.simona.agent.grid.CongestionManagementSupport.Congestions import edu.ie3.simona.agent.grid.GridAgentMessages._ import edu.ie3.simona.agent.grid.ReceivedValuesStore.NodeToReceivedPower import edu.ie3.simona.agent.participant.ParticipantAgent.ParticipantMessage import edu.ie3.simona.config.SimonaConfig import edu.ie3.simona.event.ResultEvent -import edu.ie3.simona.model.grid.{GridModel, RefSystem} +import edu.ie3.simona.event.ResultEvent.PowerFlowResultEvent +import edu.ie3.simona.model.grid.GridModel.GridComponents +import edu.ie3.simona.model.grid.{GridModel, RefSystem, VoltageLimits} import edu.ie3.simona.ontology.messages.Activation import org.apache.pekko.actor.typed.ActorRef +import squants.electro.ElectricPotential import java.time.ZonedDateTime import java.util.UUID @@ -56,6 +62,49 @@ object GridAgentData { } } + final case class AwaitingData[T] private ( + inferiorGridMap: Map[ActorRef[GridAgent.Request], Option[T]] + ) { + + /** Returns true if congestion data from inferior grids is expected and no + * data was received yet. + */ + def notDone: Boolean = + inferiorGridMap.values.exists(_.isEmpty) + + def values: Iterable[T] = inferiorGridMap.values.flatten.toSeq + + def mappedValues: Map[ActorRef[GridAgent.Request], T] = + inferiorGridMap.flatMap { case (ref, option) => + option.map(value => ref -> value) + } + + def update(sender: ActorRef[GridAgent.Request], data: T): AwaitingData[T] = + handleReceivingData(Vector((sender, data))) + + /** Method for updating the data with the received data. + * + * @param receivedData + * data that was received + * @return + * a updated copy of this data + */ + def handleReceivingData( + receivedData: Vector[(ActorRef[GridAgent.Request], T)] + ): AwaitingData[T] = { + val mappedData = receivedData.map(res => res._1 -> Some(res._2)).toMap + copy(inferiorGridMap = inferiorGridMap ++ mappedData) + } + } + + object AwaitingData { + def apply[T]( + inferiorGrids: Seq[ActorRef[GridAgent.Request]] + ): AwaitingData[T] = { + AwaitingData(inferiorGrids.map(ref => ref -> None).toMap) + } + } + /** Data that is send to the [[GridAgent]] directly after startup. It contains * the main information for initialization. This data should include all * [[GridAgent]] individual data, for data that is the same for all @@ -75,6 +124,7 @@ object GridAgentData { thermalIslandGrids: Seq[ThermalGrid], subGridGateToActorRef: Map[SubGridGate, ActorRef[GridAgent.Request]], refSystem: RefSystem, + voltageLimits: VoltageLimits, ) extends GridAgentData with GridAgentDataHelper { override protected val subgridGates: Vector[SubGridGate] = @@ -125,6 +175,7 @@ object GridAgentData { superiorGridNodeUuids: Vector[UUID], inferiorGridGates: Vector[SubGridGate], powerFlowParams: PowerFlowParams, + congestionManagementParams: CongestionManagementParams, actorName: String, ): GridAgentBaseData = { @@ -140,6 +191,7 @@ object GridAgentData { GridAgentBaseData( GridEnvironment(gridModel, subgridGateToActorRef, nodeToAssetAgents), powerFlowParams, + congestionManagementParams, currentSweepNo, ReceivedValuesStore.empty( nodeToAssetAgents, @@ -182,10 +234,10 @@ object GridAgentData { ), currentSweepNo = 0, sweepValueStores = Map.empty[Int, SweepValueStore], + congestionManagementParams = + gridAgentBaseData.congestionManagementParams.clean, ) - } - } /** The base aka default data of a [[GridAgent]]. Contains information on the @@ -208,6 +260,7 @@ object GridAgentData { final case class GridAgentBaseData private ( gridEnv: GridEnvironment, powerFlowParams: PowerFlowParams, + congestionManagementParams: CongestionManagementParams, currentSweepNo: Int, receivedValueStore: ReceivedValuesStore, sweepValueStores: Map[Int, SweepValueStore], @@ -464,4 +517,201 @@ object GridAgentData { } } + final case class CongestionManagementData private ( + gridAgentBaseData: GridAgentBaseData, + currentTick: Long, + powerFlowResults: PowerFlowResultEvent, + congestions: Congestions, + inferiorGrids: Seq[ActorRef[GridAgent.Request]], + ) extends GridAgentData { + + def getCongestionResult(startTime: ZonedDateTime): CongestionResult = { + val gridModel = gridAgentBaseData.gridEnv.gridModel + + val node = powerFlowResults.nodeResults.map(n => + n.getvMag().getValue.doubleValue() + ) + val line = powerFlowResults.lineResults.map(l => + Math.max( + l.getiAMag().getValue.doubleValue(), + l.getiBMag().getValue.doubleValue(), + ) + ) + + new CongestionResult( + startTime.plusSeconds(currentTick), + gridModel.subnetNo, + gridModel.voltageLimits.vMin, + gridModel.voltageLimits.vMax, + congestions.voltageCongestions, + congestions.lineCongestions, + congestions.transformerCongestions, + ) + } + + def inferiorRefs: Set[ActorRef[GridAgent.Request]] = inferiorGrids.toSet + + def cleanAfterTransformerTapping: GridAgentBaseData = { + val params = gridAgentBaseData.congestionManagementParams + val updatedParams = params.copy(hasRunTransformerTapping = true) + + gridAgentBaseData.copy(congestionManagementParams = updatedParams) + } + + def cleanAfterTopologyChange: GridAgentBaseData = { + val params = gridAgentBaseData.congestionManagementParams + + // updating the params to the next iteration + val updatedParams = params.copy( + hasRunTransformerTapping = false, + iteration = params.iteration + 1, + ) + + gridAgentBaseData.copy(congestionManagementParams = updatedParams) + } + + def cleanAfterFlexOptions: GridAgentBaseData = { + val params = gridAgentBaseData.congestionManagementParams + val updatedData = params.copy( + hasUsedFlexOptions = true + ) + + gridAgentBaseData.copy(congestionManagementParams = updatedData) + } + } + + object CongestionManagementData { + def apply( + gridAgentBaseData: GridAgentBaseData, + currentTick: Long, + powerFlowResults: PowerFlowResultEvent, + ): CongestionManagementData = { + val gridModel = gridAgentBaseData.gridEnv.gridModel + + val congestions = findCongestions( + powerFlowResults, + gridModel.gridComponents, + gridModel.voltageLimits, + gridModel.mainRefSystem.nominalVoltage, + gridModel.subnetNo, + ) + + // extracting one inferior ref for all inferior grids + val inferiorGrids = gridAgentBaseData.inferiorGridGates + .map { inferiorGridGate => + gridAgentBaseData.gridEnv.subgridGateToActorRef( + inferiorGridGate + ) -> inferiorGridGate.superiorNode.getUuid + } + .groupMap { + // Group the gates by target actor, so that only one request is sent per grid agent + case (inferiorGridAgentRef, _) => + inferiorGridAgentRef + } { case (_, inferiorGridGates) => + inferiorGridGates + } + .map { case (inferiorGridAgentRef, _) => + inferiorGridAgentRef + } + .toSeq + + CongestionManagementData( + gridAgentBaseData, + currentTick, + powerFlowResults, + congestions, + inferiorGrids, + ) + } + + def empty( + gridAgentBaseData: GridAgentBaseData, + currentTick: Long, + ): CongestionManagementData = apply( + gridAgentBaseData, + currentTick, + PowerFlowResultEvent( + Seq.empty, + Seq.empty, + Seq.empty, + Seq.empty, + Seq.empty, + ), + ) + + private def findCongestions( + powerFlowResults: PowerFlowResultEvent, + gridComponents: GridComponents, + voltageLimits: VoltageLimits, + vNom: ElectricPotential, + subnetNo: Int, + ): Congestions = { + + val nodeRes = + powerFlowResults.nodeResults.map(res => res.getInputModel -> res).toMap + + // filter nodes in subnet + val nodesInSubnet = + gridComponents.nodes.filter(_.subnet == subnetNo).map(_.uuid) + + // checking for voltage congestions + val voltageCongestion = nodeRes.values + .filter(res => nodesInSubnet.contains(res.getInputModel)) + .exists { res => + !voltageLimits.isInLimits(res.getvMag()) + } + + // checking for line congestions + val linesLimits = gridComponents.lines.map { line => + line.uuid -> line + }.toMap + val lineCongestion = powerFlowResults.lineResults.exists { res => + val iA = res.getiAMag().getValue.doubleValue() + val iB = res.getiBMag().getValue.doubleValue() + val iNom = linesLimits(res.getInputModel).iNom.value + + iA > iNom || iB > iNom + } + + // checking for transformer congestions + val transformer2w = gridComponents.transformers.map { transformer => + transformer.uuid -> transformer + }.toMap + val transformer2wCongestion = + powerFlowResults.transformer2wResults.exists { res => + val transformer = transformer2w(res.getInputModel) + + val vMag = nodeRes( + transformer.lvNodeUuid + ).getvMag().getValue.doubleValue() * vNom.toKilovolts + + sqrt(3.0) * res + .getiBMag() + .getValue + .doubleValue() * vMag > transformer.sRated.toKilowatts + } + + val transformer3w = gridComponents.transformers3w.map { transformer => + transformer.uuid -> transformer + }.toMap + val transformer3wCongestion = + powerFlowResults.transformer3wResults.exists { res => + val transformer = transformer3w(res.input) + + val vMag = nodeRes( + transformer.lvNodeUuid + ).getvMag().getValue.doubleValue() * vNom.toKilovolts + + sqrt( + 3.0 + ) * res.currentMagnitude.value * vMag > transformer.sRated.toKilowatts + } + + Congestions( + voltageCongestion, + lineCongestion, + transformer2wCongestion || transformer3wCongestion, + ) + } + } } diff --git a/src/main/scala/edu/ie3/simona/agent/grid/GridAgentMessages.scala b/src/main/scala/edu/ie3/simona/agent/grid/GridAgentMessages.scala index b0d98be1e9..8dd4a67f0a 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/GridAgentMessages.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/GridAgentMessages.scala @@ -6,19 +6,28 @@ package edu.ie3.simona.agent.grid +import edu.ie3.datamodel.models.result.CongestionResult +import edu.ie3.simona.agent.grid.CongestionManagementSupport.{ + CongestionManagementSteps, + Congestions, + VoltageRange, +} import edu.ie3.simona.agent.grid.GridAgentData.GridAgentInitData import edu.ie3.simona.agent.grid.GridAgentMessages.Responses.{ ExchangePower, ExchangeVoltage, } +import edu.ie3.simona.model.grid.TransformerTapping import edu.ie3.simona.ontology.messages.Activation import edu.ie3.simona.scheduler.ScheduleLock.ScheduleKey import edu.ie3.util.scala.quantities.ReactivePower import org.apache.pekko.actor.typed.ActorRef import squants.Power import squants.electro.ElectricPotential +import tech.units.indriya.ComparableQuantity import java.util.UUID +import javax.measure.quantity.Dimensionless /** Defines all messages that can be received by a [[GridAgent]] without the * need for an adapter. @@ -256,4 +265,71 @@ object GridAgentMessages { f: ElectricPotential, ) } + + // DCM messages + + sealed trait CMRequest extends GridAgent.InternalRequest { + def sender: ActorRef[GridAgent.Request] + } + + sealed trait CMReceiveResponse[T] extends GridAgent.InternalReply { + def sender: ActorRef[GridAgent.Request] + def value: T + } + + sealed trait CMResponse[T] extends GridAgent.InternalReply { + def values: Vector[(ActorRef[GridAgent.Request], T)] + } + + // general congestion messages + final case class CongestionCheckRequest( + override val sender: ActorRef[GridAgent.Request] + ) extends CMRequest + + final case class CongestionResponse( + override val sender: ActorRef[GridAgent.Request], + override val value: Congestions, + ) extends CMReceiveResponse[Congestions] + + final case class ReceivedCongestions( + override val values: Vector[(ActorRef[GridAgent.Request], Congestions)] + ) extends CMResponse[Congestions] + + // transformer tapping messages + final case class RequestVoltageOptions( + override val sender: ActorRef[GridAgent.Request], + subnet: Int, + ) extends CMRequest + + final case class VoltageRangeResponse( + override val sender: ActorRef[GridAgent.Request], + override val value: (VoltageRange, Set[TransformerTapping]), + ) extends CMReceiveResponse[(VoltageRange, Set[TransformerTapping])] + + final case class ReceivedVoltageRange( + override val values: Vector[ + (ActorRef[GridAgent.Request], (VoltageRange, Set[TransformerTapping])) + ] + ) extends CMResponse[(VoltageRange, Set[TransformerTapping])] + + final case class VoltageDeltaResponse( + delta: ComparableQuantity[Dimensionless] + ) extends GridAgent.InternalReply + + final case class NextStepRequest( + nextStep: CongestionManagementSteps.Value + ) extends GridAgent.InternalRequest + + /** Message that indicates all actors that the current step is started. + */ + final case object StartStep extends GridAgent.InternalRequest + + /** Message that indicates all actors that the current step is finished. + */ + final case object FinishStep extends GridAgent.InternalRequest + + /** Message that indicates all actors that the next state is the idle state. + */ + final case object GotoIdle extends GridAgent.InternalRequest + } diff --git a/src/main/scala/edu/ie3/simona/agent/grid/GridResultsSupport.scala b/src/main/scala/edu/ie3/simona/agent/grid/GridResultsSupport.scala index c872cc8e54..1557597c85 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/GridResultsSupport.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/GridResultsSupport.scala @@ -607,7 +607,7 @@ object GridResultsSupport { sealed trait PartialTransformer3wResult { val time: ZonedDateTime val input: UUID - protected val currentMagnitude: ElectricCurrent + val currentMagnitude: ElectricCurrent protected val currentAngle: Angle } diff --git a/src/main/scala/edu/ie3/simona/config/ConfigFailFast.scala b/src/main/scala/edu/ie3/simona/config/ConfigFailFast.scala index 3437944c78..d5fb9bd441 100644 --- a/src/main/scala/edu/ie3/simona/config/ConfigFailFast.scala +++ b/src/main/scala/edu/ie3/simona/config/ConfigFailFast.scala @@ -120,6 +120,9 @@ case object ConfigFailFast extends LazyLogging { if (refSystems.isDefined) refSystems.foreach(refsys => checkRefSystem(refsys)) + // check if the provided combinations of voltageLimits provided are valid + simonaConfig.simona.gridConfig.voltageLimits.foreach(checkVoltageLimits) + /* Check all participant model configurations */ checkParticipantRuntimeConfiguration( simonaConfig.simona.runtime.participant @@ -511,6 +514,62 @@ case object ConfigFailFast extends LazyLogging { } } + /** Sanity checks for a [[SimonaConfig.VoltageLimitsConfig]] + * + * @param voltageLimits + * the [[SimonaConfig.VoltageLimitsConfig]] that should be checked + */ + private def checkVoltageLimits( + voltageLimits: List[VoltageLimitsConfig] + ): Unit = { + voltageLimits.foreach { limit => + val voltLvls = + limit.voltLvls.getOrElse(List.empty[SimonaConfig.VoltLvlConfig]) + val gridIds = limit.gridIds.getOrElse(List.empty[String]) + + if (voltLvls.isEmpty && gridIds.isEmpty) + throw new InvalidConfigParameterException( + "The provided values for voltLvls and gridIds are empty! " + + s"At least one of these optional parameters has to be provided for valid voltage limits! " + + s"Provided voltage limits are: $voltageLimits." + ) + + voltLvls.foreach { voltLvl => + Try(Quantities.getQuantity(voltLvl.vNom)) match { + case Success(quantity) => + if (!quantity.getUnit.isCompatible(Units.VOLT)) + throw new InvalidConfigParameterException( + s"The given nominal voltage '${voltLvl.vNom}' cannot be parsed to electrical potential! Please provide the volt level with its unit, e.g. \"20 kV\"" + ) + case Failure(exception) => + throw new InvalidConfigParameterException( + s"The given nominal voltage '${voltLvl.vNom}' cannot be parsed to a quantity. Did you provide the volt level with it's unit (e.g. \"20 kV\")?", + exception, + ) + } + } + + gridIds.foreach { + case gridIdRange @ ConfigConventions.gridIdDotRange(from, to) => + rangeCheck(from.toInt, to.toInt, gridIdRange) + case gridIdRange @ ConfigConventions.gridIdMinusRange(from, to) => + rangeCheck(from.toInt, to.toInt, gridIdRange) + case ConfigConventions.singleGridId(_) => + case gridId => + throw new InvalidConfigParameterException( + s"The provided gridId $gridId is malformed!" + ) + } + + def rangeCheck(from: Int, to: Int, gridIdRange: String): Unit = { + if (from >= to) + throw new InvalidConfigParameterException( + s"Invalid gridId Range $gridIdRange. Start $from cannot be equals or bigger than end $to." + ) + } + } + } + private def checkGridDataSource( gridDataSource: SimonaConfig.Simona.Input.Grid.Datasource ): Unit = { diff --git a/src/main/scala/edu/ie3/simona/config/SimonaConfig.scala b/src/main/scala/edu/ie3/simona/config/SimonaConfig.scala index d8ec6729ef..4ab18f2951 100644 --- a/src/main/scala/edu/ie3/simona/config/SimonaConfig.scala +++ b/src/main/scala/edu/ie3/simona/config/SimonaConfig.scala @@ -278,6 +278,7 @@ object SimonaConfig { } final case class GridOutputConfig( + congestions: scala.Boolean, lines: scala.Boolean, nodes: scala.Boolean, notifier: java.lang.String, @@ -292,6 +293,8 @@ object SimonaConfig { $tsCfgValidator: $TsCfgValidator, ): SimonaConfig.GridOutputConfig = { SimonaConfig.GridOutputConfig( + congestions = + c.hasPathOrNull("congestions") && c.getBoolean("congestions"), lines = c.hasPathOrNull("lines") && c.getBoolean("lines"), nodes = c.hasPathOrNull("nodes") && c.getBoolean("nodes"), notifier = $_reqStr(parentPath, c, "notifier", $tsCfgValidator), @@ -1010,6 +1013,73 @@ object SimonaConfig { } + final case class VoltageLimitsConfig( + gridIds: scala.Option[scala.List[java.lang.String]], + vMax: scala.Double, + vMin: scala.Double, + voltLvls: scala.Option[scala.List[SimonaConfig.VoltLvlConfig]], + ) + object VoltageLimitsConfig { + def apply( + c: com.typesafe.config.Config, + parentPath: java.lang.String, + $tsCfgValidator: $TsCfgValidator, + ): SimonaConfig.VoltageLimitsConfig = { + SimonaConfig.VoltageLimitsConfig( + gridIds = + if (c.hasPathOrNull("gridIds")) + scala.Some( + $_L$_str(c.getList("gridIds"), parentPath, $tsCfgValidator) + ) + else None, + vMax = $_reqDbl(parentPath, c, "vMax", $tsCfgValidator), + vMin = $_reqDbl(parentPath, c, "vMin", $tsCfgValidator), + voltLvls = + if (c.hasPathOrNull("voltLvls")) + scala.Some( + $_LSimonaConfig_VoltLvlConfig( + c.getList("voltLvls"), + parentPath, + $tsCfgValidator, + ) + ) + else None, + ) + } + private def $_LSimonaConfig_VoltLvlConfig( + cl: com.typesafe.config.ConfigList, + parentPath: java.lang.String, + $tsCfgValidator: $TsCfgValidator, + ): scala.List[SimonaConfig.VoltLvlConfig] = { + import scala.jdk.CollectionConverters._ + cl.asScala + .map(cv => + SimonaConfig.VoltLvlConfig( + cv.asInstanceOf[com.typesafe.config.ConfigObject].toConfig, + parentPath, + $tsCfgValidator, + ) + ) + .toList + } + private def $_reqDbl( + parentPath: java.lang.String, + c: com.typesafe.config.Config, + path: java.lang.String, + $tsCfgValidator: $TsCfgValidator, + ): scala.Double = { + if (c == null) 0 + else + try c.getDouble(path) + catch { + case e: com.typesafe.config.ConfigException => + $tsCfgValidator.addBadPath(parentPath + path, e) + 0 + } + } + + } + final case class WecRuntimeConfig( override val calculateMissingReactivePowerWithModel: scala.Boolean, override val scaling: scala.Double, @@ -1071,6 +1141,7 @@ object SimonaConfig { } final case class Simona( + congestionManagement: SimonaConfig.Simona.CongestionManagement, control: scala.Option[SimonaConfig.Simona.Control], event: SimonaConfig.Simona.Event, gridConfig: SimonaConfig.Simona.GridConfig, @@ -1082,6 +1153,42 @@ object SimonaConfig { time: SimonaConfig.Simona.Time, ) object Simona { + final case class CongestionManagement( + enable: scala.Boolean, + enableTopologyChanges: scala.Boolean, + enableTransformerTapping: scala.Boolean, + maxOptimizationIterations: scala.Int, + timeout: java.time.Duration, + useFlexOptions: scala.Boolean, + ) + object CongestionManagement { + def apply( + c: com.typesafe.config.Config, + parentPath: java.lang.String, + $tsCfgValidator: $TsCfgValidator, + ): SimonaConfig.Simona.CongestionManagement = { + SimonaConfig.Simona.CongestionManagement( + enable = c.hasPathOrNull("enable") && c.getBoolean("enable"), + enableTopologyChanges = + c.hasPathOrNull("enableTopologyChanges") && c.getBoolean( + "enableTopologyChanges" + ), + enableTransformerTapping = c.hasPathOrNull( + "enableTransformerTapping" + ) && c.getBoolean("enableTransformerTapping"), + maxOptimizationIterations = + if (c.hasPathOrNull("maxOptimizationIterations")) + c.getInt("maxOptimizationIterations") + else 1, + timeout = + if (c.hasPathOrNull("timeout")) c.getDuration("timeout") + else java.time.Duration.parse("PT30S"), + useFlexOptions = + c.hasPathOrNull("useFlexOptions") && c.getBoolean("useFlexOptions"), + ) + } + } + final case class Control( transformer: scala.List[SimonaConfig.TransformerControlGroup] ) @@ -1203,7 +1310,10 @@ object SimonaConfig { } final case class GridConfig( - refSystems: scala.Option[scala.List[SimonaConfig.RefSystemConfig]] + refSystems: scala.Option[scala.List[SimonaConfig.RefSystemConfig]], + voltageLimits: scala.Option[ + scala.List[SimonaConfig.VoltageLimitsConfig] + ], ) object GridConfig { def apply( @@ -1221,7 +1331,17 @@ object SimonaConfig { $tsCfgValidator, ) ) - else None + else None, + voltageLimits = + if (c.hasPathOrNull("voltageLimits")) + scala.Some( + $_LSimonaConfig_VoltageLimitsConfig( + c.getList("voltageLimits"), + parentPath, + $tsCfgValidator, + ) + ) + else None, ) } private def $_LSimonaConfig_RefSystemConfig( @@ -1240,6 +1360,22 @@ object SimonaConfig { ) .toList } + private def $_LSimonaConfig_VoltageLimitsConfig( + cl: com.typesafe.config.ConfigList, + parentPath: java.lang.String, + $tsCfgValidator: $TsCfgValidator, + ): scala.List[SimonaConfig.VoltageLimitsConfig] = { + import scala.jdk.CollectionConverters._ + cl.asScala + .map(cv => + SimonaConfig.VoltageLimitsConfig( + cv.asInstanceOf[com.typesafe.config.ConfigObject].toConfig, + parentPath, + $tsCfgValidator, + ) + ) + .toList + } } final case class Input( @@ -2905,6 +3041,15 @@ object SimonaConfig { $tsCfgValidator: $TsCfgValidator, ): SimonaConfig.Simona = { SimonaConfig.Simona( + congestionManagement = SimonaConfig.Simona.CongestionManagement( + if (c.hasPathOrNull("congestionManagement")) + c.getConfig("congestionManagement") + else + com.typesafe.config.ConfigFactory + .parseString("congestionManagement{}"), + parentPath + "congestionManagement.", + $tsCfgValidator, + ), control = if (c.hasPathOrNull("control")) scala.Some( diff --git a/src/main/scala/edu/ie3/simona/config/VoltageLimitsParser.scala b/src/main/scala/edu/ie3/simona/config/VoltageLimitsParser.scala new file mode 100644 index 0000000000..ed4ccdaeec --- /dev/null +++ b/src/main/scala/edu/ie3/simona/config/VoltageLimitsParser.scala @@ -0,0 +1,122 @@ +/* + * © 2024. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.config + +import edu.ie3.datamodel.models.voltagelevels.{ + GermanVoltageLevelUtils, + VoltageLevel, +} +import edu.ie3.simona.exceptions.InvalidConfigParameterException +import edu.ie3.simona.model.grid.VoltageLimits +import edu.ie3.simona.util.CollectionUtils + +object VoltageLimitsParser { + + final case class ConfigVoltageLimits( + private val gridIdVoltageLimits: Map[Int, VoltageLimits], + private val voltLvLVoltageLimits: Map[VoltageLevel, VoltageLimits], + ) { + def find( + gridId: Int, + voltLvl: Option[VoltageLevel] = None, + ): Option[VoltageLimits] = gridIdVoltageLimits + .get(gridId) + .orElse(voltLvl.flatMap(voltLvLVoltageLimits.get)) + } + + def parse( + configVoltageLimits: Option[List[SimonaConfig.VoltageLimitsConfig]] + ): ConfigVoltageLimits = { + val distributionVoltageLimits = VoltageLimits(0.9, 1.1) + + val defaultVoltageLimits = ConfigVoltageLimits( + Map.empty, + Map( + GermanVoltageLevelUtils.LV -> distributionVoltageLimits, + GermanVoltageLevelUtils.MV_10KV -> distributionVoltageLimits, + GermanVoltageLevelUtils.MV_20KV -> distributionVoltageLimits, + GermanVoltageLevelUtils.MV_30KV -> distributionVoltageLimits, + GermanVoltageLevelUtils.HV -> distributionVoltageLimits, + GermanVoltageLevelUtils.EHV_220KV -> VoltageLimits(0.9, 1.118), + GermanVoltageLevelUtils.EHV_380KV -> VoltageLimits(0.9, 1.05), + ), + ) + + configVoltageLimits match { + case Some(voltageLimits) if voltageLimits.nonEmpty => + val parsedVoltageLimits = voltageLimits.flatMap { configVoltageLimit => + val voltageLimits = + VoltageLimits(configVoltageLimit.vMin, configVoltageLimit.vMax) + + configVoltageLimit.gridIds.getOrElse(Seq.empty).flatMap { + case ConfigConventions.gridIdDotRange(from, to) => + from.toInt to to.toInt + case ConfigConventions.gridIdMinusRange(from, to) => + from.toInt to to.toInt + case ConfigConventions.singleGridId(singleGridId) => + Seq(singleGridId.toInt) + case unknownGridIdFormat => + throw new InvalidConfigParameterException( + s"Unknown gridId format $unknownGridIdFormat provided for voltage limits $configVoltageLimit" + ) + } ++ configVoltageLimit.voltLvls.getOrElse(Seq.empty).map { + voltLvlDef => + (VoltLvlParser.from(voltLvlDef), voltageLimits) + } + } + + val gridIdVoltageLimitsList: List[(Int, VoltageLimits)] = + parsedVoltageLimits.flatMap { + case (gridId: Int, refSystems) => + refSystems match { + case voltageLimits: VoltageLimits => + Some(gridId -> voltageLimits) + case _ => None + } + case _ => None + } + + val gridIdVoltageLimits: Map[Int, VoltageLimits] = + gridIdVoltageLimitsList.toMap + + if (CollectionUtils.listHasDuplicates(gridIdVoltageLimitsList)) { + throw new InvalidConfigParameterException( + s"The provided gridIds in simona.gridConfig.voltageLimits contains duplicates. " + + s"Please check if there are either duplicate entries or overlapping ranges!" + ) + } + + val voltLvLVoltageLimitsList: List[(VoltageLevel, VoltageLimits)] = + parsedVoltageLimits.flatMap { + case (voltLvl: VoltageLevel, refSystems) => + refSystems match { + case voltageLimits: VoltageLimits => + Some(voltLvl -> voltageLimits) + case _ => None + } + case _ => None + } + + if (CollectionUtils.listHasDuplicates(voltLvLVoltageLimitsList)) + throw new InvalidConfigParameterException( + s"The provided voltLvls in simona.gridConfig.voltageLimits contains duplicates. " + + s"Please check your configuration for duplicates in voltLvl entries!" + ) + + val voltLvLVoltageLimits: Map[VoltageLevel, VoltageLimits] = + parsedVoltageLimits.collect { + case (voltLvl: VoltageLevel, values: VoltageLimits) => + (voltLvl, values) + }.toMap + + ConfigVoltageLimits(gridIdVoltageLimits, voltLvLVoltageLimits) + + case _ => defaultVoltageLimits + } + } + +} diff --git a/src/main/scala/edu/ie3/simona/event/ResultEvent.scala b/src/main/scala/edu/ie3/simona/event/ResultEvent.scala index d81242c608..3a2465245c 100644 --- a/src/main/scala/edu/ie3/simona/event/ResultEvent.scala +++ b/src/main/scala/edu/ie3/simona/event/ResultEvent.scala @@ -6,7 +6,7 @@ package edu.ie3.simona.event -import edu.ie3.datamodel.models.result.NodeResult +import edu.ie3.datamodel.models.result.{CongestionResult, NodeResult} import edu.ie3.datamodel.models.result.connector.{ LineResult, SwitchResult, @@ -59,6 +59,8 @@ object ResultEvent { * the power flow results for two winding transformers * @param transformer3wResults * the partial power flow results for three winding transformers + * @param congestionResults + * the congestion found by the congestion managements (default: empty) */ final case class PowerFlowResultEvent( nodeResults: Iterable[NodeResult], @@ -66,6 +68,7 @@ object ResultEvent { lineResults: Iterable[LineResult], transformer2wResults: Iterable[Transformer2WResult], transformer3wResults: Iterable[PartialTransformer3wResult], + congestionResults: Iterable[CongestionResult] = Iterable.empty, ) extends ResultEvent /** Event that holds the flexibility options result of a diff --git a/src/main/scala/edu/ie3/simona/event/listener/ResultEventListener.scala b/src/main/scala/edu/ie3/simona/event/listener/ResultEventListener.scala index d5acf17912..d460f63b8c 100644 --- a/src/main/scala/edu/ie3/simona/event/listener/ResultEventListener.scala +++ b/src/main/scala/edu/ie3/simona/event/listener/ResultEventListener.scala @@ -291,10 +291,11 @@ object ResultEventListener extends Transformer3wResultSupport { lineResults, transformer2wResults, transformer3wResults, + congestionResults, ), ) => val updatedBaseData = - (nodeResults ++ switchResults ++ lineResults ++ transformer2wResults ++ transformer3wResults) + (nodeResults ++ switchResults ++ lineResults ++ transformer2wResults ++ transformer3wResults ++ congestionResults) .foldLeft(baseData) { case (currentBaseData, resultEntity: ResultEntity) => handleResult(resultEntity, currentBaseData, ctx.log) diff --git a/src/main/scala/edu/ie3/simona/exceptions/ResultException.scala b/src/main/scala/edu/ie3/simona/exceptions/ResultException.scala new file mode 100644 index 0000000000..472025bee1 --- /dev/null +++ b/src/main/scala/edu/ie3/simona/exceptions/ResultException.scala @@ -0,0 +1,9 @@ +/* + * © 2024. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.exceptions + +class ResultException(message: String) extends Exception(message) {} diff --git a/src/main/scala/edu/ie3/simona/model/grid/GridModel.scala b/src/main/scala/edu/ie3/simona/model/grid/GridModel.scala index af90cfc344..e72350f216 100644 --- a/src/main/scala/edu/ie3/simona/model/grid/GridModel.scala +++ b/src/main/scala/edu/ie3/simona/model/grid/GridModel.scala @@ -39,6 +39,7 @@ final case class GridModel( subnetNo: Int, mainRefSystem: RefSystem, gridComponents: GridComponents, + voltageLimits: VoltageLimits, gridControls: GridControls, ) { @@ -66,12 +67,14 @@ object GridModel { def apply( subGridContainer: SubGridContainer, refSystem: RefSystem, + voltageLimits: VoltageLimits, startDate: ZonedDateTime, endDate: ZonedDateTime, simonaConfig: SimonaConfig, ): GridModel = buildAndValidate( subGridContainer, refSystem, + voltageLimits, startDate, endDate, simonaConfig, @@ -500,6 +503,7 @@ object GridModel { private def buildAndValidate( subGridContainer: SubGridContainer, refSystem: RefSystem, + voltageLimits: VoltageLimits, startDate: ZonedDateTime, endDate: ZonedDateTime, simonaConfig: SimonaConfig, @@ -600,6 +604,7 @@ object GridModel { subGridContainer.getSubnet, refSystem, gridComponents, + voltageLimits, GridControls(transformerControlGroups), ) diff --git a/src/main/scala/edu/ie3/simona/model/grid/NodeModel.scala b/src/main/scala/edu/ie3/simona/model/grid/NodeModel.scala index 506ea804a1..7a96e155ab 100644 --- a/src/main/scala/edu/ie3/simona/model/grid/NodeModel.scala +++ b/src/main/scala/edu/ie3/simona/model/grid/NodeModel.scala @@ -40,6 +40,7 @@ final case class NodeModel( isSlack: Boolean, vTarget: squants.Dimensionless, voltLvl: VoltageLevel, + subnet: Int, ) extends SystemComponent( uuid, id, @@ -70,6 +71,7 @@ case object NodeModel { nodeInput.isSlack, Each(nodeInput.getvTarget.to(PowerSystemUnits.PU).getValue.doubleValue()), nodeInput.getVoltLvl, + nodeInput.getSubnet, ) /* Checks, if the participant is in operation right from the start */ diff --git a/src/main/scala/edu/ie3/simona/model/grid/Transformer3wModel.scala b/src/main/scala/edu/ie3/simona/model/grid/Transformer3wModel.scala index 8f61c2a897..c7e34ac09d 100644 --- a/src/main/scala/edu/ie3/simona/model/grid/Transformer3wModel.scala +++ b/src/main/scala/edu/ie3/simona/model/grid/Transformer3wModel.scala @@ -27,8 +27,9 @@ import edu.ie3.simona.model.grid.Transformer3wPowerFlowCase.{ import edu.ie3.simona.util.SimonaConstants import edu.ie3.util.quantities.PowerSystemUnits._ import edu.ie3.util.scala.OperationInterval +import squants.Power import squants.electro.{Kilovolts, Ohms, Siemens} -import squants.energy.Megawatts +import squants.energy.{Kilowatts, Megawatts, Watts} import tech.units.indriya.AbstractUnit import tech.units.indriya.quantity.Quantities import tech.units.indriya.unit.Units.{OHM, SIEMENS} @@ -66,6 +67,9 @@ import scala.math.BigDecimal.RoundingMode * number of parallel transformers * @param powerFlowCase * the [[Transformer3wPowerFlowCase]] + * @param sRated + * the rated power at the port that is defined by the + * [[Transformer3wPowerFlowCase]] * @param r * resistance r, real part of the transformer impedance z (referenced to the * nominal impedance of the grid) in p.u. @@ -91,6 +95,7 @@ final case class Transformer3wModel( override protected val transformerTappingModel: TransformerTappingModel, amount: Int, powerFlowCase: Transformer3wPowerFlowCase, + sRated: Power, protected val r: squants.Dimensionless, protected val x: squants.Dimensionless, protected val g: squants.Dimensionless, @@ -258,6 +263,21 @@ case object Transformer3wModel extends LazyLogging { .setScale(5, RoundingMode.HALF_UP) } + val sRated = powerFlowCase match { + case PowerFlowCaseA => + Watts( + trafo3wType.getsRatedA().to(VOLTAMPERE).getValue.doubleValue() + ) + case PowerFlowCaseB => + Watts( + trafo3wType.getsRatedB().to(VOLTAMPERE).getValue.doubleValue() + ) + case PowerFlowCaseC => + Watts( + trafo3wType.getsRatedC().to(VOLTAMPERE).getValue.doubleValue() + ) + } + val operationInterval = SystemComponent.determineOperationInterval( startDate, @@ -277,6 +297,7 @@ case object Transformer3wModel extends LazyLogging { transformerTappingModel, transformer3wInput.getParallelDevices, powerFlowCase, + sRated, r, x, g, diff --git a/src/main/scala/edu/ie3/simona/model/grid/TransformerModel.scala b/src/main/scala/edu/ie3/simona/model/grid/TransformerModel.scala index ee03ef837a..aef6c8abdc 100644 --- a/src/main/scala/edu/ie3/simona/model/grid/TransformerModel.scala +++ b/src/main/scala/edu/ie3/simona/model/grid/TransformerModel.scala @@ -17,9 +17,9 @@ import edu.ie3.simona.model.SystemComponent import edu.ie3.simona.util.SimonaConstants import edu.ie3.util.quantities.PowerSystemUnits._ import edu.ie3.util.scala.OperationInterval -import squants.Each +import squants.{Each, Power} import squants.electro.{Kilovolts, Ohms, Siemens} -import squants.energy.Watts +import squants.energy.{Kilowatts, Watts} import tech.units.indriya.unit.Units._ import java.time.ZonedDateTime @@ -50,6 +50,8 @@ import scala.math.BigDecimal.RoundingMode * nominal current on the high voltage side of the transformer * @param iNomLv * nominal current on the low voltage side of the transformer + * @param sRated + * the rated power of the transformer * @param r * resistance r, real part of the transformer impedance z (referenced to the * nominal impedance of the grid) in p.u. @@ -74,6 +76,7 @@ final case class TransformerModel( voltRatioNominal: BigDecimal, iNomHv: squants.electro.ElectricCurrent, iNomLv: squants.electro.ElectricCurrent, + sRated: Power, protected val r: squants.Dimensionless, protected val x: squants.Dimensionless, protected val g: squants.Dimensionless, @@ -237,6 +240,9 @@ case object TransformerModel { voltRatioNominal, iNomHv, iNomLv, + Kilowatts( + trafoType.getsRated().to(KILOVOLTAMPERE).getValue.doubleValue() + ), r, x, g, 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 809e7c5f8f..7b82483660 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.RichQuantityDouble +import tech.units.indriya.ComparableQuantity + import javax.measure.Quantity import javax.measure.quantity.Dimensionless import tech.units.indriya.quantity.Quantities @@ -26,8 +30,21 @@ trait TransformerTapping { protected var tapRatio: Double = _ + /** Returns [[TransformerTappingModel.autoTap]]. + */ + def hasAutoTap: Boolean = transformerTappingModel.autoTap + + def tapMax: Int = transformerTappingModel.tapMax + + def tapMin: Int = transformerTappingModel.tapMin + + def deltaV: ComparableQuantity[Dimensionless] = + transformerTappingModel.deltaV.getValue.doubleValue().asPu + def currentTapPos: Int = transformerTappingModel.currentTapPos + def getTapSide: ConnectorPort = transformerTappingModel.tapSide + /** Initialize the tapping model. Should be called after creating the * implementing model */ @@ -57,12 +74,15 @@ 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()]] * * @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 @@ -72,8 +92,98 @@ trait TransformerTapping { */ def computeDeltaTap( vChangeRequest: Quantity[Dimensionless], + tapSide: ConnectorPort = ConnectorPort.A, + deadBand: Quantity[Dimensionless] = Quantities.getQuantity(0.75, PU), + ): Int = { + if (tapSide == transformerTappingModel.tapSide) { + transformerTappingModel.computeDeltaTap(vChangeRequest, deadBand) + } else { + transformerTappingModel.computeDeltaTap( + vChangeRequest.multiply(-1), + deadBand, + ) + } + } + + /** Determines all possible voltage deltas that can be achieved by tapping. + * @param tapSide + * side of the tapping + * @return + * a list of possible voltage deltas + */ + def possibleDeltas( + maxIncrease: ComparableQuantity[Dimensionless], + maxDecrease: ComparableQuantity[Dimensionless], + tapSide: ConnectorPort = ConnectorPort.A, + ): List[ComparableQuantity[Dimensionless]] = { + if (hasAutoTap) { + val plus = tapMax - currentTapPos + val minus = tapMin - currentTapPos + + val range = + Range.inclusive(minus, plus).map(deltaV.multiply(_).divide(100)).toList + + val values = if (tapSide == transformerTappingModel.tapSide) { + range + } else { + range.map(_.multiply(-1)).sortBy(_.getValue.doubleValue()) + } + + if (maxIncrease.isLessThan(0.asPu)) { + values.filter(value => + value.isLessThanOrEqualTo(0.asPu) && value.isGreaterThanOrEqualTo( + maxDecrease + ) + ) + } else if (maxDecrease.isGreaterThan(0.asPu)) { + values.filter(value => + value.isLessThanOrEqualTo(maxIncrease) && value + .isGreaterThanOrEqualTo(0.asPu) + ) + } else { + values.filter(value => + value.isLessThanOrEqualTo(maxIncrease) && value + .isGreaterThanOrEqualTo( + maxDecrease + ) + ) + } + } else List(0.asPu) + } + + /** 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. + * + * @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 (tapSide == transformerTappingModel.tapSide) { + (taps, deltaV.asPu) + } else { + (taps, deltaV.asPu.multiply(-1)) + } + } } diff --git a/src/main/scala/edu/ie3/simona/model/grid/VoltageLimits.scala b/src/main/scala/edu/ie3/simona/model/grid/VoltageLimits.scala new file mode 100644 index 0000000000..dc90679f46 --- /dev/null +++ b/src/main/scala/edu/ie3/simona/model/grid/VoltageLimits.scala @@ -0,0 +1,27 @@ +/* + * © 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.util.quantities.QuantityUtils.RichQuantityDouble +import tech.units.indriya.ComparableQuantity + +import javax.measure.quantity.Dimensionless + +case class VoltageLimits( + vMin: ComparableQuantity[Dimensionless], + vMax: ComparableQuantity[Dimensionless], +) { + def isInLimits(voltage: ComparableQuantity[Dimensionless]): Boolean = + vMin.isLessThanOrEqualTo(voltage) && voltage.isLessThanOrEqualTo(vMax) +} + +object VoltageLimits { + def apply( + vMin: Double, + vMax: Double, + ): VoltageLimits = VoltageLimits(vMin.asPu, vMax.asPu) +} diff --git a/src/main/scala/edu/ie3/simona/sim/setup/SetupHelper.scala b/src/main/scala/edu/ie3/simona/sim/setup/SetupHelper.scala index 535b140798..1d30da36c7 100644 --- a/src/main/scala/edu/ie3/simona/sim/setup/SetupHelper.scala +++ b/src/main/scala/edu/ie3/simona/sim/setup/SetupHelper.scala @@ -17,10 +17,11 @@ import edu.ie3.simona.agent.grid.GridAgent import edu.ie3.simona.agent.grid.GridAgentData.GridAgentInitData import edu.ie3.simona.config.RefSystemParser.ConfigRefSystems import edu.ie3.simona.config.SimonaConfig +import edu.ie3.simona.config.VoltageLimitsParser.ConfigVoltageLimits import edu.ie3.simona.exceptions.InitializationException import edu.ie3.simona.exceptions.agent.GridAgentInitializationException import edu.ie3.simona.io.result.ResultSinkType -import edu.ie3.simona.model.grid.RefSystem +import edu.ie3.simona.model.grid.{RefSystem, VoltageLimits} import edu.ie3.simona.util.ConfigUtil.{GridOutputConfigUtil, OutputConfigUtil} import edu.ie3.simona.util.ResultFileHierarchy.ResultEntityPathConfig import edu.ie3.simona.util.{EntityMapperUtil, ResultFileHierarchy} @@ -61,6 +62,7 @@ trait SetupHelper extends LazyLogging { subGridToActorRef: Map[Int, ActorRef[GridAgent.Request]], gridGates: Set[SubGridGate], configRefSystems: ConfigRefSystems, + configVoltageLimits: ConfigVoltageLimits, thermalGrids: Seq[ThermalGrid], ): GridAgentInitData = { val subGridGateToActorRef = buildGateToActorRef( @@ -73,6 +75,8 @@ trait SetupHelper extends LazyLogging { val refSystem = getRefSystem(configRefSystems, subGridContainer) + val voltageLimits = getVoltageLimits(configVoltageLimits, subGridContainer) + /* Prepare the subgrid container for the agents by adapting the transformer high voltage nodes to be slacks */ val updatedSubGridContainer = ContainerUtils.withTrafoNodeAsSlack(subGridContainer) @@ -83,6 +87,7 @@ trait SetupHelper extends LazyLogging { thermalGrids, subGridGateToActorRef, refSystem, + voltageLimits, ) } @@ -199,6 +204,21 @@ trait SetupHelper extends LazyLogging { refSystem } + def getVoltageLimits( + configVoltageLimits: ConfigVoltageLimits, + subGridContainer: SubGridContainer, + ): VoltageLimits = configVoltageLimits + .find( + subGridContainer.getSubnet, + Some(subGridContainer.getPredominantVoltageLevel), + ) + .getOrElse( + throw new InitializationException( + s"Unable to determine voltage limits for grid with id ${subGridContainer.getSubnet} @ " + + s"volt level ${subGridContainer.getPredominantVoltageLevel}. Please either provide voltage limits for the grid id or the whole volt level!" + ) + ) + /** Build the result file hierarchy based on the provided configuration file. * The provided type safe config must be able to be parsed as * [[SimonaConfig]], otherwise an exception is thrown diff --git a/src/main/scala/edu/ie3/simona/sim/setup/SimonaStandaloneSetup.scala b/src/main/scala/edu/ie3/simona/sim/setup/SimonaStandaloneSetup.scala index 65d47863cf..33563dce3e 100644 --- a/src/main/scala/edu/ie3/simona/sim/setup/SimonaStandaloneSetup.scala +++ b/src/main/scala/edu/ie3/simona/sim/setup/SimonaStandaloneSetup.scala @@ -19,7 +19,12 @@ import edu.ie3.simona.api.ExtSimAdapter import edu.ie3.simona.api.data.ExtData import edu.ie3.simona.api.data.ev.{ExtEvData, ExtEvSimulation} import edu.ie3.simona.api.simulation.ExtSimAdapterData -import edu.ie3.simona.config.{ArgsParser, RefSystemParser, SimonaConfig} +import edu.ie3.simona.config.{ + ArgsParser, + RefSystemParser, + SimonaConfig, + VoltageLimitsParser, +} import edu.ie3.simona.event.listener.{ResultEventListener, RuntimeEventListener} import edu.ie3.simona.event.{ResultEvent, RuntimeEvent} import edu.ie3.simona.exceptions.agent.GridAgentInitializationException @@ -91,6 +96,9 @@ class SimonaStandaloneSetup( val configRefSystems = RefSystemParser.parse(simonaConfig.simona.gridConfig.refSystems) + val configVoltageLimits = + VoltageLimitsParser.parse(simonaConfig.simona.gridConfig.voltageLimits) + /* Create all agents and map the sub grid id to their actor references */ val subGridToActorRefMap = buildSubGridToActorRefMap( subGridTopologyGraph, @@ -136,6 +144,7 @@ class SimonaStandaloneSetup( subGridToActorRefMap, subGridGates, configRefSystems, + configVoltageLimits, thermalGrids, ) diff --git a/src/main/scala/edu/ie3/simona/util/ConfigUtil.scala b/src/main/scala/edu/ie3/simona/util/ConfigUtil.scala index f32b66ed5f..2fbf4e117f 100644 --- a/src/main/scala/edu/ie3/simona/util/ConfigUtil.scala +++ b/src/main/scala/edu/ie3/simona/util/ConfigUtil.scala @@ -18,7 +18,11 @@ import edu.ie3.datamodel.models.result.connector.{ Transformer2WResult, Transformer3WResult, } -import edu.ie3.datamodel.models.result.{NodeResult, ResultEntity} +import edu.ie3.datamodel.models.result.{ + CongestionResult, + NodeResult, + ResultEntity, +} import edu.ie3.simona.config.SimonaConfig import edu.ie3.simona.config.SimonaConfig._ import edu.ie3.simona.event.notifier.{Notifier, NotifierConfig} @@ -272,6 +276,8 @@ object ConfigUtil { entities += classOf[Transformer2WResult] if (subConfig.transformers3w) entities += classOf[Transformer3WResult] + if (subConfig.congestions) + entities += classOf[CongestionResult] entities.toSet } diff --git a/src/test/scala/edu/ie3/simona/agent/grid/CongestionManagementSupportSpec.scala b/src/test/scala/edu/ie3/simona/agent/grid/CongestionManagementSupportSpec.scala new file mode 100644 index 0000000000..96a05f33a4 --- /dev/null +++ b/src/test/scala/edu/ie3/simona/agent/grid/CongestionManagementSupportSpec.scala @@ -0,0 +1,675 @@ +/* + * © 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.result.NodeResult +import edu.ie3.datamodel.models.result.connector.LineResult +import edu.ie3.simona.agent.grid.CongestionManagementSupport.{ + TappingGroup, + VoltageRange, +} +import edu.ie3.simona.event.ResultEvent.PowerFlowResultEvent +import edu.ie3.simona.model.grid.GridModel.GridComponents +import edu.ie3.simona.model.grid.{TransformerTapping, VoltageLimits} +import edu.ie3.simona.test.common.UnitSpec +import edu.ie3.simona.test.common.model.grid.{ + GridComponentsMokka, + SubGridGateMokka, +} +import edu.ie3.simona.test.common.result.ResultMokka +import edu.ie3.util.quantities.QuantityUtils.RichQuantityDouble +import org.apache.pekko.actor.testkit.typed.scaladsl.{ + ScalaTestWithActorTestKit, + TestProbe, +} +import org.apache.pekko.actor.typed.ActorRef + +class CongestionManagementSupportSpec + extends ScalaTestWithActorTestKit + with UnitSpec + with GridComponentsMokka + with ResultMokka + with SubGridGateMokka + with CongestionManagementSupport { + + val voltageTolerance = 1e-3 + + val inferior1: TestProbe[GridAgent.Request] = + TestProbe[GridAgent.Request]("inferior1") + val inferior2: TestProbe[GridAgent.Request] = + TestProbe[GridAgent.Request]("inferior2") + + "CongestionManagementSupport" 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() + + val ref4 = TestProbe[GridAgent.Request]("ref4").ref + val transformer4a = mockTransformerModel() + 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 = groupTappingModels( + 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( + TappingGroup(Set(ref1, ref2), Set(transformer1, transformer3wA)), + TappingGroup(Set(ref3), Set(transformer3)), + TappingGroup(Set(ref4), Set(transformer4a, transformer4b)), + ) + } + + "calculate the tap and voltage change for one transformer" in { + val tappingModel = dummyTappingModel() + val tapping = dummyTransformerModel(tappingModel) + + 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) = + calculateTapAndVoltage(range, Seq(tapping)) + + 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 = dummyTransformerModel(tappingModel1) + val transformer12 = dummyTransformerModel(tappingModel1) + + val transformer21 = dummyTransformerModel(tappingModel2) + val transformer22 = dummyTransformer3wModel(tappingModel2) + + val transformer31 = dummyTransformerModel(tappingModel1) + val transformer32 = dummyTransformer3wModel(tappingModel2) + + val modelCase1 = Seq(transformer11, transformer12) + val modelCase2 = Seq(transformer21, transformer22) + val modelCase3 = Seq(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 (tapChanges, delta) = calculateTapAndVoltage(range, models) + + tapChanges shouldBe expectedTaps + delta should equalWithTolerance(expectedDelta) + } + + } + + "calculate the common delta correctly" in { + + val cases = Table( + ("suggestion", "possibleDeltas", "expected"), + (0.015.asPu, Seq(List(0.03.asPu, 0.015.asPu, 0.asPu)), 0.015.asPu), + ( + 0.012.asPu, + Seq(List(0.03.asPu, 0.02.asPu, 0.01.asPu, 0.asPu)), + 0.01.asPu, + ), + (0.006.asPu, Seq(List(0.03.asPu, 0.015.asPu, 0.asPu)), 0.asPu), + ( + 0.03.asPu, + Seq( + 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, + Seq(List(0.06.asPu, 0.03.asPu), List(0.03.asPu, 0.015.asPu)), + 0.03.asPu, + ), + ( + 0.035.asPu, + Seq( + 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, + Seq(List(0.06.asPu, 0.03.asPu), List(0.03.asPu, 0.015.asPu)), + 0.03.asPu, + ), + ( + 0.06.asPu, + Seq(List(0.06.asPu, 0.03.asPu), List(0.03.asPu, 0.015.asPu)), + 0.asPu, + ), + ( + (-0.02).asPu, + Seq(List(0.06.asPu, 0.03.asPu), List(0.03.asPu, 0.015.asPu)), + 0.asPu, + ), + ) + + forAll(cases) { (suggestion, possibleDeltas, expected) => + val delta = findCommonDelta(suggestion, possibleDeltas) + + delta 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 + + calculatePossibleVoltageDeltaForLines( + nodeResults, + results.lineResults, + gridComponents, + ) should equalWithTolerance(deltaV, 1e-3) + } + } + + "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 = calculatePossibleVoltageRange( + 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 = mockTransformerTappingModel( + autoTap = true, + currentTapPos = 0, + 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 = calculatePossibleVoltageRange( + 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) + } + + def buildPowerFlowResultEvent( + nodeResults: Set[NodeResult], + lineResults: Set[LineResult], + ): PowerFlowResultEvent = { + PowerFlowResultEvent( + nodeResults, + Set.empty, + lineResults, + Set.empty, + Set.empty, + ) + } + + } + + "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) + } + } + + "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 = + mockTransformerTappingModel( + autoTap = false, + currentTapPos = 0, + 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 = mockTransformerTappingModel( + 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) + } + } + } +} diff --git a/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmCenGridSpec.scala b/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmCenGridSpec.scala index 4da922de62..50082d6ac6 100644 --- a/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmCenGridSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmCenGridSpec.scala @@ -16,7 +16,7 @@ import edu.ie3.simona.agent.grid.GridAgentMessages.Responses.{ import edu.ie3.simona.agent.grid.GridAgentMessages._ import edu.ie3.simona.event.ResultEvent.PowerFlowResultEvent import edu.ie3.simona.event.{ResultEvent, RuntimeEvent} -import edu.ie3.simona.model.grid.RefSystem +import edu.ie3.simona.model.grid.{RefSystem, VoltageLimits} import edu.ie3.simona.ontology.messages.SchedulerMessage.{ Completion, ScheduleActivation, @@ -116,6 +116,7 @@ class DBFSAlgorithmCenGridSpec Seq.empty[ThermalGrid], subGridGateToActorRef, RefSystem("2000 MVA", "110 kV"), + VoltageLimits(0.9, 1.1), ) val key = ScheduleLock.singleKey(TSpawner, scheduler.ref, INIT_SIM_TICK) diff --git a/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmFailedPowerFlowSpec.scala b/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmFailedPowerFlowSpec.scala index ce904d0b11..a6ad983dc8 100644 --- a/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmFailedPowerFlowSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmFailedPowerFlowSpec.scala @@ -15,7 +15,7 @@ import edu.ie3.simona.agent.grid.GridAgentMessages.Responses.{ } import edu.ie3.simona.agent.grid.GridAgentMessages._ import edu.ie3.simona.event.{ResultEvent, RuntimeEvent} -import edu.ie3.simona.model.grid.RefSystem +import edu.ie3.simona.model.grid.{RefSystem, VoltageLimits} import edu.ie3.simona.ontology.messages.SchedulerMessage.{ Completion, ScheduleActivation, @@ -100,6 +100,7 @@ class DBFSAlgorithmFailedPowerFlowSpec Seq.empty[ThermalGrid], subGridGateToActorRef, RefSystem("2000 MVA", "110 kV"), + VoltageLimits(0.9, 1.1), ) val key = @@ -312,6 +313,7 @@ class DBFSAlgorithmFailedPowerFlowSpec Seq.empty[ThermalGrid], subnetGatesToActorRef, RefSystem("5000 MVA", "380 kV"), + VoltageLimits(0.9, 1.1), ) val key = diff --git a/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmParticipantSpec.scala b/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmParticipantSpec.scala index fa43ee2d1e..11d674b401 100644 --- a/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmParticipantSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmParticipantSpec.scala @@ -15,7 +15,7 @@ import edu.ie3.simona.agent.grid.GridAgentMessages.Responses.{ } import edu.ie3.simona.agent.grid.GridAgentMessages._ import edu.ie3.simona.event.{ResultEvent, RuntimeEvent} -import edu.ie3.simona.model.grid.RefSystem +import edu.ie3.simona.model.grid.{RefSystem, VoltageLimits} import edu.ie3.simona.ontology.messages.SchedulerMessage.{ Completion, ScheduleActivation, @@ -92,6 +92,7 @@ class DBFSAlgorithmParticipantSpec Seq.empty, subGridGateToActorRef, RefSystem("2000 MVA", "110 kV"), + VoltageLimits(0.9, 1.1), ) val key = diff --git a/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmSupGridSpec.scala b/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmSupGridSpec.scala index 9c716def88..b33489d215 100644 --- a/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmSupGridSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmSupGridSpec.scala @@ -14,7 +14,7 @@ import edu.ie3.simona.agent.grid.GridAgentMessages.Responses.ExchangePower import edu.ie3.simona.agent.grid.GridAgentMessages._ import edu.ie3.simona.event.ResultEvent.PowerFlowResultEvent import edu.ie3.simona.event.{ResultEvent, RuntimeEvent} -import edu.ie3.simona.model.grid.RefSystem +import edu.ie3.simona.model.grid.{RefSystem, VoltageLimits} import edu.ie3.simona.ontology.messages.SchedulerMessage.{ Completion, ScheduleActivation, @@ -87,6 +87,7 @@ class DBFSAlgorithmSupGridSpec Seq.empty[ThermalGrid], subnetGatesToActorRef, RefSystem("5000 MVA", "380 kV"), + VoltageLimits(0.9, 1.1), ) val key = diff --git a/src/test/scala/edu/ie3/simona/agent/grid/DBFSMockGridAgents.scala b/src/test/scala/edu/ie3/simona/agent/grid/DBFSMockGridAgents.scala index c1f5a1e885..fba240bf67 100644 --- a/src/test/scala/edu/ie3/simona/agent/grid/DBFSMockGridAgents.scala +++ b/src/test/scala/edu/ie3/simona/agent/grid/DBFSMockGridAgents.scala @@ -6,11 +6,16 @@ package edu.ie3.simona.agent.grid +import edu.ie3.simona.agent.grid.CongestionManagementSupport.{ + Congestions, + VoltageRange, +} import edu.ie3.simona.agent.grid.GridAgentMessages.Responses.{ ExchangePower, ExchangeVoltage, } import edu.ie3.simona.agent.grid.GridAgentMessages._ +import edu.ie3.simona.model.grid.TransformerTapping import edu.ie3.simona.test.common.UnitSpec import edu.ie3.util.scala.quantities.{Megavars, ReactivePower} import org.apache.pekko.actor.testkit.typed.scaladsl.TestProbe @@ -18,8 +23,10 @@ import org.apache.pekko.actor.typed.ActorRef import squants.Power import squants.electro.Volts import squants.energy.Megawatts +import tech.units.indriya.ComparableQuantity import java.util.UUID +import javax.measure.quantity.Dimensionless import scala.concurrent.duration.{DurationInt, FiniteDuration} import scala.language.postfixOps @@ -93,6 +100,16 @@ trait DBFSMockGridAgents extends UnitSpec { sweepNo: Int, ): Unit = receiver ! SlackVoltageRequest(sweepNo, nodeUuids, gaProbe.ref) + + def expectCongestionCheckRequest( + maxDuration: FiniteDuration = 30 seconds + ): ActorRef[GridAgent.Request] = { + gaProbe.expectMessageType[CongestionCheckRequest](maxDuration).sender + } + + def expectVoltageRangeRequest(): ActorRef[GridAgent.Request] = { + gaProbe.expectMessageType[RequestVoltageOptions].sender + } } final case class SuperiorGA( @@ -151,5 +168,33 @@ trait DBFSMockGridAgents extends UnitSpec { ): Unit = { receiver ! RequestGridPower(sweepNo, nodeUuids, gaProbe.ref) } + + def expectCongestionResponse( + congestions: Congestions, + maxDuration: FiniteDuration = 30 seconds, + ): ActorRef[GridAgent.Request] = { + gaProbe.expectMessageType[CongestionResponse](maxDuration) match { + case CongestionResponse(sender, value) => + value.voltageCongestions shouldBe congestions.voltageCongestions + value.lineCongestions shouldBe congestions.lineCongestions + value.transformerCongestions shouldBe congestions.transformerCongestions + + sender + } + } + + def expectVoltageRangeResponse( + voltageRange: VoltageRange, + maxDuration: FiniteDuration = 30 seconds, + ): (ActorRef[GridAgent.Request], Set[TransformerTapping]) = { + gaProbe.expectMessageType[VoltageRangeResponse](maxDuration) match { + case VoltageRangeResponse(sender, (range, tappings)) => + range.deltaPlus shouldBe voltageRange.deltaPlus + range.deltaMinus shouldBe voltageRange.deltaMinus + range.suggestion should equalWithTolerance(voltageRange.suggestion) + + (sender, tappings) + } + } } } diff --git a/src/test/scala/edu/ie3/simona/agent/grid/DCMAlgorithmCenGridSpec.scala b/src/test/scala/edu/ie3/simona/agent/grid/DCMAlgorithmCenGridSpec.scala new file mode 100644 index 0000000000..168a352e0f --- /dev/null +++ b/src/test/scala/edu/ie3/simona/agent/grid/DCMAlgorithmCenGridSpec.scala @@ -0,0 +1,716 @@ +/* + * © 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 com.typesafe.config.ConfigFactory +import edu.ie3.datamodel.models.input.container.ThermalGrid +import edu.ie3.simona.agent.EnvironmentRefs +import edu.ie3.simona.agent.grid.CongestionManagementSupport.CongestionManagementSteps.TransformerTapping +import edu.ie3.simona.agent.grid.CongestionManagementSupport.{ + Congestions, + VoltageRange, +} +import edu.ie3.simona.agent.grid.GridAgentData.GridAgentInitData +import edu.ie3.simona.agent.grid.GridAgentMessages.Responses.{ + ExchangePower, + ExchangeVoltage, +} +import edu.ie3.simona.agent.grid.GridAgentMessages._ +import edu.ie3.simona.config.SimonaConfig +import edu.ie3.simona.event.{ResultEvent, RuntimeEvent} +import edu.ie3.simona.model.grid.{RefSystem, VoltageLimits} +import edu.ie3.simona.ontology.messages.SchedulerMessage.{ + Completion, + ScheduleActivation, +} +import edu.ie3.simona.ontology.messages.{Activation, SchedulerMessage} +import edu.ie3.simona.scheduler.ScheduleLock +import edu.ie3.simona.test.common.model.grid.DbfsTestGrid +import edu.ie3.simona.test.common.{ConfigTestData, TestSpawnerTyped} +import edu.ie3.simona.util.SimonaConstants.INIT_SIM_TICK +import edu.ie3.util.quantities.QuantityUtils.RichQuantityDouble +import edu.ie3.util.scala.quantities.Megavars +import org.apache.pekko.actor.testkit.typed.scaladsl.{ + ScalaTestWithActorTestKit, + TestProbe, +} +import org.apache.pekko.actor.typed.ActorRef +import org.apache.pekko.actor.typed.scaladsl.adapter.TypedActorRefOps +import squants.electro.Kilovolts +import squants.energy.Megawatts + +class DCMAlgorithmCenGridSpec + extends ScalaTestWithActorTestKit + with DBFSMockGridAgents + with ConfigTestData + with DbfsTestGrid + with TestSpawnerTyped { + + private val tappingEnabledConfig = ConfigFactory.parseString(""" + |simona.congestionManagement.enable = true + |simona.congestionManagement.enableTransformerTapping = true + |""".stripMargin) + + private val configWithTransformerTapping = SimonaConfig( + tappingEnabledConfig.withFallback(typesafeConfig) + ) + + private val scheduler: TestProbe[SchedulerMessage] = TestProbe("scheduler") + private val runtimeEvents: TestProbe[RuntimeEvent] = + TestProbe("runtimeEvents") + private val primaryService = TestProbe("primaryService") + private val weatherService = TestProbe("weatherService") + + private val superiorGridAgent = SuperiorGA( + TestProbe("superiorGridAgent_1000"), + Seq(supNodeA.getUuid, supNodeB.getUuid), + ) + + private val inferiorGrid11 = + InferiorGA(TestProbe("inferiorGridAgent_11"), Seq(node1.getUuid)) + + private val inferiorGrid12 = + InferiorGA(TestProbe("inferiorGridAgent_12"), Seq(node2.getUuid)) + + private val inferiorGrid13 = InferiorGA( + TestProbe("inferiorGridAgent_13"), + Seq(node3.getUuid, node4.getUuid), + ) + + private val environmentRefs = EnvironmentRefs( + scheduler = scheduler.ref, + runtimeEventListener = runtimeEvents.ref, + primaryServiceProxy = primaryService.ref.toClassic, + weather = weatherService.ref.toClassic, + evDataService = None, + ) + + val resultListener: TestProbe[ResultEvent] = TestProbe("resultListener") + + "A GridAgent actor in center position with async test" should { + val noCongestions = Congestions( + voltageCongestions = false, + lineCongestions = false, + transformerCongestions = false, + ) + + val voltageCongestions = noCongestions.copy(voltageCongestions = true) + + s"simulate grid and check for congestions correctly if no congestions occurred" in { + // init grid agent and simulate the grid + val centerGridAgent = + initAgentAndGotoSimulateGrid(configWithTransformerTapping) + simulateGrid(centerGridAgent) + + // after the simulation ends the center grid should receive a CongestionCheckRequest + // from the superior grid + centerGridAgent ! CongestionCheckRequest(superiorGridAgent.ref) + + // we expect a request for grid congestion values here + val congestionCheckRequestSender11 = + inferiorGrid11.expectCongestionCheckRequest() + val congestionCheckRequestSender12 = + inferiorGrid12.expectCongestionCheckRequest() + val congestionCheckRequestSender13 = + inferiorGrid13.expectCongestionCheckRequest() + + // send the center grid messages that indicate that no congestion occurred + // in the inferior grids + congestionCheckRequestSender11 ! CongestionResponse( + inferiorGrid11.ref, + noCongestions, + ) + congestionCheckRequestSender12 ! CongestionResponse( + inferiorGrid12.ref, + noCongestions, + ) + congestionCheckRequestSender13 ! CongestionResponse( + inferiorGrid13.ref, + noCongestions, + ) + + // after the center grid agent has processed all received congestions + // the superior grid will receive a congestion response + superiorGridAgent.expectCongestionResponse(noCongestions) + + // since there are no congestions tell all inferior grids to go back to idle + centerGridAgent ! GotoIdle + + // inferior should receive a next state message to go to the idle state + inferiorGrid11.gaProbe.expectMessageType[GotoIdle.type] + inferiorGrid12.gaProbe.expectMessageType[GotoIdle.type] + inferiorGrid13.gaProbe.expectMessageType[GotoIdle.type] + + // expect a completion message from the superior grid + scheduler.expectMessageType[Completion] match { + case Completion(_, Some(7200)) => + case x => + fail( + s"Invalid message received when expecting a completion message for simulate grid! Message was $x" + ) + } + } + + s"simulate grid and update transformer tapping correctly" in { + // init grid agent and simulate the grid + val centerGridAgent = + initAgentAndGotoSimulateGrid(configWithTransformerTapping) + simulateGrid(centerGridAgent) + + // after the simulation ends the center grid should receive a CongestionCheckRequest + // from the superior grid + centerGridAgent ! CongestionCheckRequest(superiorGridAgent.ref) + + // we expect a request for grid congestion values here + val congestionCheckRequestSender11 = + inferiorGrid11.expectCongestionCheckRequest() + val congestionCheckRequestSender12 = + inferiorGrid12.expectCongestionCheckRequest() + val congestionCheckRequestSender13 = + inferiorGrid13.expectCongestionCheckRequest() + + // send the center grid messages that indicate that congestions occurred + // in some inferior grids + congestionCheckRequestSender11 ! CongestionResponse( + inferiorGrid11.ref, + voltageCongestions, + ) + congestionCheckRequestSender12 ! CongestionResponse( + inferiorGrid12.ref, + noCongestions, + ) + congestionCheckRequestSender13 ! CongestionResponse( + inferiorGrid13.ref, + voltageCongestions, + ) + + // after the center grid agent has processed all received congestions + // the superior grid will receive a congestion response + superiorGridAgent.expectCongestionResponse(voltageCongestions) + + // since the superior grid receives a message the shows that there is at + // least one congestion in the grid, it starts the congestions management + // because there are voltage congestions and the transformet tapping has + // not run yet, the next step is the transformer tapping + centerGridAgent ! NextStepRequest( + TransformerTapping + ) + + // inferior grids should receive a next state message to go to the transformer tapping step + inferiorGrid11.gaProbe + .expectMessageType[NextStepRequest] + .nextStep shouldBe TransformerTapping + inferiorGrid12.gaProbe + .expectMessageType[NextStepRequest] + .nextStep shouldBe TransformerTapping + inferiorGrid13.gaProbe + .expectMessageType[NextStepRequest] + .nextStep shouldBe TransformerTapping + + // since the transformer tapping was started, the superior grid + // requests the possible voltage range from the center grid + centerGridAgent ! RequestVoltageOptions(superiorGridAgent.ref, 1000) + + // the center grid will request the voltage ranges from its inferior grid + // therefore the inferior grids should receive a VoltageRangeRequest + val voltageRangeRequester11 = inferiorGrid11.expectVoltageRangeRequest() + val voltageRangeRequester12 = inferiorGrid12.expectVoltageRangeRequest() + val voltageRangeRequester13 = inferiorGrid13.expectVoltageRangeRequest() + + // each inferior grid will send its possible voltage range to the center grid + voltageRangeRequester11 ! VoltageRangeResponse( + inferiorGrid11.ref, + ( + VoltageRange((-0.01).asPu, (-0.02).asPu), + Set(mvTransformers(transformer11.getUuid)), + ), + ) + voltageRangeRequester12 ! VoltageRangeResponse( + inferiorGrid12.ref, + ( + VoltageRange(0.07.asPu, 0.01.asPu), + Set(mvTransformers(transformer12.getUuid)), + ), + ) + voltageRangeRequester13 ! VoltageRangeResponse( + inferiorGrid13.ref, + ( + VoltageRange(0.06.asPu, 0.asPu), + Set( + mvTransformers(transformer13a.getUuid), + mvTransformers(transformer13b.getUuid), + ), + ), + ) + + // after the center grid received all voltage ranges + // the superior grid should receive a voltage range from the center grid + val (voltageDeltaRequest, tappingModels) = + superiorGridAgent.expectVoltageRangeResponse( + VoltageRange(0.04.asPu, 0.01.asPu, 0.025.asPu) + ) + + // the superior grid will update the transformer tappings + // and send the the resulting voltage delta to the center grid + tappingModels.size shouldBe 2 + tappingModels.foreach(_.decrTapPos(2)) + voltageDeltaRequest ! VoltageDeltaResponse(0.03.asPu) + + // the inferior grids should receive a voltage delta from the center grid + inferiorGrid11.gaProbe + .expectMessageType[VoltageDeltaResponse] + .delta should equalWithTolerance((-0.01).asPu) + inferiorGrid12.gaProbe + .expectMessageType[VoltageDeltaResponse] + .delta should equalWithTolerance(0.03.asPu) + inferiorGrid13.gaProbe + .expectMessageType[VoltageDeltaResponse] + .delta should equalWithTolerance(0.03.asPu) + + // this transformer can be tapped + mvTransformers(transformer11.getUuid).currentTapPos shouldBe 4 + + // these transformers can't be tapped and should keep their default tap pos + mvTransformers(transformer12.getUuid).currentTapPos shouldBe 0 + mvTransformers(transformer13a.getUuid).currentTapPos shouldBe 0 + mvTransformers(transformer13a.getUuid).currentTapPos shouldBe 0 + + // skipping this simulation step + skipSimulation(centerGridAgent) + + // aks for congestion check + centerGridAgent ! CongestionCheckRequest(superiorGridAgent.ref) + + // we expect a request for grid congestion values here + val congestionCheckRequestSender21 = + inferiorGrid11.expectCongestionCheckRequest() + val congestionCheckRequestSender22 = + inferiorGrid12.expectCongestionCheckRequest() + val congestionCheckRequestSender23 = + inferiorGrid13.expectCongestionCheckRequest() + + // send congestions + congestionCheckRequestSender21 ! CongestionResponse( + inferiorGrid11.ref, + noCongestions, + ) + congestionCheckRequestSender22 ! CongestionResponse( + inferiorGrid12.ref, + noCongestions, + ) + congestionCheckRequestSender23 ! CongestionResponse( + inferiorGrid13.ref, + noCongestions, + ) + + // after the simulation ends the center grid should receive a CongestionCheckRequest + // from the superior grid + superiorGridAgent.expectCongestionResponse(noCongestions) + + // since there are no congestions tell all inferior grids to go back to idle + centerGridAgent ! GotoIdle + + // inferior should receive a next state message to go to the idle state + inferiorGrid11.gaProbe.expectMessageType[GotoIdle.type] + inferiorGrid12.gaProbe.expectMessageType[GotoIdle.type] + inferiorGrid13.gaProbe.expectMessageType[GotoIdle.type] + + // expect a completion message from the superior grid + scheduler.expectMessageType[Completion] match { + case Completion(_, Some(7200)) => + case x => + fail( + s"Invalid message received when expecting a completion message for simulate grid! Message was $x" + ) + } + } + + // helper methods + + /** Method to initialize a superior grid agent with the given config. The + * grid agent is already in the simulateGrid state. + * + * @param simonaConfig + * that enables or disables certain congestion management steps + * @return + * the [[ActorRef]] of the created superior grid agent + */ + def initAgentAndGotoSimulateGrid( + simonaConfig: SimonaConfig + ): ActorRef[GridAgent.Request] = { + val centerGridAgent = + testKit.spawn( + GridAgent( + environmentRefs, + simonaConfig, + listener = Iterable(resultListener.ref), + ) + ) + + // this subnet has 1 superior grid (ehv) and 3 inferior grids (mv). Map the gates to test probes accordingly + val subGridGateToActorRef = hvSubGridGates.map { + case gate if gate.getInferiorSubGrid == hvGridContainer.getSubnet => + gate -> superiorGridAgent.ref + case gate => + val actor = gate.getInferiorSubGrid match { + case 11 => inferiorGrid11 + case 12 => inferiorGrid12 + case 13 => inferiorGrid13 + } + gate -> actor.ref + }.toMap + + val gridAgentInitData = + GridAgentInitData( + hvGridContainer, + Seq.empty[ThermalGrid], + subGridGateToActorRef, + RefSystem("2000 MVA", "110 kV"), + VoltageLimits(0.9, 1.1), + ) + + val key = ScheduleLock.singleKey(TSpawner, scheduler.ref, INIT_SIM_TICK) + // lock activation scheduled + scheduler.expectMessageType[ScheduleActivation] + + centerGridAgent ! CreateGridAgent( + gridAgentInitData, + key, + ) + + val scheduleActivationMsg = + scheduler.expectMessageType[ScheduleActivation] + scheduleActivationMsg.tick shouldBe INIT_SIM_TICK + scheduleActivationMsg.unlockKey shouldBe Some(key) + val gridAgentActivation = scheduleActivationMsg.actor + + centerGridAgent ! WrappedActivation(Activation(INIT_SIM_TICK)) + scheduler.expectMessage(Completion(gridAgentActivation, Some(3600))) + + // goto simulate grid + centerGridAgent ! WrappedActivation(Activation(3600)) + + // we expect a completion message + scheduler.expectMessageType[Completion].newTick shouldBe Some(3600) + + centerGridAgent + } + + /** Method to skip a simulation step. + * @param centerGridAgent + * center grid agent + */ + def skipSimulation(centerGridAgent: ActorRef[GridAgent.Request]): Unit = { + inferiorGrid11.gaProbe.expectMessageType[RequestGridPower] + inferiorGrid12.gaProbe.expectMessageType[RequestGridPower] + inferiorGrid13.gaProbe.expectMessageType[RequestGridPower] + superiorGridAgent.gaProbe.expectMessageType[SlackVoltageRequest] + + // skip simulation and go to congestion check + centerGridAgent ! FinishGridSimulationTrigger(3600) + + // inferior grid receives a FinishGridSimulationTrigger and goes into the congestion check state + inferiorGrid11.gaProbe.expectMessage(FinishGridSimulationTrigger(3600)) + inferiorGrid12.gaProbe.expectMessage(FinishGridSimulationTrigger(3600)) + inferiorGrid13.gaProbe.expectMessage(FinishGridSimulationTrigger(3600)) + } + + /** Method to reduce duplicate code. This runs a simple simulation based on + * the [[DBFSAlgorithmCenGridSpec]]. + */ + def simulateGrid(centerGridAgent: ActorRef[GridAgent.Request]): Unit = { + // start the simulation + val firstSweepNo = 0 + + // send the start grid simulation trigger + centerGridAgent ! WrappedActivation(Activation(3600)) + + /* We expect one grid power request message per inferior grid */ + + val firstPowerRequestSender11 = inferiorGrid11.expectGridPowerRequest() + + val firstPowerRequestSender12 = inferiorGrid12.expectGridPowerRequest() + + val firstPowerRequestSender13 = inferiorGrid13.expectGridPowerRequest() + + // we expect a request for voltage values of two nodes + // (voltages are requested by our agent under test from the superior grid) + val firstSlackVoltageRequestSender = + superiorGridAgent.expectSlackVoltageRequest(firstSweepNo) + + // normally the inferior grid agents ask for the slack voltage as well to do their power flow calculations + // we simulate this behaviour now by doing the same for our three inferior grid agents + inferiorGrid11.requestSlackVoltage(centerGridAgent, firstSweepNo) + + inferiorGrid12.requestSlackVoltage(centerGridAgent, firstSweepNo) + + inferiorGrid13.requestSlackVoltage(centerGridAgent, firstSweepNo) + + // as we are in the first sweep, all provided slack voltages should be equal + // to 1 p.u. (in physical values, here: 110kV) from the superior grid agent perspective + // (here: centerGridAgent perspective) + inferiorGrid11.expectSlackVoltageProvision( + firstSweepNo, + Seq( + ExchangeVoltage( + node1.getUuid, + Kilovolts(110d), + Kilovolts(0d), + ) + ), + ) + + inferiorGrid12.expectSlackVoltageProvision( + firstSweepNo, + Seq( + ExchangeVoltage( + node2.getUuid, + Kilovolts(110d), + Kilovolts(0d), + ) + ), + ) + + inferiorGrid13.expectSlackVoltageProvision( + firstSweepNo, + Seq( + ExchangeVoltage( + node3.getUuid, + Kilovolts(110d), + Kilovolts(0d), + ), + ExchangeVoltage( + node4.getUuid, + Kilovolts(110d), + Kilovolts(0d), + ), + ), + ) + + // we now answer the request of our centerGridAgent + // with three fake grid power messages and one fake slack voltage message + + firstPowerRequestSender11 ! GridPowerResponse( + inferiorGrid11.nodeUuids.map(nodeUuid => + ExchangePower( + nodeUuid, + Megawatts(0.0), + Megavars(0.0), + ) + ) + ) + + firstPowerRequestSender12 ! GridPowerResponse( + inferiorGrid12.nodeUuids.map(nodeUuid => + ExchangePower( + nodeUuid, + Megawatts(0.0), + Megavars(0.0), + ) + ) + ) + + firstPowerRequestSender13 ! GridPowerResponse( + inferiorGrid13.nodeUuids.map(nodeUuid => + ExchangePower( + nodeUuid, + Megawatts(0.0), + Megavars(0.0), + ) + ) + ) + + firstSlackVoltageRequestSender ! SlackVoltageResponse( + firstSweepNo, + Seq( + ExchangeVoltage( + supNodeA.getUuid, + Kilovolts(380d), + Kilovolts(0d), + ), + ExchangeVoltage( + supNodeB.getUuid, + Kilovolts(380d), + Kilovolts(0d), + ), + ), + ) + + // power flow calculation should run now. After it's done, + // our test agent should now be ready to provide the grid power values, + // hence we ask for them and expect a corresponding response + superiorGridAgent.requestGridPower(centerGridAgent, firstSweepNo) + + superiorGridAgent.expectGridPowerProvision( + Seq( + ExchangePower( + supNodeA.getUuid, + Megawatts(0.0), + Megavars(0.0), + ), + ExchangePower( + supNodeB.getUuid, + Megawatts(0.160905770717798), + Megavars(-1.4535602349123878), + ), + ) + ) + + // we start a second sweep by asking for next sweep values which should trigger the whole procedure again + val secondSweepNo = 1 + + superiorGridAgent.requestGridPower(centerGridAgent, secondSweepNo) + + // the agent now should ask for updated slack voltages from the superior grid + val secondSlackAskSender = + superiorGridAgent.expectSlackVoltageRequest(secondSweepNo) + + // the superior grid would answer with updated slack voltage values + secondSlackAskSender ! SlackVoltageResponse( + secondSweepNo, + Seq( + ExchangeVoltage( + supNodeB.getUuid, + Kilovolts(374.22694614463d), // 380 kV @ 10° + Kilovolts(65.9863075134335d), // 380 kV @ 10° + ), + ExchangeVoltage( // this one should currently be ignored anyways + supNodeA.getUuid, + Kilovolts(380d), + Kilovolts(0d), + ), + ), + ) + + // After the intermediate power flow calculation, we expect one grid power + // request message per inferior subgrid + + val secondPowerRequestSender11 = + inferiorGrid11.expectGridPowerRequest() + + val secondPowerRequestSender12 = + inferiorGrid12.expectGridPowerRequest() + + val secondPowerRequestSender13 = + inferiorGrid13.expectGridPowerRequest() + + // normally the inferior grid agents ask for the slack voltage as well to do their power flow calculations + // we simulate this behaviour now by doing the same for our three inferior grid agents + + inferiorGrid11.requestSlackVoltage(centerGridAgent, secondSweepNo) + + inferiorGrid12.requestSlackVoltage(centerGridAgent, secondSweepNo) + + inferiorGrid13.requestSlackVoltage(centerGridAgent, secondSweepNo) + + // as we are in the second sweep, all provided slack voltages should be unequal + // to 1 p.u. (in physical values, here: 110kV) from the superior grid agent perspective + // (here: centerGridAgent perspective) + + inferiorGrid11.expectSlackVoltageProvision( + secondSweepNo, + Seq( + ExchangeVoltage( + node1.getUuid, + Kilovolts(108.487669651919932d), + Kilovolts(19.101878551141232d), + ) + ), + ) + + inferiorGrid12.expectSlackVoltageProvision( + secondSweepNo, + Seq( + ExchangeVoltage( + node2.getUuid, + Kilovolts(108.449088870497683d), + Kilovolts(19.10630456834157630d), + ) + ), + ) + + inferiorGrid13.expectSlackVoltageProvision( + secondSweepNo, + Seq( + ExchangeVoltage( + node3.getUuid, + Kilovolts(108.470028019077087d), + Kilovolts(19.104403047662570d), + ), + ExchangeVoltage( + node4.getUuid, + Kilovolts(108.482524607256866d), + Kilovolts(19.1025584700935336d), + ), + ), + ) + + // we now answer the requests of our centerGridAgent + // with three fake grid power message + + secondPowerRequestSender11 ! GridPowerResponse( + inferiorGrid11.nodeUuids.map(nodeUuid => + ExchangePower( + nodeUuid, + Megawatts(0.0), + Megavars(0.0), + ) + ) + ) + + secondPowerRequestSender12 ! GridPowerResponse( + inferiorGrid12.nodeUuids.map(nodeUuid => + ExchangePower( + nodeUuid, + Megawatts(0.0), + Megavars(0.0), + ) + ) + ) + + secondPowerRequestSender13 ! GridPowerResponse( + inferiorGrid13.nodeUuids.map(nodeUuid => + ExchangePower( + nodeUuid, + Megawatts(0.0), + Megavars(0.0), + ) + ) + ) + + // we expect that the GridAgent unstashes the messages and return a value for our power request + superiorGridAgent.expectGridPowerProvision( + Seq( + ExchangePower( + supNodeA.getUuid, + Megawatts(0.0), + Megavars(0.0), + ), + ExchangePower( + supNodeB.getUuid, + Megawatts(0.16090577067051856), + Megavars(-1.4535602358772026), + ), + ) + ) + + // normally the slack node would send a FinishGridSimulationTrigger to all + // connected inferior grids, because the slack node is just a mock, we imitate this behavior + centerGridAgent ! FinishGridSimulationTrigger(3600) + + // after a FinishGridSimulationTrigger is send the inferior grids, they themselves will send the + // Trigger forward the trigger to their connected inferior grids. Therefore the inferior grid + // agent should receive a FinishGridSimulationTrigger + inferiorGrid11.gaProbe.expectMessage(FinishGridSimulationTrigger(3600)) + + inferiorGrid12.gaProbe.expectMessage(FinishGridSimulationTrigger(3600)) + + inferiorGrid13.gaProbe.expectMessage(FinishGridSimulationTrigger(3600)) + } + + } +} diff --git a/src/test/scala/edu/ie3/simona/agent/grid/DCMAlgorithmSupGridSpec.scala b/src/test/scala/edu/ie3/simona/agent/grid/DCMAlgorithmSupGridSpec.scala new file mode 100644 index 0000000000..8f71feb744 --- /dev/null +++ b/src/test/scala/edu/ie3/simona/agent/grid/DCMAlgorithmSupGridSpec.scala @@ -0,0 +1,330 @@ +/* + * © 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 com.typesafe.config.ConfigFactory +import edu.ie3.datamodel.graph.SubGridGate +import edu.ie3.datamodel.models.input.container.ThermalGrid +import edu.ie3.simona.agent.EnvironmentRefs +import edu.ie3.simona.agent.grid.CongestionManagementSupport.{ + Congestions, + VoltageRange, +} +import edu.ie3.simona.agent.grid.GridAgentData.GridAgentInitData +import edu.ie3.simona.agent.grid.GridAgentMessages._ +import edu.ie3.simona.config.SimonaConfig +import edu.ie3.simona.event.{ResultEvent, RuntimeEvent} +import edu.ie3.simona.model.grid.{RefSystem, TransformerModel, VoltageLimits} +import edu.ie3.simona.ontology.messages.SchedulerMessage.{ + Completion, + ScheduleActivation, +} +import edu.ie3.simona.ontology.messages.services.ServiceMessage +import edu.ie3.simona.ontology.messages.{Activation, SchedulerMessage} +import edu.ie3.simona.scheduler.ScheduleLock +import edu.ie3.simona.test.common.model.grid.DbfsTestGrid +import edu.ie3.simona.test.common.{ConfigTestData, TestSpawnerTyped, UnitSpec} +import edu.ie3.simona.util.SimonaConstants.INIT_SIM_TICK +import edu.ie3.util.quantities.QuantityUtils.RichQuantityDouble +import org.apache.pekko.actor.testkit.typed.scaladsl.{ + ScalaTestWithActorTestKit, + TestProbe, +} +import org.apache.pekko.actor.typed.ActorRef +import org.apache.pekko.actor.typed.scaladsl.adapter.TypedActorRefOps +import squants.electro.Kilovolts +import squants.energy.Kilowatts + +import scala.concurrent.duration.DurationInt + +/** Test to ensure the functions that a [[GridAgent]] in superior position + * should be able to do if the DCMSAlgorithm is used. The scheduler, the + * weather service as well as the [[GridAgent]] inferior to the superior + * [[GridAgent]] are simulated by the TestKit. + */ +class DCMAlgorithmSupGridSpec + extends ScalaTestWithActorTestKit + with UnitSpec + with ConfigTestData + with DbfsTestGrid + with TestSpawnerTyped { + + // config with congestion management and transformer tapping enabled + private val tappingEnabledConfig = ConfigFactory.parseString(""" + |simona.congestionManagement.enable = true + |simona.congestionManagement.enableTransformerTapping = true + |""".stripMargin) + + private val configWithTransformerTapping = SimonaConfig( + tappingEnabledConfig.withFallback(typesafeConfig) + ) + + private val scheduler: TestProbe[SchedulerMessage] = TestProbe("scheduler") + private val runtimeEvents: TestProbe[RuntimeEvent] = + TestProbe("runtimeEvents") + private val primaryService: TestProbe[ServiceMessage] = + TestProbe("primaryService") + private val weatherService = TestProbe("weatherService") + private val hvGrid: TestProbe[GridAgent.Request] = TestProbe("hvGrid") + + private val environmentRefs = EnvironmentRefs( + scheduler = scheduler.ref, + runtimeEventListener = runtimeEvents.ref, + primaryServiceProxy = primaryService.ref.toClassic, + weather = weatherService.ref.toClassic, + evDataService = None, + ) + + val resultListener: TestProbe[ResultEvent] = TestProbe("resultListener") + + "A GridAgent actor in superior position with async test" should { + val refSystem = RefSystem(Kilowatts(600), Kilovolts(110)) + + val tappingModel = TransformerModel( + transformer1, + refSystem, + start, + end, + ) + + val tappingModel2 = TransformerModel( + transformer2, + refSystem, + start, + end, + ) + + s"skip simulate grid and check for congestions correctly if no congestions occurred" in { + val superiorGridAgent = + initAgentAndGotoSimulateGrid(configWithTransformerTapping) + + val lastSender = skipSimulationAndGetNextStep(superiorGridAgent) + + // send the requesting grid agent a CongestionResponse with no congestions + lastSender ! CongestionResponse( + hvGrid.ref, + Congestions( + voltageCongestions = false, + lineCongestions = false, + transformerCongestions = false, + ), + ) + + // since their are no congestions the inferior should receive + // a next state message to go to the idle state + hvGrid.expectMessageType[GotoIdle.type] + + // expect a completion message from the superior grid + scheduler.expectMessageType[Completion] match { + case Completion(_, Some(7200)) => + case x => + fail( + s"Invalid message received when expecting a completion message for simulate grid! Message was $x" + ) + } + } + + s"skip simulate grid and handle unresolvable congestions correctly" in { + val superiorGridAgent = + initAgentAndGotoSimulateGrid(configWithTransformerTapping) + + val lastSender = skipSimulationAndGetNextStep(superiorGridAgent) + + // voltage congestion cannot be resolved, because using flex options is not + // enable by the provided config + lastSender ! CongestionResponse( + hvGrid.ref, + Congestions( + voltageCongestions = false, + lineCongestions = false, + transformerCongestions = true, + ), + ) + + // since the congestion management can't resolve the congestions + // the inferior should receive a next state message to go to the idle state + hvGrid.expectMessageType[GotoIdle.type] + + // expect a completion message from the superior grid + scheduler.expectMessageType[Completion] match { + case Completion(_, Some(7200)) => + case x => + fail( + s"Invalid message received when expecting a completion message for simulate grid! Message was $x" + ) + } + } + + s"skip simulate grid and update transformer tapping correctly" in { + val superiorGridAgent = + initAgentAndGotoSimulateGrid(configWithTransformerTapping) + + val lastSender1 = skipSimulationAndGetNextStep(superiorGridAgent) + + // sending the superior grid a solvable congestion + lastSender1 ! CongestionResponse( + hvGrid.ref, + Congestions( + voltageCongestions = true, + lineCongestions = false, + transformerCongestions = false, + ), + ) + + // since the received congestion can be resolved the inferior should + // receive a next state message to go to a congestion management step + hvGrid.expectMessageType[NextStepRequest] + + // both transformer models should have their default tap pos + tappingModel.currentTapPos shouldBe 0 + tappingModel2.currentTapPos shouldBe 0 + + // the inferior will receive a request to send the possible voltage range + // and send a VoltageRangeResponse to the superior grid + hvGrid.expectMessageType[RequestVoltageOptions] match { + case RequestVoltageOptions(sender, subnet) => + subnet shouldBe 1000 + + sender ! VoltageRangeResponse( + hvGrid.ref, + ( + VoltageRange(0.025.asPu, 0.01.asPu), + Set(tappingModel, tappingModel2), + ), + ) + } + + // the inferior will receive a voltage delta from the superior grid + // after the superior grid change the transformer tapping + hvGrid.expectMessageType[VoltageDeltaResponse](120.seconds) match { + case VoltageDeltaResponse(delta) => + delta should equalWithTolerance(0.015.asPu) + } + + // both transformer models tap pos should have changed by the same amount + tappingModel.currentTapPos shouldBe -1 + tappingModel2.currentTapPos shouldBe -1 + + // skipping the simulation + hvGrid.expectMessageType[RequestGridPower] + + val lastSender2 = skipSimulationAndGetNextStep(superiorGridAgent) + + // sending the superior grid that no more congestions are present + lastSender2 ! CongestionResponse( + hvGrid.ref, + Congestions( + voltageCongestions = false, + lineCongestions = false, + transformerCongestions = false, + ), + ) + + // since their are no congestions the inferior should receive + // a next state message to go to the idle state + hvGrid.expectMessageType[GotoIdle.type] + + // expect a completion message from the superior grid + // after all steps are finished + scheduler.expectMessageType[Completion](10.seconds) match { + case Completion(_, Some(7200)) => + case x => + fail( + s"Invalid message received when expecting a completion message for simulate grid! Message was $x" + ) + } + } + + // helper methods + + /** There is no need to perform an actual simulation of the grid, therefor + * we can use this method to skip the + * @param superiorGridAgent + * the superior grid agent + * @return + * the [[ActorRef]] of the last sender + */ + def skipSimulationAndGetNextStep( + superiorGridAgent: ActorRef[GridAgent.Request] + ): ActorRef[GridAgent.Request] = { + // skip simulation and go to congestion check + superiorGridAgent ! FinishGridSimulationTrigger(3600) + + // inferior grid receives a FinishGridSimulationTrigger and goes into the congestion check state + hvGrid.expectMessage(FinishGridSimulationTrigger(3600)) + + // we expect a request for grid congestion values here + val lastSender = + hvGrid.expectMessageType[CongestionCheckRequest](10.seconds) match { + case CongestionCheckRequest(sender) => sender + case x => + fail( + s"Invalid message received when expecting a request for grid congestion values! Message was $x" + ) + } + + // return the last sender + lastSender + } + + /** Method to initialize a superior grid agent with the given config. The + * grid agent is already in the simulateGrid state. + * @param simonaConfig + * that enables or disables certain congestion management steps + * @return + * the [[ActorRef]] of the created superior grid agent + */ + def initAgentAndGotoSimulateGrid( + simonaConfig: SimonaConfig + ): ActorRef[GridAgent.Request] = { + // init a superior grid agent with the given config + // that enabled certain congestion management options + val superiorGridAgent: ActorRef[GridAgent.Request] = testKit.spawn( + GridAgent( + environmentRefs, + simonaConfig, + listener = Iterable(resultListener.ref), + ) + ) + + val subnetGatesToActorRef: Map[SubGridGate, ActorRef[GridAgent.Request]] = + ehvSubGridGates.map(gate => gate -> hvGrid.ref).toMap + + val gridAgentInitData = + GridAgentInitData( + ehvGridContainer, + Seq.empty[ThermalGrid], + subnetGatesToActorRef, + RefSystem("5000 MVA", "110 kV"), + VoltageLimits(0.9, 1.05), + ) + + val key = ScheduleLock.singleKey(TSpawner, scheduler.ref, INIT_SIM_TICK) + // lock activation scheduled + scheduler.expectMessageType[ScheduleActivation] + + superiorGridAgent ! CreateGridAgent(gridAgentInitData, key) + + val scheduleActivationMsg = + scheduler.expectMessageType[ScheduleActivation] + scheduleActivationMsg.tick shouldBe INIT_SIM_TICK + scheduleActivationMsg.unlockKey shouldBe Some(key) + val gridAgentActivation = scheduleActivationMsg.actor + + superiorGridAgent ! WrappedActivation(Activation(INIT_SIM_TICK)) + scheduler.expectMessage(Completion(gridAgentActivation, Some(3600))) + + // goto simulate grid + superiorGridAgent ! WrappedActivation(Activation(3600)) + + // we expect a completion message + scheduler.expectMessageType[Completion].newTick shouldBe Some(3600) + + superiorGridAgent + } + } +} diff --git a/src/test/scala/edu/ie3/simona/agent/grid/GridAgentDataSpec.scala b/src/test/scala/edu/ie3/simona/agent/grid/GridAgentDataSpec.scala new file mode 100644 index 0000000000..3cdda238c5 --- /dev/null +++ b/src/test/scala/edu/ie3/simona/agent/grid/GridAgentDataSpec.scala @@ -0,0 +1,234 @@ +/* + * © 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.CongestionManagementSupport.Congestions +import edu.ie3.simona.agent.grid.GridAgentData.CongestionManagementData +import edu.ie3.simona.event.ResultEvent.PowerFlowResultEvent +import edu.ie3.simona.model.grid.{GridModel, RefSystem, VoltageLimits} +import edu.ie3.simona.test.common.model.grid.DbfsTestGrid +import edu.ie3.simona.test.common.{ConfigTestData, UnitSpec} +import edu.ie3.util.quantities.PowerSystemUnits.PU +import edu.ie3.util.quantities.QuantityUtils.RichQuantityDouble +import squants.electro.Kilovolts +import squants.energy.Kilowatts +import tech.units.indriya.quantity.Quantities + +import java.time.ZonedDateTime + +class GridAgentDataSpec extends UnitSpec with DbfsTestGrid with ConfigTestData { + + "The CongestionManagementData" should { + val startTime = ZonedDateTime.now() + + val gridModel = GridModel( + hvGridContainer, + RefSystem(Kilowatts(600), Kilovolts(110)), + VoltageLimits(0.9, 1.1), + startTime, + startTime.plusHours(2), + simonaConfig, + ) + + val findCongestions = PrivateMethod[Congestions](Symbol("findCongestions")) + + "find congestions correctly for empty results" in { + val emptyResults = PowerFlowResultEvent( + Seq.empty, + Seq.empty, + Seq.empty, + Seq.empty, + Seq.empty, + ) + + CongestionManagementData invokePrivate findCongestions( + emptyResults, + gridModel.gridComponents, + gridModel.voltageLimits, + gridModel.mainRefSystem.nominalVoltage, + gridModel.subnetNo, + ) 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, + ) + + CongestionManagementData invokePrivate findCongestions( + results, + gridModel.gridComponents, + gridModel.voltageLimits, + gridModel.mainRefSystem.nominalVoltage, + gridModel.subnetNo, + ) 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, + ) + + CongestionManagementData invokePrivate findCongestions( + results, + gridModel.gridComponents, + gridModel.voltageLimits, + gridModel.mainRefSystem.nominalVoltage, + gridModel.subnetNo, + ) 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, + ) + + CongestionManagementData invokePrivate findCongestions( + results, + gridModel.gridComponents, + gridModel.voltageLimits, + gridModel.mainRefSystem.nominalVoltage, + gridModel.subnetNo, + ) shouldBe Congestions( + voltageCongestions = false, + lineCongestions = false, + transformerCongestions = true, + ) + } + } + +} diff --git a/src/test/scala/edu/ie3/simona/agent/grid/GridResultsSupportSpec.scala b/src/test/scala/edu/ie3/simona/agent/grid/GridResultsSupportSpec.scala index a0fc556f49..57fa4fa049 100644 --- a/src/test/scala/edu/ie3/simona/agent/grid/GridResultsSupportSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/grid/GridResultsSupportSpec.scala @@ -45,7 +45,7 @@ import edu.ie3.util.scala.quantities.{QuantityUtil => ScalaQuantityUtil} import org.scalatest.prop.TableDrivenPropertyChecks import squants.Each import squants.electro.{Amperes, Volts} -import squants.energy.Kilowatts +import squants.energy.{Kilowatts, Watts} import squants.space.Degrees import tech.units.indriya.quantity.Quantities import tech.units.indriya.unit.Units @@ -444,6 +444,7 @@ class GridResultsSupportSpec ), 1, PowerFlowCaseA, + Watts(10), Each(0.1d), Each(0.2d), Each(0.3d), diff --git a/src/test/scala/edu/ie3/simona/model/grid/GridSpec.scala b/src/test/scala/edu/ie3/simona/model/grid/GridSpec.scala index 2f29248054..3d7d81d6c8 100644 --- a/src/test/scala/edu/ie3/simona/model/grid/GridSpec.scala +++ b/src/test/scala/edu/ie3/simona/model/grid/GridSpec.scala @@ -215,6 +215,7 @@ class GridSpec Set.empty[Transformer3wModel], switches, ), + defaultVoltageLimits, GridControls.empty, ) // get the private method for validation @@ -231,7 +232,7 @@ class GridSpec nodes.foreach(_.enable()) // remove a line from the grid - val adaptedLines = lines - line3To4 + val adaptedLines: Set[LineModel] = lines - line3To4 adaptedLines.foreach(_.enable()) // enable transformer @@ -251,6 +252,7 @@ class GridSpec Set.empty[Transformer3wModel], switches, ), + defaultVoltageLimits, GridControls.empty, ) @@ -355,6 +357,7 @@ class GridSpec Set.empty[Transformer3wModel], switches, ), + defaultVoltageLimits, GridControls.empty, ) @@ -407,6 +410,7 @@ class GridSpec Set.empty[Transformer3wModel], Set.empty[SwitchModel], ), + defaultVoltageLimits, GridControls.empty, ) @@ -460,13 +464,14 @@ class GridSpec Set.empty[Transformer3wModel], switches, ), + defaultVoltageLimits, GridControls.empty, ) updateUuidToIndexMap(gridModel) // nodes 1, 13 and 14 should map to the same node - val node1Index = gridModel.nodeUuidToIndexMap + val node1Index: Int = gridModel.nodeUuidToIndexMap .get(node1.uuid) .value gridModel.nodeUuidToIndexMap.get(node13.uuid).value shouldBe node1Index @@ -540,6 +545,7 @@ class GridSpec Set.empty, switches, ), + defaultVoltageLimits, GridControls.empty, ) @@ -643,6 +649,7 @@ class GridSpec GridModel( validTestGridInputModel, gridInputModelTestDataRefSystem, + defaultVoltageLimits, defaultSimulationStart, defaultSimulationEnd, simonaConfig, diff --git a/src/test/scala/edu/ie3/simona/model/grid/NodeInputModelSpec.scala b/src/test/scala/edu/ie3/simona/model/grid/NodeInputModelSpec.scala index 140ecec17f..bba7ab411a 100644 --- a/src/test/scala/edu/ie3/simona/model/grid/NodeInputModelSpec.scala +++ b/src/test/scala/edu/ie3/simona/model/grid/NodeInputModelSpec.scala @@ -38,6 +38,7 @@ class NodeInputModelSpec extends UnitSpec with NodeInputTestData { isSlack, vTarget, voltLvl, + subnet, ) => uuid shouldBe nodeInputNoSlackNs04KvA.getUuid id shouldBe nodeInputNoSlackNs04KvA.getId @@ -47,6 +48,7 @@ class NodeInputModelSpec extends UnitSpec with NodeInputTestData { Each(nodeInputNoSlackNs04KvA.getvTarget.getValue.doubleValue()) ) voltLvl shouldBe nodeInputNoSlackNs04KvA.getVoltLvl + subnet shouldBe -1 } } 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 4f865f58af..e386cd4ca0 100644 --- a/src/test/scala/edu/ie3/simona/model/grid/Transformer3wModelSpec.scala +++ b/src/test/scala/edu/ie3/simona/model/grid/Transformer3wModelSpec.scala @@ -17,7 +17,8 @@ import edu.ie3.simona.test.common.UnitSpec import edu.ie3.simona.test.common.input.Transformer3wTestData import edu.ie3.util.quantities.PowerSystemUnits._ import org.scalatest.prop.{TableDrivenPropertyChecks, TableFor4} -import squants.Each +import squants.energy.Megawatts +import squants.{Amperes, Each} import tech.units.indriya.quantity.Quantities import scala.math.BigDecimal.RoundingMode @@ -27,6 +28,8 @@ class Transformer3wModelSpec with TableDrivenPropertyChecks with Transformer3wTestData { val testingTolerance = 1e-5 + implicit val electricCurrentTolerance: squants.electro.ElectricCurrent = + Amperes(1e-8) implicit val dimensionlessTolerance: squants.Dimensionless = Each(1e-8) "A three winding transformer input model" should { @@ -67,6 +70,7 @@ class Transformer3wModelSpec transformerTappingModel, amount, powerFlowCase, + sRated, r, x, g, @@ -85,6 +89,7 @@ class Transformer3wModelSpec transformerTappingModel shouldBe expectedTappingModel amount shouldBe transformer3wInput.getParallelDevices powerFlowCase shouldBe PowerFlowCaseA + sRated shouldBe Megawatts(120) r should approximate(Each(1.03878e-3)) x should approximate(Each(166.34349e-3)) g should approximate(Each(1.874312e-6)) @@ -142,6 +147,7 @@ class Transformer3wModelSpec transformerTappingModel, amount, powerFlowCase, + sRated, r, x, g, @@ -160,6 +166,7 @@ class Transformer3wModelSpec transformerTappingModel shouldBe expectedTappingModel amount shouldBe transformer3wInput.getParallelDevices powerFlowCase shouldBe PowerFlowCaseB + sRated shouldBe Megawatts(60) r should approximate(Each(240.9972299e-6)) x should approximate(Each(24.99307479224e-3)) g should approximate(Each(0d)) @@ -217,6 +224,7 @@ class Transformer3wModelSpec transformerTappingModel, amount, powerFlowCase, + sRated, r, x, g, @@ -235,6 +243,7 @@ class Transformer3wModelSpec transformerTappingModel shouldBe expectedTappingModel amount shouldBe transformer3wInput.getParallelDevices powerFlowCase shouldBe PowerFlowCaseC + sRated shouldBe Megawatts(40) r should approximate(Each(3.185595567e-6)) x should approximate(Each(556.0941828e-6)) g should approximate(Each(0d)) @@ -480,7 +489,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/TransformerModelSpec.scala b/src/test/scala/edu/ie3/simona/model/grid/TransformerModelSpec.scala index 880ca12116..67ff272e29 100644 --- a/src/test/scala/edu/ie3/simona/model/grid/TransformerModelSpec.scala +++ b/src/test/scala/edu/ie3/simona/model/grid/TransformerModelSpec.scala @@ -18,19 +18,18 @@ import edu.ie3.powerflow.model.NodeData.{PresetData, StateData} import edu.ie3.powerflow.model.StartData.WithForcedStartVoltages import edu.ie3.powerflow.model.enums.NodeType import edu.ie3.powerflow.model.{NodeData, PowerFlowResult} -import edu.ie3.simona.test.common.{ConfigTestData, UnitSpec} import edu.ie3.simona.test.common.model.grid.{ TapTestData, TransformerTestData, TransformerTestGrid, } +import edu.ie3.simona.test.common.{ConfigTestData, UnitSpec} import edu.ie3.util.quantities.PowerSystemUnits._ import org.scalatest.prop.{TableDrivenPropertyChecks, TableFor4} import squants.Each -import squants.electro.{Amperes, Kilovolts} +import squants.electro.Amperes import squants.energy.Kilowatts import tech.units.indriya.quantity.Quantities -import tech.units.indriya.unit.Units._ import java.time.ZonedDateTime import java.time.temporal.ChronoUnit @@ -96,6 +95,7 @@ class TransformerModelSpec voltRatioNominal, iNomHv, iNomLv, + sRated, r, x, g, @@ -127,6 +127,7 @@ class TransformerModelSpec voltRatioNominal shouldBe BigDecimal("25") iNomHv should approximate(Amperes(36.373066958946424d)) iNomLv should approximate(Amperes(909.3266739736606d)) + sRated shouldBe Kilowatts(630) r should approximate(Each(7.357e-3)) x should approximate(Each(24.30792e-3)) g should approximate(Each(0.0)) @@ -281,7 +282,7 @@ class TransformerModelSpec transformerModelTapHv.updateTapPos(currentTapPos) transformerModelTapHv.computeDeltaTap( vChange, - deadBand, + deadBand = deadBand, ) shouldBe expected } } @@ -379,6 +380,7 @@ class TransformerModelSpec val gridModel = GridModel( grid, refSystem, + voltageLimits, defaultSimulationStart, defaultSimulationEnd, simonaConfig, 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 c046493ca6..b00380e367 100644 --- a/src/test/scala/edu/ie3/simona/test/common/ConfigTestData.scala +++ b/src/test/scala/edu/ie3/simona/test/common/ConfigTestData.scala @@ -145,6 +145,12 @@ trait ConfigTestData { |simona.powerflow.newtonraphson.iterations = 50 | |simona.gridConfig.refSystems = [] + |simona.gridConfig.voltageLimits = [] + | + |simona.congestionManagement.enable = false + |simona.congestionManagement.enableTransformerTapping = false + |simona.congestionManagement.enableTopologyChanges = false + |simona.congestionManagement.useFlexOptions = false |""".stripMargin ) protected val simonaConfig: SimonaConfig = SimonaConfig(typesafeConfig) diff --git a/src/test/scala/edu/ie3/simona/test/common/DefaultTestData.scala b/src/test/scala/edu/ie3/simona/test/common/DefaultTestData.scala index b2ddb887a7..5073b82945 100644 --- a/src/test/scala/edu/ie3/simona/test/common/DefaultTestData.scala +++ b/src/test/scala/edu/ie3/simona/test/common/DefaultTestData.scala @@ -10,7 +10,7 @@ import com.typesafe.config.{Config, ConfigFactory} import edu.ie3.datamodel.models.OperationTime import edu.ie3.simona.config.SimonaConfig import edu.ie3.simona.model.SystemComponent -import edu.ie3.simona.model.grid.RefSystem +import edu.ie3.simona.model.grid.{RefSystem, VoltageLimits} import edu.ie3.simona.model.participant.load.{LoadModelBehaviour, LoadReference} import edu.ie3.util.scala.OperationInterval import org.locationtech.jts.geom.{Coordinate, GeometryFactory, Point} @@ -63,6 +63,8 @@ trait DefaultTestData { Kilovolts(10d), ) + protected val defaultVoltageLimits: VoltageLimits = VoltageLimits(0.9, 1.1) + /** Creates a [[SimonaConfig]], that provides the desired participant model * configurations * @@ -234,6 +236,11 @@ trait DefaultTestData { |simona.powerflow.resolution = "3600s" | |simona.gridConfig.refSystems = [] + |simona.gridConfig.voltageLimits = [] + | + |simona.congestionManagement.enableTransformerTapping = false + |simona.congestionManagement.enableTopologyChanges = false + |simona.congestionManagement.useFlexOptions = false |""".stripMargin ) SimonaConfig(typesafeConfig) diff --git a/src/test/scala/edu/ie3/simona/test/common/input/GridInputTestData.scala b/src/test/scala/edu/ie3/simona/test/common/input/GridInputTestData.scala index d9cdbb2681..7a3ded4c6a 100644 --- a/src/test/scala/edu/ie3/simona/test/common/input/GridInputTestData.scala +++ b/src/test/scala/edu/ie3/simona/test/common/input/GridInputTestData.scala @@ -21,7 +21,7 @@ import edu.ie3.datamodel.models.voltagelevels.GermanVoltageLevelUtils.{ HV, MV_10KV, } -import edu.ie3.simona.model.grid.RefSystem +import edu.ie3.simona.model.grid.{RefSystem, VoltageLimits} import edu.ie3.simona.test.common.DefaultTestData import edu.ie3.simona.util.TestGridFactory import testutils.TestObjectFactory @@ -155,6 +155,9 @@ trait GridInputTestData protected def gridInputModelTestDataRefSystem: RefSystem = default400Kva10KvRefSystem + protected def gridInputModelTestDataVoltageLimits: VoltageLimits = + defaultVoltageLimits + // create the grid protected val validTestGridInputModel: SubGridContainer = { val rawGridElements = new RawGridElements( diff --git a/src/test/scala/edu/ie3/simona/test/common/model/grid/BasicGrid.scala b/src/test/scala/edu/ie3/simona/test/common/model/grid/BasicGrid.scala index 959a3074f7..db89b516ab 100644 --- a/src/test/scala/edu/ie3/simona/test/common/model/grid/BasicGrid.scala +++ b/src/test/scala/edu/ie3/simona/test/common/model/grid/BasicGrid.scala @@ -13,7 +13,8 @@ import edu.ie3.simona.model.grid.{ } import edu.ie3.simona.test.common.DefaultTestData import edu.ie3.util.quantities.PowerSystemUnits._ -import squants.{Amperes, Each} +import squants.energy.Watts +import squants.{Amperes, Each, Power} import tech.units.indriya.ComparableQuantity import tech.units.indriya.quantity.Quantities import tech.units.indriya.unit.Units._ @@ -114,6 +115,8 @@ trait BasicGrid extends FiveLinesWithNodes with DefaultTestData { protected val iNomLv: squants.electro.ElectricCurrent = Amperes(2309.401076758503d) + protected val sRated: Power = Watts(1) + // / transformer protected val transformer2wModel = new TransformerModel( UUID.fromString("a28eb631-2c26-4831-9d05-aa1b3f90b96a"), @@ -126,6 +129,7 @@ trait BasicGrid extends FiveLinesWithNodes with DefaultTestData { BigDecimal("11"), iNomHv, iNomLv, + sRated, transformerRInPu, transformerXInPu, transformerGInPu, diff --git a/src/test/scala/edu/ie3/simona/test/common/model/grid/BasicGridWithSwitches.scala b/src/test/scala/edu/ie3/simona/test/common/model/grid/BasicGridWithSwitches.scala index bd709daa01..495fd134db 100644 --- a/src/test/scala/edu/ie3/simona/test/common/model/grid/BasicGridWithSwitches.scala +++ b/src/test/scala/edu/ie3/simona/test/common/model/grid/BasicGridWithSwitches.scala @@ -231,6 +231,7 @@ trait BasicGridWithSwitches extends BasicGrid { Set.empty[Transformer3wModel], gridSwitches, ), + defaultVoltageLimits, GridControls.empty, ) } diff --git a/src/test/scala/edu/ie3/simona/test/common/model/grid/DbfsTestGrid.scala b/src/test/scala/edu/ie3/simona/test/common/model/grid/DbfsTestGrid.scala index 07a2ef7f8f..eb9df24c3c 100644 --- a/src/test/scala/edu/ie3/simona/test/common/model/grid/DbfsTestGrid.scala +++ b/src/test/scala/edu/ie3/simona/test/common/model/grid/DbfsTestGrid.scala @@ -22,11 +22,15 @@ import edu.ie3.datamodel.models.input.{ } import edu.ie3.datamodel.models.voltagelevels.GermanVoltageLevelUtils import edu.ie3.datamodel.utils.GridAndGeoUtils +import edu.ie3.simona.model.grid.{RefSystem, TransformerModel} import edu.ie3.simona.util.TestGridFactory import edu.ie3.util.quantities.PowerSystemUnits._ +import squants.electro.Kilovolts +import squants.energy.Kilowatts import tech.units.indriya.quantity.Quantities import tech.units.indriya.unit.Units._ +import java.time.ZonedDateTime import java.util.UUID import scala.collection.mutable import scala.jdk.CollectionConverters._ @@ -47,7 +51,7 @@ import scala.jdk.CollectionConverters._ * (3)----(4) * }}} */ -trait DbfsTestGrid extends SubGridGateMokka { +trait DbfsTestGrid extends SubGridGateMokka with GridComponentsMokka { // 4 HV nodes, 1 slack EHV node protected val node1 = new NodeInput( UUID.fromString("78c5d473-e01b-44c4-afd2-e4ff3c4a5d7c"), @@ -123,11 +127,31 @@ trait DbfsTestGrid extends SubGridGateMokka { * MS1_01 @ 11 -> 1676e48c-5353-4f06-b671-c579cf6a7072 @ 11 * MS3_01 @ 13 -> 9237e237-01e9-446f-899f-c3b5cf69d288 @ 13 */ + protected val node13a: NodeInput = mockNode( + UUID.fromString("1129b00d-3d89-4a4a-8ae1-2a56041b95aa"), + 13, + GermanVoltageLevelUtils.MV_10KV, + ) + protected val node12: NodeInput = mockNode( + UUID.fromString("139c435d-e550-48d8-b590-ee897621f42a"), + 12, + GermanVoltageLevelUtils.MV_10KV, + ) + protected val node11: NodeInput = mockNode( + UUID.fromString("1676e48c-5353-4f06-b671-c579cf6a7072"), + 11, + GermanVoltageLevelUtils.MV_10KV, + ) + protected val node13b: NodeInput = mockNode( + UUID.fromString("9237e237-01e9-446f-899f-c3b5cf69d288"), + 13, + GermanVoltageLevelUtils.MV_10KV, + ) // 5 lines between the nodes protected val lineType1 = new LineTypeInput( UUID.randomUUID(), - "Freileitung_110kV_1 ", + "Freileitung_110kV_1", Quantities.getQuantity(0.0, SIEMENS_PER_KILOMETRE), Quantities.getQuantity(0.0, SIEMENS_PER_KILOMETRE), Quantities.getQuantity(0.1094, OHM_PER_KILOMETRE), @@ -257,6 +281,23 @@ trait DbfsTestGrid extends SubGridGateMokka { -5, 5, ) + private val trafoType10kV = new Transformer2WTypeInput( + UUID.randomUUID(), + "HV-10kV", + Quantities.getQuantity(5.415, OHM), + Quantities.getQuantity(108.165, OHM), + Quantities.getQuantity(200000.0, KILOVOLTAMPERE), + Quantities.getQuantity(110.0, KILOVOLT), + Quantities.getQuantity(10.0, KILOVOLT), + Quantities.getQuantity(555.5, NANOSIEMENS), + Quantities.getQuantity(-1.27, NANOSIEMENS), + Quantities.getQuantity(1, PERCENT), + Quantities.getQuantity(0, RADIAN), + false, + 0, + -5, + 5, + ) protected val transformer1 = new Transformer2WInput( UUID.fromString("6e9d912b-b652-471b-84d2-6ed571e53a7b"), @@ -268,7 +309,7 @@ trait DbfsTestGrid extends SubGridGateMokka { 1, trafoType, 0, - false, + true, ) protected val transformer2 = new Transformer2WInput( UUID.fromString("ceccd8cb-29dc-45d6-8a13-4b0033c5f1ef"), @@ -280,9 +321,74 @@ trait DbfsTestGrid extends SubGridGateMokka { 1, trafoType, 0, + true, + ) + protected val transformer11 = new Transformer2WInput( + UUID.randomUUID(), + "HV-MV-Trafo_11", + OperatorInput.NO_OPERATOR_ASSIGNED, + OperationTime.notLimited(), + node1, + node11, + 1, + trafoType10kV, + 0, + true, + ) + protected val transformer12 = new Transformer2WInput( + UUID.randomUUID(), + "HV-MV-Trafo_12", + OperatorInput.NO_OPERATOR_ASSIGNED, + OperationTime.notLimited(), + node2, + node12, + 1, + trafoType10kV, + 0, + false, + ) + protected val transformer13a = new Transformer2WInput( + UUID.randomUUID(), + "HV-MV-Trafo_13_1", + OperatorInput.NO_OPERATOR_ASSIGNED, + OperationTime.notLimited(), + node4, + node13a, + 1, + trafoType10kV, + 0, + false, + ) + protected val transformer13b = new Transformer2WInput( + UUID.randomUUID(), + "HV-MV-Trafo_13_2", + OperatorInput.NO_OPERATOR_ASSIGNED, + OperationTime.notLimited(), + node4, + node13b, + 1, + trafoType10kV, + 0, false, ) + protected val start: ZonedDateTime = ZonedDateTime.now() + protected val end: ZonedDateTime = start.plusHours(3) + + protected val mvTransformers: Map[UUID, TransformerModel] = Seq( + transformer11, + transformer12, + transformer13a, + transformer13b, + ).map { model => + model.getUuid -> TransformerModel( + model, + RefSystem(Kilowatts(30), Kilovolts(10)), + start, + end, + ) + }.toMap + protected val (hvGridContainer, hvSubGridGates) = { // LinkedHashSet in order to preserve the given order. // This is important as long as only one slack node between two sub grids can exist @@ -309,30 +415,10 @@ trait DbfsTestGrid extends SubGridGateMokka { SubGridGate.fromTransformer3W(transformer, ConnectorPort.C), ) ) ++ Seq( - build2wSubGridGate( - node4.getUuid, - 1, - UUID.fromString("1129b00d-3d89-4a4a-8ae1-2a56041b95aa"), - 13, - ), - build2wSubGridGate( - node2.getUuid, - 1, - UUID.fromString("139c435d-e550-48d8-b590-ee897621f42a"), - 12, - ), - build2wSubGridGate( - node1.getUuid, - 1, - UUID.fromString("1676e48c-5353-4f06-b671-c579cf6a7072"), - 11, - ), - build2wSubGridGate( - node3.getUuid, - 1, - UUID.fromString("9237e237-01e9-446f-899f-c3b5cf69d288"), - 13, - ), + new SubGridGate(transformer13a, node4, node13a), + new SubGridGate(transformer12, node2, node12), + new SubGridGate(transformer11, node1, node11), + new SubGridGate(transformer13b, node3, node13b), ) ( diff --git a/src/test/scala/edu/ie3/simona/test/common/model/grid/FiveLinesWithNodes.scala b/src/test/scala/edu/ie3/simona/test/common/model/grid/FiveLinesWithNodes.scala index 615be682f4..25dad51c98 100644 --- a/src/test/scala/edu/ie3/simona/test/common/model/grid/FiveLinesWithNodes.scala +++ b/src/test/scala/edu/ie3/simona/test/common/model/grid/FiveLinesWithNodes.scala @@ -60,6 +60,7 @@ trait FiveLinesWithNodes { isSlack, Each(1.0d), GermanVoltageLevelUtils.parse(vNominal), + -1, ) } 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..f5ff6885f0 --- /dev/null +++ b/src/test/scala/edu/ie3/simona/test/common/model/grid/GridComponentsMokka.scala @@ -0,0 +1,162 @@ +/* + * © 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.Transformer3wPowerFlowCase._ +import edu.ie3.simona.model.grid._ +import edu.ie3.util.quantities.QuantityUtils.RichQuantityDouble +import edu.ie3.util.scala.OperationInterval +import org.mockito.Mockito.when +import org.scalatestplus.mockito.MockitoSugar +import squants.energy.Watts +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 = Watts(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 = Watts(1), + r = Each(1), + x = Each(1), + g = Each(1), + b = Each(1), + ) + + protected def mockTransformerTappingModel( + uuid: UUID = UUID.randomUUID(), + autoTap: Boolean, + tapMax: Int, + tapMin: Int, + currentTapPos: Int, + deltaV: ComparableQuantity[Dimensionless], + ): 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() + ): TransformerModel = { + val transformer = mock[TransformerModel] + when(transformer.uuid).thenReturn(uuid) + + transformer + } + + protected def mockTransformer3wModel( + uuid: UUID = UUID.randomUUID() + ): (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.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/model/grid/SubGridGateMokka.scala b/src/test/scala/edu/ie3/simona/test/common/model/grid/SubGridGateMokka.scala index 6cf1b682d4..001cb12e4f 100644 --- a/src/test/scala/edu/ie3/simona/test/common/model/grid/SubGridGateMokka.scala +++ b/src/test/scala/edu/ie3/simona/test/common/model/grid/SubGridGateMokka.scala @@ -6,7 +6,6 @@ package edu.ie3.simona.test.common.model.grid -import java.util.UUID import edu.ie3.datamodel.graph.SubGridGate import edu.ie3.datamodel.models.input.NodeInput import edu.ie3.datamodel.models.input.connector.{ @@ -14,9 +13,12 @@ import edu.ie3.datamodel.models.input.connector.{ Transformer2WInput, Transformer3WInput, } +import edu.ie3.datamodel.models.voltagelevels.VoltageLevel import org.mockito.Mockito._ import org.scalatestplus.mockito.MockitoSugar +import java.util.UUID + /** Hold my cup of coffee and let me mock you some models. */ trait SubGridGateMokka extends MockitoSugar { @@ -37,6 +39,29 @@ trait SubGridGateMokka extends MockitoSugar { node } + /** Mocks a node with it's basic needed information + * + * @param uuid + * Unique identifier of the node + * @param subnet + * Sub net number + * @param voltLvl + * [[VoltageLevel]] of the node + * @return + * [[NodeInput]] with these information + */ + protected def mockNode( + uuid: UUID, + subnet: Int, + voltLvl: VoltageLevel, + ): NodeInput = { + val node = mock[NodeInput] + when(node.getUuid).thenReturn(uuid) + when(node.getSubnet).thenReturn(subnet) + when(node.getVoltLvl).thenReturn(voltLvl) + node + } + /** Mocks a transformer, that only holds information on what nodes are * connected * @@ -57,6 +82,18 @@ trait SubGridGateMokka extends MockitoSugar { transformer } + protected def mockTransformer2w( + uuid: UUID, + nodeA: NodeInput, + nodeB: NodeInput, + ): Transformer2WInput = { + val transformer = mock[Transformer2WInput] + when(transformer.getNodeA).thenReturn(nodeA) + when(transformer.getNodeB).thenReturn(nodeB) + when(transformer.getUuid).thenReturn(uuid) + transformer + } + /** Mocks a transformer, that only holds information on what nodes are * connected * @@ -89,6 +126,26 @@ trait SubGridGateMokka extends MockitoSugar { transformer } + protected def mockTransformer3w( + uuid: UUID, + nodeA: NodeInput, + nodeASubnet: Int, + nodeB: NodeInput, + nodeC: NodeInput, + ): Transformer3WInput = { + val internalNode = mock[NodeInput] + when(internalNode.getUuid).thenReturn(UUID.randomUUID()) + when(internalNode.getSubnet).thenReturn(nodeASubnet) + + val transformer = mock[Transformer3WInput] + when(transformer.getNodeA).thenReturn(nodeA) + when(transformer.getNodeB).thenReturn(nodeB) + when(transformer.getNodeC).thenReturn(nodeC) + when(transformer.getNodeInternal).thenReturn(internalNode) + when(transformer.getUuid).thenReturn(uuid) + transformer + } + /** Builds a sub grid gate by mocking the underlying nodes and transformer * * @param nodeAUuid diff --git a/src/test/scala/edu/ie3/simona/test/common/model/grid/TransformerTestData.scala b/src/test/scala/edu/ie3/simona/test/common/model/grid/TransformerTestData.scala index 26f26cc141..cc53f961b6 100644 --- a/src/test/scala/edu/ie3/simona/test/common/model/grid/TransformerTestData.scala +++ b/src/test/scala/edu/ie3/simona/test/common/model/grid/TransformerTestData.scala @@ -8,7 +8,7 @@ package edu.ie3.simona.test.common.model.grid import breeze.math.Complex import edu.ie3.datamodel.models.input.connector.ConnectorPort -import edu.ie3.simona.model.grid.RefSystem +import edu.ie3.simona.model.grid.{RefSystem, VoltageLimits} import edu.ie3.util.quantities.PowerSystemUnits._ import org.scalatest.prop.TableDrivenPropertyChecks.Table import org.scalatest.prop.{TableFor5, TableFor9} @@ -34,6 +34,8 @@ trait TransformerTestData extends TransformerTestGrid { Kilovolts(0.4d), ) + val voltageLimits: VoltageLimits = VoltageLimits(0.9, 1.1) + val nodeUuidToIndexMap: Map[UUID, Int] = gridTapHv.getRawGrid.getNodes.asScala .map(node => node.getUuid -> node.getSubnet) .toMap 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 + } +} diff --git a/src/test/scala/edu/ie3/simona/util/ConfigUtilSpec.scala b/src/test/scala/edu/ie3/simona/util/ConfigUtilSpec.scala index a19cca5be3..532ea5ed20 100644 --- a/src/test/scala/edu/ie3/simona/util/ConfigUtilSpec.scala +++ b/src/test/scala/edu/ie3/simona/util/ConfigUtilSpec.scala @@ -14,7 +14,11 @@ import edu.ie3.datamodel.models.result.connector.{ Transformer3WResult, } import edu.ie3.datamodel.models.result.system.{ChpResult, LoadResult} -import edu.ie3.datamodel.models.result.{NodeResult, ResultEntity} +import edu.ie3.datamodel.models.result.{ + CongestionResult, + NodeResult, + ResultEntity, +} import edu.ie3.simona.config.SimonaConfig import edu.ie3.simona.config.SimonaConfig.{apply => _, _} import edu.ie3.simona.event.notifier.NotifierConfig @@ -24,8 +28,8 @@ import edu.ie3.simona.util.ConfigUtil.NotifierIdentifier._ import edu.ie3.simona.util.ConfigUtil.{ GridOutputConfigUtil, NotifierIdentifier, - ParticipantConfigUtil, OutputConfigUtil, + ParticipantConfigUtil, } import org.scalatest.prop.{TableDrivenPropertyChecks, TableFor2} @@ -571,37 +575,98 @@ class ConfigUtilSpec Table( ("config", "expected"), ( - new GridOutputConfig(false, false, "grid", false, false, false), + new GridOutputConfig( + false, + false, + false, + "grid", + false, + false, + false, + ), Set.empty[Class[_ <: ResultEntity]], ), ( - new GridOutputConfig(true, false, "grid", false, false, false), + new GridOutputConfig( + false, + true, + false, + "grid", + false, + false, + false, + ), Set(classOf[LineResult]), ), ( - new GridOutputConfig(false, true, "grid", false, false, false), + new GridOutputConfig( + false, + false, + true, + "grid", + false, + false, + false, + ), Set(classOf[NodeResult]), ), ( - new GridOutputConfig(false, false, "grid", true, false, false), + new GridOutputConfig( + false, + false, + false, + "grid", + true, + false, + false, + ), Set(classOf[SwitchResult]), ), ( - new GridOutputConfig(false, false, "grid", false, true, false), + new GridOutputConfig( + false, + false, + false, + "grid", + false, + true, + false, + ), Set(classOf[Transformer2WResult]), ), ( - new GridOutputConfig(false, false, "grid", false, false, true), + new GridOutputConfig( + false, + false, + false, + "grid", + false, + false, + true, + ), Set(classOf[Transformer3WResult]), ), ( - new GridOutputConfig(true, true, "grid", true, true, true), + new GridOutputConfig( + true, + false, + false, + "grid", + false, + false, + false, + ), + Set(classOf[CongestionResult]), + ), + ( + new GridOutputConfig(true, true, true, "grid", true, true, true), Set( classOf[LineResult], classOf[NodeResult], classOf[SwitchResult], classOf[Transformer2WResult], classOf[Transformer3WResult], + classOf[CongestionResult], ), ), )