From 1f525cdc5f8cce3807150b543ae26ccc630932d1 Mon Sep 17 00:00:00 2001 From: "Kittl, Chris" Date: Mon, 13 Dec 2021 14:50:00 +0100 Subject: [PATCH 01/36] Fix communication protocol between agents in DBFS algorithm when there are multiple connections between two adjacent GridAgents. Co-authored-by: sebastian-peter --- input/samples/vn_simona/vn_simona.conf | 32 +- .../ie3/simona/agent/grid/DBFSAlgorithm.scala | 217 ++++--- .../ie3/simona/agent/grid/GridAgentData.scala | 188 ++++-- .../agent/grid/GridResultsSupport.scala | 55 +- .../simona/agent/grid/PowerFlowSupport.scala | 38 +- .../agent/grid/ReceivedValuesStore.scala | 37 +- .../model/participant/SystemParticipant.scala | 4 +- .../ontology/messages/PowerMessage.scala | 28 +- .../agent/grid/DBFSAlgorithmCenGridSpec.scala | 588 ++++++++++-------- .../agent/grid/DBFSAlgorithmSupGridSpec.scala | 66 +- 10 files changed, 807 insertions(+), 446 deletions(-) diff --git a/input/samples/vn_simona/vn_simona.conf b/input/samples/vn_simona/vn_simona.conf index f259b25306..bf2cce2621 100644 --- a/input/samples/vn_simona/vn_simona.conf +++ b/input/samples/vn_simona/vn_simona.conf @@ -65,21 +65,23 @@ simona.output.participant.defaultConfig = { powerRequestReply = false simulationResult = true } -simona.output.participant.individualConfigs = [{ - notifier = "pv" - powerRequestReply = false - simulationResult = true -}] -simona.output.participant.individualConfigs = [{ - notifier = "wec" - powerRequestReply = false - simulationResult = true -}] -simona.output.participant.individualConfigs = [{ - notifier = "evcs" - powerRequestReply = false - simulationResult = true -}] +simona.output.participant.individualConfigs = [ + { + notifier = "pv" + powerRequestReply = false + simulationResult = true + }, + { + notifier = "wec" + powerRequestReply = false + simulationResult = true + }, + { + notifier = "evcs" + powerRequestReply = false + simulationResult = true + } +] ################################################################## # Runtime Configuration // todo refactor as this naming is misleading and partly unneeded 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 f9caed695b..a93e79b190 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/DBFSAlgorithm.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/DBFSAlgorithm.scala @@ -17,6 +17,7 @@ import edu.ie3.powerflow.model.NodeData.StateData 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.GridAgentData.{ GridAgentBaseData, PowerFlowDoneData @@ -116,6 +117,7 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { // we just received either all provided slack voltage values or all provided power values val updatedGridAgentBaseData: GridAgentBaseData = receivedValues match { case receivedPowers: ReceivedPowerValues => + /* Can be a message from an asset or a message from an inferior grid */ gridAgentBaseData.updateWithReceivedPowerValues(receivedPowers) case receivedSlacks: ReceivedSlackValues => gridAgentBaseData.updateWithReceivedSlackVoltages(receivedSlacks) @@ -151,7 +153,6 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { allValuesReceived, updatedGridAgentBaseData ) - } // if we receive a request for slack voltages from our inferior grids we want to answer it @@ -256,14 +257,31 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { // / before power flow calc for this sweep we either have to stash() the message to answer it later (in current sweep) // / or trigger a new run for the next sweepNo case Event( - RequestGridPowerMessage(requestSweepNo, _), + RequestGridPowerMessage(requestSweepNo, requestedNodeUuids), gridAgentBaseData: GridAgentBaseData ) => + val firstRequestedNodeUuid = requestedNodeUuids.headOption match { + case Some(uuid) => uuid + case None => + throw new RuntimeException( + "Did receive a grid power request but without specified nodes" + ) + } + val queryingSubnet = gridAgentBaseData.gridEnv.subnetGateToActorRef + .find(_._1.getSuperiorNode.getUuid == firstRequestedNodeUuid) + .map(_._1.getSuperiorNode.getSubnet) + .getOrElse(-1000) + if (gridAgentBaseData.currentSweepNo == requestSweepNo) { log.debug( s"Received request for grid power values for sweepNo {} before my first power flow calc. Stashing away.", requestSweepNo ) + if (gridAgentBaseData.gridEnv.gridModel.subnetNo == 3000) { + log.info( + s"GridAgent 3000 received a grid power request from subnet $queryingSubnet, before power flow has been calculated. Stash it away." + ) + } stash() stay() } else { @@ -272,6 +290,11 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { requestSweepNo, gridAgentBaseData.currentSweepNo ) + if (gridAgentBaseData.gridEnv.gridModel.subnetNo == 3000) { + log.info( + s"GridAgent 3000 received a grid power request from subnet $queryingSubnet, before power flow has been calculated. Initiate new sweep." + ) + } self ! PrepareNextSweepTrigger(currentTick) stash() @@ -280,61 +303,114 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { // / after power flow calc for this sweepNo case Event( - RequestGridPowerMessage(_, requestedNodeUuid), - PowerFlowDoneData(gridAgentBaseData, powerFlowResult) + RequestGridPowerMessage(_, requestedNodeUuids), + powerFlowDoneData @ PowerFlowDoneData( + gridAgentBaseData, + powerFlowResult, + pendingRequestAnswers + ) ) => - log.debug( - "Received request for grid power values and im READY to provide." - ) - - // get the index from the power flow data of the requested node - val requestedNodeIdxOpt = - gridAgentBaseData.gridEnv.gridModel.nodeUuidToIndexMap - .get(requestedNodeUuid) - - powerFlowResult match { - case validNewtonRaphsonPFResult: ValidNewtonRaphsonPFResult => - val (p, q) = validNewtonRaphsonPFResult.nodeData.find(stateData => - requestedNodeIdxOpt.contains(stateData.index) - ) match { - case Some(apparentPowerS) => - val refSystem = gridAgentBaseData.gridEnv.gridModel.mainRefSystem - val (pInPu, qInPu) = - (apparentPowerS.power.real, apparentPowerS.power.imag) - // The power flow result data provides the nodal residual power at the slack node. - // A feed-in case from the inferior grid TO the superior grid leads to positive residual power at the - // inferior grid's *slack node* (superior grid seems to be a load to the inferior grid). - // To model the exchanged power from the superior grid's point of view, -1 has to be multiplied. - // (Inferior grid is a feed in facility to superior grid, which is negative then). Analogously for load case. - ( - refSystem.pInSi(pInPu).multiply(-1), - refSystem.qInSi(qInPu).multiply(-1) - ) - case None => - throw new DBFSAlgorithmException( - s"Got a request for power @ node with uuid $requestedNodeUuid but cannot find it in my result data!" - ) + /* Determine the subnet number of the grid agent, that has sent the request */ + val firstRequestedNodeUuid = requestedNodeUuids.headOption match { + case Some(uuid) => uuid + case None => + throw new RuntimeException( + "Did receive a grid power request but without specified nodes" + ) + } + gridAgentBaseData.gridEnv.subnetGateToActorRef + .find(_._1.getSuperiorNode.getUuid == firstRequestedNodeUuid) + .map(_._1.getSuperiorNode.getSubnet) match { + case Some(requestingSubnetNumber) => + if (gridAgentBaseData.gridEnv.gridModel.subnetNo == 3000) { + log.info( + s"GridAgent 3000 received a grid power request from subnet $requestingSubnetNumber, after power flow has been calculated." + ) } + log.debug( + "Received request for grid power values and im READY to provide." + ) - // update the sweep value store and clear all received maps - // note: normally it is expected that this has to be done after power flow calculations but for the sake - // of having it only once in the code we put this here. Otherwise it would have to been putted before EVERY - // return with a valid power flow result (currently happens already in two situations) - val updatedGridAgentBaseData = - gridAgentBaseData.storeSweepDataAndClearReceiveMaps( - validNewtonRaphsonPFResult, - gridAgentBaseData.superiorGridNodeUuids, - gridAgentBaseData.inferiorGridGates - ) + powerFlowResult match { + case validNewtonRaphsonPFResult: ValidNewtonRaphsonPFResult => + val exchangePowers = requestedNodeUuids + .map { nodeUuid => + /* Figure out the node index for each requested node */ + nodeUuid -> gridAgentBaseData.gridEnv.gridModel.nodeUuidToIndexMap + .get(nodeUuid) + .flatMap { nodeIndex => + /* Find matching node result */ + validNewtonRaphsonPFResult.nodeData.find(stateData => + stateData.index == nodeIndex + ) + } + .map { + case StateData(_, nodeType, _, power) + if nodeType == NodeType.SL => + val refSystem = + gridAgentBaseData.gridEnv.gridModel.mainRefSystem + val (pInPu, qInPu) = + (power.real, power.imag) + // The power flow result data provides the nodal residual power at the slack node. + // A feed-in case from the inferior grid TO the superior grid leads to positive residual power at the + // inferior grid's *slack node* (superior grid seems to be a load to the inferior grid). + // To model the exchanged power from the superior grid's point of view, -1 has to be multiplied. + // (Inferior grid is a feed in facility to superior grid, which is negative then). Analogously for load case. + ( + refSystem.pInSi(pInPu).multiply(-1), + refSystem.qInSi(qInPu).multiply(-1) + ) + case _ => + /* TODO: As long as there are no multiple slack nodes, provide "real" power only for the slack node */ + ( + Quantities.getQuantity(0d, PowerSystemUnits.MEGAWATT), + Quantities.getQuantity(0d, PowerSystemUnits.MEGAVAR) + ) + } + .getOrElse { + throw new DBFSAlgorithmException( + s"Got a request for power @ node with uuid $requestedNodeUuids but cannot find it in my result data!" + ) + } + } + .map { case (nodeUuid, (p, q)) => + ProvideGridPowerMessage.ExchangePower(nodeUuid, p, q) + } + + /* Determine the remaining replies */ + val stillPendingRequestAnswers = + pendingRequestAnswers.filterNot(_ == requestingSubnetNumber) + + // update the sweep value store and clear all received maps + // note: normally it is expected that this has to be done after power flow calculations but for the sake + // of having it only once in the code we put this here. Otherwise it would have to been putted before EVERY + // return with a valid power flow result (currently happens already in two situations) + val updatedGridAgentBaseData = + if (stillPendingRequestAnswers.isEmpty) { + gridAgentBaseData.storeSweepDataAndClearReceiveMaps( + validNewtonRaphsonPFResult, + gridAgentBaseData.superiorGridNodeUuids, + gridAgentBaseData.inferiorGridGates + ) + } else { + powerFlowDoneData.copy(pendingRequestAnswers = + stillPendingRequestAnswers + ) + } - stay() replying - ProvideGridPowerMessage( - requestedNodeUuid, - p, - q - ) using updatedGridAgentBaseData + stay() replying + ProvideGridPowerMessage( + exchangePowers + ) using updatedGridAgentBaseData - case _: FailedNewtonRaphsonPFResult => + case _: FailedNewtonRaphsonPFResult => + stay() replying FailedPowerFlow using gridAgentBaseData + } + case None => + /* It is not possible to determine, who has asked */ + log.error( + "I got a grid power request from a subnet I don't know. Can't answer it properly." + ) stay() replying FailedPowerFlow using gridAgentBaseData } @@ -1105,29 +1181,30 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { s"asking inferior grids for power values: {}", inferiorGridGates ) - if (inferiorGridGates.nonEmpty) - Some( - Future - .sequence( - inferiorGridGates.map(inferiorGridGate => { - val inferiorGridAgent = - subGridGateToActorRef(inferiorGridGate) - (inferiorGridAgent ? RequestGridPowerMessage( + Option.when(inferiorGridGates.nonEmpty) { + Future + .sequence( + inferiorGridGates + .map { subGridGate => + subGridGateToActorRef(subGridGate) -> subGridGate + } + .groupBy(_._1) // Group the gates by target actor, so that only one request is sent per grid agent + .map { case (inferiorGridAgentRef, inferiorGridGates) => + (inferiorGridAgentRef ? RequestGridPowerMessage( currentSweepNo, - inferiorGridGate.getSuperiorNode.getUuid + inferiorGridGates.map(_._2.getSuperiorNode.getUuid).distinct )).map { case provideGridPowerMessage: ProvideGridPowerMessage => - (inferiorGridAgent, Option(provideGridPowerMessage)) + (inferiorGridAgentRef, Option(provideGridPowerMessage)) case FailedPowerFlow => - (inferiorGridAgent, Some(FailedPowerFlow)) + (inferiorGridAgentRef, Some(FailedPowerFlow)) }.mapTo[ActorPowerRequestResponse] - }) - ) - .map(ReceivedGridPowerValues) - .pipeTo(self) - ) - else - None + } + .toVector + ) + .map(ReceivedGridPowerValues) + .pipeTo(self) + } } /** Triggers an execution of the akka `ask` pattern for all slack voltages of 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 89064b0318..34db7f8ffc 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/GridAgentData.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/GridAgentData.scala @@ -22,6 +22,8 @@ import edu.ie3.simona.model.grid.{GridModel, RefSystem} import edu.ie3.simona.ontology.messages.PowerMessage import edu.ie3.simona.ontology.messages.PowerMessage.{ FailedPowerFlow, + PowerResponseMessage, + ProvideGridPowerMessage, ProvidePowerMessage } @@ -63,12 +65,32 @@ object GridAgentData { * the base data of the [[GridAgent]] * @param powerFlowResult * result of the executed power flow + * @param pendingRequestAnswers + * Set of subnet numbers of those [[GridAgent]] s, that still don't have + * their request answered, yet */ - final case class PowerFlowDoneData( + final case class PowerFlowDoneData private ( gridAgentBaseData: GridAgentBaseData, - powerFlowResult: PowerFlowResult + powerFlowResult: PowerFlowResult, + pendingRequestAnswers: Set[Int] ) extends GridAgentData + object PowerFlowDoneData { + def apply( + gridAgentBaseData: GridAgentBaseData, + powerFlowResult: PowerFlowResult + ): PowerFlowDoneData = { + /* Determine the subnet numbers of all superior grids */ + val superiorSubGrids = gridAgentBaseData.gridEnv.subnetGateToActorRef + .filter( + _._1.getSuperiorNode.getSubnet != gridAgentBaseData.gridEnv.gridModel.subnetNo + ) + .map(_._1.getSuperiorNode.getSubnet) + .toSet + PowerFlowDoneData(gridAgentBaseData, powerFlowResult, superiorSubGrids) + } + } + /** The base data that is mainly used by the [[GridAgent]]. This data has to * be copied several times at several places for each state transition with * updated data. So be careful in adding more data on it! @@ -219,58 +241,46 @@ object GridAgentData { receivedValueStore.nodeToReceivedPower ) { case ( - updatedNodeToReceivedPowerValuesMap, - (senderRef, powerValueMessageOpt) + nodeToReceivedPowerValuesMapWithAddedPowerResponse, + (senderRef, Some(providePowerMessage: ProvidePowerMessage)) ) => - // extract the nodeUuid that corresponds to the sender's actorRef and check if we expect a message from the sender - val nodeUuid = powerValueMessageOpt match { - case Some( - powerValuesMessage: ProvidePowerMessage - ) => // can either be an AssetPowerChangedMessage or an AssetPowerUnchangedMessage - uuid(updatedNodeToReceivedPowerValuesMap, senderRef, replace) - .getOrElse( - throw new RuntimeException( - s"$actorName Received asset power values msg $powerValuesMessage " + - s"from $senderRef which is not in my power values nodes map or which cannot be replaced!" - ) - ) - ._1 - case Some(FailedPowerFlow) => - uuid(updatedNodeToReceivedPowerValuesMap, senderRef, replace) - .getOrElse( - throw new RuntimeException( - s"$actorName Received failed power flow message " + - s"from $senderRef which is not in my power values nodes map or which cannot be replaced!" - ) - ) - ._1 - case None => - throw new RuntimeException( - s"Received a 'None' as provided asset power from '$senderRef'. Provision of 'None' is not supported." + - s"Either a changed power or an unchanged power message is expected!" + /* This is a message with only one nodal power */ + updateNodalReceivedVector( + providePowerMessage, + nodeToReceivedPowerValuesMapWithAddedPowerResponse, + senderRef, + replace + ) + case ( + nodeToReceivedPowerValuesMapWithAddedPowerResponse, + ( + senderRef, + Some(provideGridPowerMessage: ProvideGridPowerMessage) ) - case unknownMsg => - throw new RuntimeException( - s"$actorName Unknown message received. Can't process message $unknownMsg." + ) => + /* Go over all includes messages and add them. */ + provideGridPowerMessage.nodalResidualPower.foldLeft( + nodeToReceivedPowerValuesMapWithAddedPowerResponse + ) { + case ( + nodeToReceivedPowerValuesMapWithAddedExchangedPower, + exchangedPower + ) => + updateNodalReceivedVector( + exchangedPower, + nodeToReceivedPowerValuesMapWithAddedExchangedPower, + senderRef, + replace ) } - - // update the values in the received map - val receivedVector = updatedNodeToReceivedPowerValuesMap - .getOrElse( - nodeUuid, - throw new RuntimeException( - s"NodeId $nodeUuid is not part of nodeToReceivedPowerValuesMap!" - ) - ) - .filterNot { case (k, v) => - k == senderRef & - (if (!replace) v.isEmpty else v.isDefined) - } :+ (senderRef, powerValueMessageOpt) - - updatedNodeToReceivedPowerValuesMap - .updated(nodeUuid, receivedVector) - + case (_, (senderRef, Some(unsupported))) => + throw new RuntimeException( + s"Received an unsupported type of power provision message from '$senderRef', which I cannot add to the register of received messages: $unsupported" + ) + case (_, (senderRef, None)) => + throw new RuntimeException( + s"Received a 'None' as provided power from '$senderRef'. Provision of 'None' is not supported." + ) } this.copy( receivedValueStore = receivedValueStore @@ -300,15 +310,83 @@ object GridAgentData { (UUID, Vector[(ActorRef, Option[PowerMessage.PowerResponseMessage])]) ] = { nodeToReceivedPower - .find( - _._2.exists(actorRefPowerValueOpt => - actorRefPowerValueOpt._1 == senderRef && + .find { case (_, receivedPowerMessages) => + receivedPowerMessages.exists { case (ref, maybePowerResponse) => + ref == senderRef && (if (!replace) - actorRefPowerValueOpt._2.isEmpty + maybePowerResponse.isEmpty else - actorRefPowerValueOpt._2.isDefined) + maybePowerResponse.isDefined) + } + } + } + + /** Identify and update the vector of already received information. + * + * @param powerResponse + * Optional power response message + * @param nodeToReceived + * Mapping from node uuid to received values + * @param senderRef + * Reference of current sender + * @param replace + * If existing values may be replaced or not + * @return + * The nodal uuid as well as the updated collection of received + * information + */ + private def updateNodalReceivedVector( + powerResponse: PowerResponseMessage, + nodeToReceived: Map[UUID, Vector[ + (ActorRef, Option[PowerResponseMessage]) + ]], + senderRef: ActorRef, + replace: Boolean + ): Map[UUID, Vector[(ActorRef, Option[PowerResponseMessage])]] = { + // extract the nodeUuid that corresponds to the sender's actorRef and check if we expect a message from the sender + val nodeUuid = powerResponse match { + case powerValuesMessage: ProvidePowerMessage => + uuid(nodeToReceived, senderRef, replace) + .getOrElse( + throw new RuntimeException( + s"$actorName Received asset power values msg $powerValuesMessage " + + s"from $senderRef which is not in my power values nodes map or which cannot be replaced!" + ) + ) + ._1 + case FailedPowerFlow => + uuid(nodeToReceived, senderRef, replace) + .getOrElse( + throw new RuntimeException( + s"$actorName Received failed power flow message " + + s"from $senderRef which is not in my power values nodes map or which cannot be replaced!" + ) + ) + ._1 + case unknownMsg => + throw new RuntimeException( + s"$actorName Unknown message received. Can't process message $unknownMsg." + ) + } + + // update the values in the received map + val receivedVector = nodeToReceived + .getOrElse( + nodeUuid, + throw new RuntimeException( + s"NodeId $nodeUuid is not part of nodeToReceivedPowerValuesMap!" ) ) + /* Filter out the entry, that belongs to the given sender and is not defined, yet, in case no replacement shall + * be made. If a replacement is desired, filter out the already provided data. */ + .filterNot { case (k, v) => + k == senderRef & + (if (!replace) v.isEmpty else v.isDefined) + } :+ (senderRef, Some(powerResponse)) + + /* Actually update the map and hand it back */ + nodeToReceived + .updated(nodeUuid, receivedVector) } /** Update this [[GridAgentBaseData]] with [[ReceivedSlackValues]] and 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 a4784dd0d1..1bf271592a 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/GridResultsSupport.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/GridResultsSupport.scala @@ -520,12 +520,12 @@ private[grid] trait GridResultsSupport { (iMag, iAng) } - /** Calculate the voltage magnitude and the voltage angle in physical units + /** Calculate the current magnitude and the current angle in physical units * based on a provided electric current in p.u. and the nominal referenced * electric current. The arctangent "only" calculates the angle between the * complex current and it's real part. This means, that i = (i_real, i_imag) * and i' = (-i_real, -i_imag) will lead to the same angle. However, for - * power system simulation, the absolute orientation in the complex plain + * power system simulation, the absolute orientation in the complex plane * with regard to the positive real axis is of interest. Therefore, * additional 180° are added, if the real part of the current is negative. * @@ -542,15 +542,52 @@ private[grid] trait GridResultsSupport { ): (ComparableQuantity[ElectricCurrent], ComparableQuantity[Angle]) = ( iNominal.multiply(iPu.abs).asComparable, - Quantities.getQuantity( - atan(iPu.imag / iPu.real).toDegrees + max( - 0.0, - -signum(iPu.real) - ) * 180.0, - PowerSystemUnits.DEGREE_GEOM - ) + angle(iPu) ) + /** Correct the offset of an angle dependent on the direction. If the + * direction is negative, 180° are added + */ + private val offSetCorrection + : (ComparableQuantity[Angle], Double) => ComparableQuantity[Angle] = + (angle: ComparableQuantity[Angle], dir: Double) => + if (dir < 0) { + angle.add(Quantities.getQuantity(180d, PowerSystemUnits.DEGREE_GEOM)) + } else { + angle + } + + /** Calculate the angle of the complex value given. The angle has the full + * orientation on the complex plane. + * + * @param cplx + * The complex value + * @return + * The angle of the complex value + */ + private def angle(cplx: Complex): ComparableQuantity[Angle] = cplx match { + case _ if cplx.abs == 0 => + /* The complex value has no magnitude, therefore define the angle to zero */ + Quantities.getQuantity(0d, PowerSystemUnits.DEGREE_GEOM) + case Complex(real, imag) => + /* Calculate the angle between real and imaginary part */ + val baseAngle = atan(imag / real).toDegrees + + if (baseAngle.isNaN) { + /* There is only an imaginary part */ + offSetCorrection( + Quantities.getQuantity(90d, PowerSystemUnits.DEGREE_GEOM), + imag + ) + } else { + /* Correct the angle for full orientation in the complex plane */ + offSetCorrection( + Quantities.getQuantity(baseAngle, PowerSystemUnits.DEGREE_GEOM), + real + ) + } + } + /** Calculates the electric current of a two-port element @ port i (=A) and j * (=B) based on the provided voltages @ each port and the corresponding * admittances. All values in p.u. diff --git a/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala b/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala index ebf017b22d..e05afe3ffb 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala @@ -32,7 +32,7 @@ import javax.measure.Quantity import javax.measure.quantity.{Dimensionless, ElectricPotential} import tech.units.indriya.quantity.Quantities -import scala.annotation.tailrec +import scala.util.{Failure, Success, Try} /** Support and helper methods for power flow calculations provided by * [[edu.ie3.powerflow]] @@ -417,7 +417,6 @@ trait PowerFlowSupport { * @return * The result of newton raphson power flow calculation */ - @tailrec protected final def newtonRaphsonPF( gridModel: GridModel, maxIterations: Int, @@ -445,10 +444,30 @@ trait PowerFlowSupport { // / execute val powerFlow = NewtonRaphsonPF(epsilon, maxIterations, admittanceMatrix) - powerFlow.calculate( - operatingPoint, - Some(forcedSlackNodeVoltage) - ) match { + + /* Currently, only one slack node per sub net is allowed. In case a model has more than one, set all others to + * PQ nodes. ATTENTION: This does not cover the power flow situation correctly! */ + val adaptedOperatingPoint = operatingPoint + .foldLeft((Array.empty[PresetData], true)) { + case ((adaptedOperatingPoint, firstSlack), nodePreset) + if nodePreset.nodeType == NodeType.SL => + /* If this is the first slack node see, leave it as a slack node. If it is not the first one. Make it a + * PQ node. */ + val adaptedNodePreset = + if (firstSlack) nodePreset + else nodePreset.copy(nodeType = NodeType.PQ) + (adaptedOperatingPoint.appended(adaptedNodePreset), false) + case ((adaptedOperatingPoint, firstSlack), nodePreset) => + (adaptedOperatingPoint.appended(nodePreset), firstSlack) + } + ._1 + + Try { + powerFlow.calculate( + adaptedOperatingPoint, + Some(forcedSlackNodeVoltage) + ) + }.map { case _: PowerFlowResult.FailedPowerFlowResult if epsilons.size > 1 => // if we can relax, we relax val epsilonsLeft = epsilons.drop(1) @@ -462,6 +481,13 @@ trait PowerFlowSupport { ) case result => result + } match { + case Success(result) => result + case Failure(exception) => + throw new DBFSAlgorithmException( + s"Power flow calculation in subnet ${gridModel.subnetNo} failed.", + exception + ) } case None => throw new DBFSAlgorithmException( diff --git a/src/main/scala/edu/ie3/simona/agent/grid/ReceivedValuesStore.scala b/src/main/scala/edu/ie3/simona/agent/grid/ReceivedValuesStore.scala index e3d9c74bb9..391032e071 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/ReceivedValuesStore.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/ReceivedValuesStore.scala @@ -97,22 +97,29 @@ case object ReceivedValuesStore { (uuid, actorRefs.toVector.map(actorRef => actorRef -> None)) } - /* Add everything, that I expect from my sub ordinate grid agents */ - inferiorSubGridGateToActorRef.foldLeft(assetsToReceivedPower) { - case (subOrdinateToReceivedPower, (gate, inferiorSubGridRef)) => - val couplingNodeUuid = gate.getSuperiorNode.getUuid + /* Add everything, that I expect from my sub ordinate grid agents. Build distinct pairs of sending actor reference + * and target node */ + inferiorSubGridGateToActorRef.toVector + .map { case (gate, reference) => + reference -> gate.getSuperiorNode.getUuid + } + .distinct + .foldLeft(assetsToReceivedPower) { + case ( + subOrdinateToReceivedPower, + (inferiorSubGridRef, couplingNodeUuid) + ) => + /* Check, if there is yet something expected for the given coupling node and add reference to the subordinate + * grid agent */ + val actorRefToMessage = subOrdinateToReceivedPower + .getOrElse( + couplingNodeUuid, + Vector.empty[(ActorRef, Option[ProvidePowerMessage])] + ) :+ (inferiorSubGridRef -> None) - /* Check, if there is yet something expected for the given coupling node and add reference to the subordinate - * grid agent */ - val actorRefToMessage = subOrdinateToReceivedPower - .getOrElse( - couplingNodeUuid, - Vector.empty[(ActorRef, Option[ProvidePowerMessage])] - ) :+ (inferiorSubGridRef -> None) - - /* Update the existing map */ - subOrdinateToReceivedPower + (couplingNodeUuid -> actorRefToMessage) - } + /* Update the existing map */ + subOrdinateToReceivedPower + (couplingNodeUuid -> actorRefToMessage) + } } /** Composes an empty [[NodeToReceivedSlackVoltage]] with all options diff --git a/src/main/scala/edu/ie3/simona/model/participant/SystemParticipant.scala b/src/main/scala/edu/ie3/simona/model/participant/SystemParticipant.scala index 3331b2280d..a3fa10be44 100644 --- a/src/main/scala/edu/ie3/simona/model/participant/SystemParticipant.scala +++ b/src/main/scala/edu/ie3/simona/model/participant/SystemParticipant.scala @@ -154,10 +154,10 @@ abstract class SystemParticipant[CD <: CalcRelevantData]( KILOVOLTAMPERE ) - if (apparentPower.isGreaterThan(sMax)) { + if (apparentPower.isGreaterThan(sMax.multiply(1.05))) { logger.warn( s"The var characteristics \'$qControl\' of model \'$id\' ($uuid) imposes an apparent " + - s"power (= $apparentPower) that exceeds " + + s"power (= $apparentPower) that exceeds 105 % of " + s"rated apparent power specifications (= $sMax). " + s"Therefore, setting reactive power output to the to the upper limit " + s"in correspondence to the existing active power $activePower." diff --git a/src/main/scala/edu/ie3/simona/ontology/messages/PowerMessage.scala b/src/main/scala/edu/ie3/simona/ontology/messages/PowerMessage.scala index 90cddbf5de..8248357023 100644 --- a/src/main/scala/edu/ie3/simona/ontology/messages/PowerMessage.scala +++ b/src/main/scala/edu/ie3/simona/ontology/messages/PowerMessage.scala @@ -6,8 +6,9 @@ package edu.ie3.simona.ontology.messages -import java.util.UUID +import edu.ie3.simona.ontology.messages.PowerMessage.ProvideGridPowerMessage.ExchangePower +import java.util.UUID import javax.measure.quantity.{Dimensionless, Power} import tech.units.indriya.ComparableQuantity @@ -63,14 +64,29 @@ object PowerMessage { final case class RequestGridPowerMessage( currentSweepNo: Int, - nodeUuid: UUID + nodeUuids: Vector[UUID] ) extends PowerRequestMessage final case class ProvideGridPowerMessage( - nodeUuid: UUID, - override val p: ComparableQuantity[Power], - override val q: ComparableQuantity[Power] - ) extends ProvidePowerMessage + nodalResidualPower: Vector[ExchangePower] + ) extends PowerResponseMessage + object ProvideGridPowerMessage { + + /** Defining the exchanged power at one interconnection point + * + * @param nodeUuid + * Unique identifier of the node, at which this residual power did appear + * @param p + * active power from the previous request + * @param q + * reactive power from the previous request + */ + final case class ExchangePower( + nodeUuid: UUID, + override val p: ComparableQuantity[Power], + override val q: ComparableQuantity[Power] + ) extends ProvidePowerMessage + } final case object FailedPowerFlow extends PowerResponseMessage 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 f0a2bf2293..2a0141a2e3 100644 --- a/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmCenGridSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmCenGridSpec.scala @@ -8,7 +8,7 @@ package edu.ie3.simona.agent.grid import akka.actor.{ActorRef, ActorSystem} import akka.pattern.ask -import akka.testkit.{ImplicitSender, TestFSMRef} +import akka.testkit.{ImplicitSender, TestFSMRef, TestProbe} import akka.util.Timeout import com.typesafe.config.ConfigFactory import edu.ie3.simona.agent.EnvironmentRefs @@ -20,6 +20,7 @@ import edu.ie3.simona.agent.state.GridAgentState.{ SimulateGrid } import edu.ie3.simona.model.grid.RefSystem +import edu.ie3.simona.ontology.messages.PowerMessage.ProvideGridPowerMessage.ExchangePower import edu.ie3.simona.ontology.messages.PowerMessage.{ ProvideGridPowerMessage, RequestGridPowerMessage @@ -78,10 +79,17 @@ class DBFSAlgorithmCenGridSpec private val floatPrecision: Double = 0.00000000001 + private val scheduler = TestProbe("scheduler") + private val primaryService = TestProbe("primaryService") + private val weatherService = TestProbe("weatherService") + + private val superiorGridAgent = TestProbe("superiorGridAgent") + private val inferiorGridAgent = TestProbe("inferiorGridAgent") + private val environmentRefs = EnvironmentRefs( - scheduler = self, - primaryServiceProxy = self, - weather = self, + scheduler = scheduler.ref, + primaryServiceProxy = primaryService.ref, + weather = weatherService.ref, evDataService = None ) @@ -102,8 +110,13 @@ class DBFSAlgorithmCenGridSpec s"initialize itself when it receives a $InitializeGridAgentTrigger with corresponding data" in { val triggerId = 0 - // this subnet has 1 superior grid (HöS) and 4 inferior grids (MS) - val subGridGateToActorRef = hvSubGridGates.map(gate => gate -> self).toMap + // this subnet has 1 superior grid (HöS) and 4 inferior grids (MS). Map the gates to test probes accordingly + val subGridGateToActorRef = hvSubGridGates.map { + case gate + if gate.getInferiorNode.getSubnet == hvGridContainer.getSubnet => + gate -> superiorGridAgent.ref + case gate => gate -> inferiorGridAgent.ref + }.toMap val gridAgentInitData = GridAgentInitData( @@ -145,13 +158,16 @@ class DBFSAlgorithmCenGridSpec val activityStartTriggerId = 1 - centerGridAgent ! TriggerWithIdMessage( - ActivityStartTrigger(3600), - activityStartTriggerId, - centerGridAgent + scheduler.send( + centerGridAgent, + TriggerWithIdMessage( + ActivityStartTrigger(3600), + activityStartTriggerId, + centerGridAgent + ) ) - expectMsgPF() { + scheduler.expectMsgPF() { case CompletionMessage( triggerId, Some(Vector(ScheduleTriggerMessage(triggerToBeScheduled, _))) @@ -183,46 +199,41 @@ class DBFSAlgorithmCenGridSpec ) // send the start grid simulation trigger - centerGridAgent ! TriggerWithIdMessage( - StartGridSimulationTrigger(3600), - startGridSimulationTriggerId, - centerGridAgent - ) - - // we expect 4 requests for grid power values as we have 4 inferior grids - val firstGridPowerRequests = receiveWhile() { - case msg @ RequestGridPowerMessage(_, _) => msg -> lastSender - }.toMap - - firstGridPowerRequests.size shouldBe 4 - firstGridPowerRequests.keys should contain allOf ( - RequestGridPowerMessage( - firstSweepNo, - UUID.fromString("d44ba8ed-81db-4a22-a40d-f7c0d0808a75") - ), - RequestGridPowerMessage( - firstSweepNo, - UUID.fromString("e364ef00-e6ca-46b1-ba2b-bb73c0c6fee0") - ), - RequestGridPowerMessage( - firstSweepNo, - UUID.fromString("78c5d473-e01b-44c4-afd2-e4ff3c4a5d7c") - ), - RequestGridPowerMessage( - firstSweepNo, - UUID.fromString("47ef9983-8fcf-4713-be90-093fc27864ae") + scheduler.send( + centerGridAgent, + TriggerWithIdMessage( + StartGridSimulationTrigger(3600), + startGridSimulationTriggerId, + centerGridAgent ) ) + /* We expect one grid power request message, as all four sub grids are mapped onto one actor reference */ + val firstGridPowerRequests = inferiorGridAgent + .receiveWhile() { case msg @ RequestGridPowerMessage(_, _) => + msg -> inferiorGridAgent.lastSender + } + .toMap + + /* We receive one message with all requests for all inferior grid agents. This is, because the requests are + * grouped by the actor, they are sent to. Here, the test itself serves as the dummy for all inferior grid + * agents */ + firstGridPowerRequests.size shouldBe 1 + firstGridPowerRequests.keys.headOption match { + case Some(RequestGridPowerMessage(_, nodeUuids)) => + nodeUuids should contain theSameElementsAs inferiorGridNodeUuids + case None => fail("Did expect to receive something") + } + // we expect 1 request for slack voltage values // (slack values are requested by our agent under test from the superior grid) - val firstSlackVoltageRequest = expectMsgPF() { + val firstSlackVoltageRequest = superiorGridAgent.expectMsgPF() { case request @ RequestSlackVoltageMessage(sweepNo, nodeId) => sweepNo shouldBe firstSweepNo nodeId shouldBe UUID.fromString( "9fe5fa33-6d3b-4153-a829-a16f4347bc4e" ) - (request, lastSender) + (request, superiorGridAgent.lastSender) case x => fail( s"Invalid message received when expecting slack voltage request message. Message was $x" @@ -232,13 +243,16 @@ class DBFSAlgorithmCenGridSpec // 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 4 inferior grid agents inferiorGridNodeUuids.foreach { nodeUuid => - centerGridAgent ! RequestSlackVoltageMessage(firstSweepNo, nodeUuid) + inferiorGridAgent.send( + centerGridAgent, + RequestSlackVoltageMessage(firstSweepNo, nodeUuid) + ) } // 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) - val providedSlackVoltages = receiveWhile() { + val providedSlackVoltages = inferiorGridAgent.receiveWhile() { case provideSlackVoltageMessage: ProvideSlackVoltageMessage => provideSlackVoltageMessage case x => @@ -276,26 +290,39 @@ class DBFSAlgorithmCenGridSpec ) // we now answer the request of our centerGridAgent - // with 4 fake grid power messages and 1 fake slack voltage message - firstGridPowerRequests.foreach { requestSender => - val askSender = requestSender._2 - val requestNodeUuid = requestSender._1.nodeUuid - askSender ! ProvideGridPowerMessage( - requestNodeUuid, - Quantities.getQuantity(0, KILOWATT), - Quantities.getQuantity(0, KILOVAR) + // with 1 fake grid power message and 1 fake slack voltage message + firstGridPowerRequests.foreach { case (requestMessage, askSender) => + val requestNodeUuids = requestMessage.nodeUuids + val exchangePowers = requestNodeUuids.map { uuid => + ExchangePower( + uuid, + Quantities.getQuantity(0, KILOWATT), + Quantities.getQuantity(0, KILOVAR) + ) + } + inferiorGridAgent.send( + askSender, + ProvideGridPowerMessage( + exchangePowers + ) ) } - val slackAskSender = firstSlackVoltageRequest._2 - val slackRequestNodeUuid = firstSlackVoltageRequest._1.nodeUuid - val slackRequestSweepNo = firstSlackVoltageRequest._1.currentSweepNo - slackAskSender ! ProvideSlackVoltageMessage( - slackRequestSweepNo, - slackRequestNodeUuid, - Quantities.getQuantity(380, KILOVOLT), - Quantities.getQuantity(0, KILOVOLT) - ) + val slackRequestNodeUuid = firstSlackVoltageRequest match { + case (requestMessage, slackAskSender) => + val slackRequestNodeUuid = requestMessage.nodeUuid + val slackRequestSweepNo = requestMessage.currentSweepNo + superiorGridAgent.send( + slackAskSender, + ProvideSlackVoltageMessage( + slackRequestSweepNo, + slackRequestNodeUuid, + Quantities.getQuantity(380, KILOVOLT), + Quantities.getQuantity(0, KILOVOLT) + ) + ) + slackRequestNodeUuid + } // we expect to end up in SimulateGrid but we have to pass HandlePowerFlowCalculations beforehand // hence we wait until we reached this condition @@ -306,19 +333,31 @@ class DBFSAlgorithmCenGridSpec // our test agent should now be ready to provide the grid power values, hence we ask for them and expect a // corresponding response - centerGridAgent ! RequestGridPowerMessage(firstSweepNo, slackNodeUuid) + superiorGridAgent.send( + centerGridAgent, + RequestGridPowerMessage( + firstSweepNo, + Vector(slackNodeUuid) + ) + ) - expectMsgPF(Duration(15, TimeUnit.SECONDS)) { - case ProvideGridPowerMessage(nodeUuid, p, q) => - nodeUuid shouldBe slackNodeUuid - p should equalWithTolerance( - Quantities.getQuantity(0.080423711881702500000, MEGAVOLTAMPERE), - floatPrecision - ) - q should equalWithTolerance( - Quantities.getQuantity(-1.45357503915666260000, MEGAVOLTAMPERE), - floatPrecision - ) + superiorGridAgent.expectMsgPF(Duration(15, TimeUnit.SECONDS)) { + case ProvideGridPowerMessage(exchangedPowers) => + exchangedPowers.size shouldBe 1 + exchangedPowers.headOption match { + case Some(ExchangePower(nodeUuid, p, q)) => + nodeUuid shouldBe slackNodeUuid + p should equalWithTolerance( + Quantities.getQuantity(0.080423711881702500000, MEGAVOLTAMPERE), + floatPrecision + ) + q should equalWithTolerance( + Quantities.getQuantity(-1.45357503915666260000, MEGAVOLTAMPERE), + floatPrecision + ) + case None => + fail("Did not expect to get nothing") + } case x => fail( s"Invalid message received when expecting grid power values message. Message was $x" @@ -327,16 +366,22 @@ class DBFSAlgorithmCenGridSpec // we start a second sweep by asking for next sweep values which should trigger the whole procedure again val secondSweepNo = firstSweepNo + 1 - centerGridAgent ! RequestGridPowerMessage(secondSweepNo, slackNodeUuid) + superiorGridAgent.send( + centerGridAgent, + RequestGridPowerMessage( + secondSweepNo, + Vector(slackNodeUuid) + ) + ) // the agent now should ask for updated slack voltages from the superior grid - val secondSlackVoltageRequest = expectMsgPF() { + val secondSlackVoltageRequest = superiorGridAgent.expectMsgPF() { case request @ RequestSlackVoltageMessage(sweepNo, nodeId) => sweepNo shouldBe secondSweepNo nodeId shouldBe UUID.fromString( "9fe5fa33-6d3b-4153-a829-a16f4347bc4e" ) - (request, lastSender) + (request, superiorGridAgent.lastSender) case x => fail( s"Invalid message received when expecting slack voltage request message. Message was $x" @@ -348,38 +393,33 @@ class DBFSAlgorithmCenGridSpec // the superior grid would answer with updated slack voltage values val secondSlackAskSender = secondSlackVoltageRequest._2 - secondSlackAskSender ! ProvideSlackVoltageMessage( - secondSweepNo, - slackRequestNodeUuid, - Quantities.getQuantity(380, KILOVOLT), - Quantities.getQuantity(0, KILOVOLT) + superiorGridAgent.send( + secondSlackAskSender, + ProvideSlackVoltageMessage( + secondSweepNo, + slackRequestNodeUuid, + Quantities.getQuantity(380, KILOVOLT), + Quantities.getQuantity(0, KILOVOLT) + ) ) // after the intermediate power flow calculation // we expect 4 requests for grid power values as we have 4 inferior grids - val secondGridPowerRequests = receiveWhile() { - case msg @ RequestGridPowerMessage(_, _) => msg -> lastSender - }.toMap - - secondGridPowerRequests.size shouldBe 4 - secondGridPowerRequests.keys should contain allOf ( - RequestGridPowerMessage( - secondSweepNo, - UUID.fromString("d44ba8ed-81db-4a22-a40d-f7c0d0808a75") - ), - RequestGridPowerMessage( - secondSweepNo, - UUID.fromString("e364ef00-e6ca-46b1-ba2b-bb73c0c6fee0") - ), - RequestGridPowerMessage( - secondSweepNo, - UUID.fromString("78c5d473-e01b-44c4-afd2-e4ff3c4a5d7c") - ), - RequestGridPowerMessage( - secondSweepNo, - UUID.fromString("47ef9983-8fcf-4713-be90-093fc27864ae") - ) - ) + val secondGridPowerRequests = inferiorGridAgent + .receiveWhile() { case msg @ RequestGridPowerMessage(_, _) => + msg -> inferiorGridAgent.lastSender + } + .toMap + + /* We receive one message with all requests for all inferior grid agents. This is, because the requests are + * grouped by the actor, they are sent to. Here, the test itself serves as the dummy for all inferior grid + * agents */ + secondGridPowerRequests.size shouldBe 1 + secondGridPowerRequests.keys.headOption match { + case Some(RequestGridPowerMessage(_, nodeUuids)) => + nodeUuids should contain theSameElementsAs inferiorGridNodeUuids + case None => fail("Did expect to receive something") + } // the agent should then go back to SimulateGrid and wait for the powers of the inferior grid awaitAssert(centerGridAgent.stateName shouldBe SimulateGrid) @@ -387,13 +427,16 @@ class DBFSAlgorithmCenGridSpec // 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 4 inferior grid agents inferiorGridNodeUuids.foreach { nodeUuid => - centerGridAgent ! RequestSlackVoltageMessage(firstSweepNo, nodeUuid) + inferiorGridAgent.send( + centerGridAgent, + RequestSlackVoltageMessage(firstSweepNo, nodeUuid) + ) } // 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) - val secondProvidedSlackVoltages = receiveWhile() { + val secondProvidedSlackVoltages = inferiorGridAgent.receiveWhile() { case provideSlackVoltageMessage: ProvideSlackVoltageMessage => provideSlackVoltageMessage case x => @@ -463,14 +506,20 @@ class DBFSAlgorithmCenGridSpec }) // we now answer the request of our centerGridAgent - // with 4 fake grid power messages - secondGridPowerRequests.foreach { requestSender => - val askSender = requestSender._2 - val requestNodeUuid = requestSender._1.nodeUuid - askSender ! ProvideGridPowerMessage( - requestNodeUuid, - Quantities.getQuantity(0, KILOWATT), - Quantities.getQuantity(0, KILOVAR) + // with 1 fake grid power messages + secondGridPowerRequests.foreach { case (requestMessage, askSender) => + val requestNodeUuid = requestMessage.nodeUuids + inferiorGridAgent.send( + askSender, + ProvideGridPowerMessage( + requestNodeUuid.map { uuid => + ExchangePower( + uuid, + Quantities.getQuantity(0, KILOWATT), + Quantities.getQuantity(0, KILOVAR) + ) + } + ) ) } @@ -485,19 +534,31 @@ class DBFSAlgorithmCenGridSpec statesPassed1 should contain allOf (HandlePowerFlowCalculations, SimulateGrid) // as the akka testkit does the unstash handling incorrectly, we need to send a second request for grid power messages - centerGridAgent ! RequestGridPowerMessage(secondSweepNo, slackNodeUuid) + superiorGridAgent.send( + centerGridAgent, + RequestGridPowerMessage( + secondSweepNo, + Vector(slackNodeUuid) + ) + ) - expectMsgPF() { - case ProvideGridPowerMessage(nodeUuid, p, q) => - nodeUuid shouldBe slackNodeUuid - p should equalWithTolerance( - Quantities.getQuantity(0.080423711881452700000, MEGAVOLTAMPERE), - floatPrecision - ) - q should equalWithTolerance( - Quantities.getQuantity(-1.45357503915621860000, MEGAVOLTAMPERE), - floatPrecision - ) + superiorGridAgent.expectMsgPF() { + case ProvideGridPowerMessage(exchangedPower) => + exchangedPower.size shouldBe 1 + exchangedPower.headOption match { + case Some(ExchangePower(nodeUuid, p, q)) => + nodeUuid shouldBe slackNodeUuid + p should equalWithTolerance( + Quantities.getQuantity(0.080423711881452700000, MEGAVOLTAMPERE), + floatPrecision + ) + q should equalWithTolerance( + Quantities.getQuantity(-1.45357503915621860000, MEGAVOLTAMPERE), + floatPrecision + ) + case None => + fail("I did not expect to get nothing.") + } case x => fail( s"Invalid message received when expecting grid power message. Message was $x" @@ -522,7 +583,12 @@ class DBFSAlgorithmCenGridSpec val triggerId = 0 // this subnet has 1 superior grid (HöS) and 4 inferior grids (MS) - val subGridGateToActorRef = hvSubGridGates.map(gate => gate -> self).toMap + val subGridGateToActorRef = hvSubGridGates.map { + case gate + if gate.getInferiorNode.getSubnet == hvGridContainer.getSubnet => + gate -> superiorGridAgent.ref + case gate => gate -> inferiorGridAgent.ref + }.toMap val gridAgentInitData = GridAgentInitData( @@ -561,13 +627,16 @@ class DBFSAlgorithmCenGridSpec val activityStartTriggerId = 1 - centerGridAgent ! TriggerWithIdMessage( - ActivityStartTrigger(3600), - activityStartTriggerId, - centerGridAgent + scheduler.send( + centerGridAgent, + TriggerWithIdMessage( + ActivityStartTrigger(3600), + activityStartTriggerId, + centerGridAgent + ) ) - expectMsgPF() { + scheduler.expectMsgPF() { case CompletionMessage( triggerId, Some(Vector(ScheduleTriggerMessage(triggerToBeScheduled, _))) @@ -595,46 +664,41 @@ class DBFSAlgorithmCenGridSpec ) // send the start grid simulation trigger - centerGridAgent ! TriggerWithIdMessage( - StartGridSimulationTrigger(3600), - startGridSimulationTriggerId, - centerGridAgent - ) - - // we expect 4 requests for grid power values as we have 4 inferior grids - val firstGridPowerRequests = receiveWhile() { - case msg @ RequestGridPowerMessage(_, _) => msg -> lastSender - }.toMap - - firstGridPowerRequests.size shouldBe 4 - firstGridPowerRequests.keys should contain allOf ( - RequestGridPowerMessage( - firstSweepNo, - UUID.fromString("d44ba8ed-81db-4a22-a40d-f7c0d0808a75") - ), - RequestGridPowerMessage( - firstSweepNo, - UUID.fromString("e364ef00-e6ca-46b1-ba2b-bb73c0c6fee0") - ), - RequestGridPowerMessage( - firstSweepNo, - UUID.fromString("78c5d473-e01b-44c4-afd2-e4ff3c4a5d7c") - ), - RequestGridPowerMessage( - firstSweepNo, - UUID.fromString("47ef9983-8fcf-4713-be90-093fc27864ae") + scheduler.send( + centerGridAgent, + TriggerWithIdMessage( + StartGridSimulationTrigger(3600), + startGridSimulationTriggerId, + centerGridAgent ) ) + /* We expect one grid power request message, as all four sub grids are mapped onto one actor reference */ + val firstGridPowerRequests = inferiorGridAgent + .receiveWhile() { case msg: RequestGridPowerMessage => + msg -> inferiorGridAgent.lastSender + } + .toMap + + /* We receive one message with all requests for all inferior grid agents. This is, because the requests are + * grouped by the actor, they are sent to. Here, the test itself serves as the dummy for all inferior grid + * agents */ + firstGridPowerRequests.size shouldBe 1 + firstGridPowerRequests.keys.headOption match { + case Some(RequestGridPowerMessage(_, nodeUuids)) => + nodeUuids should contain theSameElementsAs inferiorGridNodeUuids + case None => fail("Did expect to receive something") + } + // we expect 1 request for slack voltage values // (slack values are requested by our agent under test from the superior grid) - val firstSlackVoltageRequest = expectMsgPF() { + val firstSlackVoltageRequest = superiorGridAgent.expectMsgPF() { case request @ RequestSlackVoltageMessage(sweepNo, nodeId) => sweepNo shouldBe firstSweepNo nodeId shouldBe UUID.fromString( "9fe5fa33-6d3b-4153-a829-a16f4347bc4e" ) - (request, lastSender) + (request, superiorGridAgent.lastSender) case x => fail( s"Invalid message received when expecting slack voltage request message. Message was $x" @@ -644,13 +708,16 @@ class DBFSAlgorithmCenGridSpec // 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 4 inferior grid agents inferiorGridNodeUuids.foreach { nodeUuid => - centerGridAgent ! RequestSlackVoltageMessage(firstSweepNo, nodeUuid) + inferiorGridAgent.send( + centerGridAgent, + RequestSlackVoltageMessage(firstSweepNo, nodeUuid) + ) } // 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) - val providedSlackVoltages = receiveWhile() { + val providedSlackVoltages = inferiorGridAgent.receiveWhile() { case provideSlackVoltageMessage: ProvideSlackVoltageMessage => provideSlackVoltageMessage case x => @@ -688,41 +755,59 @@ class DBFSAlgorithmCenGridSpec ) // we now answer the request of our centerGridAgent // with 4 fake grid power messages and 1 fake slack voltage message - firstGridPowerRequests.foreach { requestSender => - val askSender = requestSender._2 - val requestNodeUuid = requestSender._1.nodeUuid + firstGridPowerRequests.foreach { case (requestMessage, askSender) => + val requestNodeUuid = requestMessage.nodeUuids askSender ! ProvideGridPowerMessage( - requestNodeUuid, - Quantities.getQuantity(0, KILOWATT), - Quantities.getQuantity(0, KILOVAR) + requestNodeUuid.map { uuid => + ExchangePower( + uuid, + Quantities.getQuantity(0, KILOWATT), + Quantities.getQuantity(0, KILOVAR) + ) + } ) } - val slackAskSender = firstSlackVoltageRequest._2 - val slackRequestNodeUuid = firstSlackVoltageRequest._1.nodeUuid - val slackRequestSweepNo = firstSlackVoltageRequest._1.currentSweepNo - slackAskSender ! ProvideSlackVoltageMessage( - slackRequestSweepNo, - slackRequestNodeUuid, - Quantities.getQuantity(380, KILOVOLT), - Quantities.getQuantity(0, KILOVOLT) - ) + val slackRequestNodeUuid = firstSlackVoltageRequest match { + case (voltageRequest, slackAskSender) => + val slackRequestNodeUuid = voltageRequest.nodeUuid + val slackRequestSweepNo = voltageRequest.currentSweepNo + slackAskSender ! ProvideSlackVoltageMessage( + slackRequestSweepNo, + slackRequestNodeUuid, + Quantities.getQuantity(380, KILOVOLT), + Quantities.getQuantity(0, KILOVOLT) + ) + slackRequestNodeUuid + } // our test agent should now be ready to provide the grid power values, hence we ask for them and expect a // corresponding response - centerGridAgent ! RequestGridPowerMessage(firstSweepNo, slackNodeUuid) + superiorGridAgent.send( + centerGridAgent, + RequestGridPowerMessage( + firstSweepNo, + Vector(slackNodeUuid) + ) + ) - expectMsgPF() { - case ProvideGridPowerMessage(nodeUuid, p, q) => - nodeUuid shouldBe slackNodeUuid - p should equalWithTolerance( - Quantities.getQuantity(0.080423711881452700000, MEGAVOLTAMPERE), - floatPrecision - ) - q should equalWithTolerance( - Quantities.getQuantity(-1.45357503915666260000, MEGAVOLTAMPERE), - floatPrecision - ) + superiorGridAgent.expectMsgPF() { + case ProvideGridPowerMessage(exchangedPower) => + exchangedPower.size shouldBe 1 + exchangedPower.headOption match { + case Some(ExchangePower(nodeUuid, p, q)) => + nodeUuid shouldBe slackNodeUuid + p should equalWithTolerance( + Quantities.getQuantity(0.080423711881452700000, MEGAVOLTAMPERE), + floatPrecision + ) + q should equalWithTolerance( + Quantities.getQuantity(-1.45357503915666260000, MEGAVOLTAMPERE), + floatPrecision + ) + case None => + fail("Did not expect to get nothing.") + } case x => fail( s"Invalid message received when expecting grid power values message. Message was $x" @@ -731,16 +816,22 @@ class DBFSAlgorithmCenGridSpec // we start a second sweep by asking for next sweep values which should trigger the whole procedure again val secondSweepNo = firstSweepNo + 1 - centerGridAgent ! RequestGridPowerMessage(secondSweepNo, slackNodeUuid) + superiorGridAgent.send( + centerGridAgent, + RequestGridPowerMessage( + secondSweepNo, + Vector(slackNodeUuid) + ) + ) // the agent now should ask for updated slack voltages from the superior grid - val secondSlackVoltageRequest = expectMsgPF() { + val secondSlackVoltageRequest = superiorGridAgent.expectMsgPF() { case request @ RequestSlackVoltageMessage(sweepNo, nodeId) => sweepNo shouldBe secondSweepNo nodeId shouldBe UUID.fromString( "9fe5fa33-6d3b-4153-a829-a16f4347bc4e" ) - (request, lastSender) + (request, superiorGridAgent.lastSender) case x => fail( s"Invalid message received when expecting slack voltage request message. Message was $x" @@ -749,52 +840,50 @@ class DBFSAlgorithmCenGridSpec // the superior grid would answer with updated slack voltage values val secondSlackAskSender = secondSlackVoltageRequest._2 - secondSlackAskSender ! ProvideSlackVoltageMessage( - secondSweepNo, - slackRequestNodeUuid, - Quantities.getQuantity(380, KILOVOLT), - Quantities.getQuantity(0, KILOVOLT) - ) - - // after the intermediate power flow calculation - // we expect 4 requests for grid power values as we have 4 inferior grids - val secondGridPowerRequests = receiveWhile() { - case msg @ RequestGridPowerMessage(_, _) => msg -> lastSender - }.toMap - - secondGridPowerRequests.size shouldBe 4 - secondGridPowerRequests.keys should contain allOf ( - RequestGridPowerMessage( - secondSweepNo, - UUID.fromString("d44ba8ed-81db-4a22-a40d-f7c0d0808a75") - ), - RequestGridPowerMessage( - secondSweepNo, - UUID.fromString("e364ef00-e6ca-46b1-ba2b-bb73c0c6fee0") - ), - RequestGridPowerMessage( - secondSweepNo, - UUID.fromString("78c5d473-e01b-44c4-afd2-e4ff3c4a5d7c") - ), - RequestGridPowerMessage( + superiorGridAgent.send( + secondSlackAskSender, + ProvideSlackVoltageMessage( secondSweepNo, - UUID.fromString("47ef9983-8fcf-4713-be90-093fc27864ae") + slackRequestNodeUuid, + Quantities.getQuantity(380, KILOVOLT), + Quantities.getQuantity(0, KILOVOLT) ) ) + // after the intermediate power flow calculation + // We expect one grid power request message, as all four sub grids are mapped onto one actor reference + val secondGridPowerRequests = inferiorGridAgent + .receiveWhile() { case msg: RequestGridPowerMessage => + msg -> inferiorGridAgent.lastSender + } + .toMap + + /* We receive one message with all requests for all inferior grid agents. This is, because the requests are + * grouped by the actor, they are sent to. Here, the test itself serves as the dummy for all inferior grid + * agents */ + secondGridPowerRequests.size shouldBe 1 + secondGridPowerRequests.keys.headOption match { + case Some(RequestGridPowerMessage(_, nodeUuids)) => + nodeUuids should contain theSameElementsAs inferiorGridNodeUuids + case None => fail("Did expect to receive something") + } + // the agent should then go back to SimulateGrid and wait for the powers of the inferior grid // awaitAssert(centerGridAgent.stateName shouldBe SimulateGrid) // 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 4 inferior grid agents inferiorGridNodeUuids.foreach { nodeUuid => - centerGridAgent ! RequestSlackVoltageMessage(firstSweepNo, nodeUuid) + inferiorGridAgent.send( + centerGridAgent, + RequestSlackVoltageMessage(firstSweepNo, nodeUuid) + ) } // 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) - val secondProvidedSlackVoltages = receiveWhile() { + val secondProvidedSlackVoltages = inferiorGridAgent.receiveWhile() { case provideSlackVoltageMessage: ProvideSlackVoltageMessage => provideSlackVoltageMessage case x => @@ -862,29 +951,42 @@ class DBFSAlgorithmCenGridSpec }) // we now answer the request of our centerGridAgent - // with 4 fake grid power messages - secondGridPowerRequests.foreach { requestSender => - val askSender = requestSender._2 - val requestNodeUuid = requestSender._1.nodeUuid - askSender ! ProvideGridPowerMessage( - requestNodeUuid, - Quantities.getQuantity(0, KILOWATT), - Quantities.getQuantity(0, KILOVAR) + // with 1 fake grid power message + secondGridPowerRequests.foreach { case (request, askSender) => + val requestNodeUuid = request.nodeUuids + inferiorGridAgent.send( + askSender, + ProvideGridPowerMessage( + requestNodeUuid.map { uuid => + ExchangePower( + uuid, + Quantities.getQuantity(0, KILOWATT), + Quantities.getQuantity(0, KILOVAR) + ) + } + ) ) } // we expect that the GridAgent unstashes the messages and return a value for our power request - expectMsgPF() { - case ProvideGridPowerMessage(nodeUuid, p, q) => - nodeUuid shouldBe slackNodeUuid - p should equalWithTolerance( - Quantities.getQuantity(0.080423711881702500000, MEGAVOLTAMPERE), - floatPrecision - ) - q should equalWithTolerance( - Quantities.getQuantity(-1.45357503915621860000, MEGAVOLTAMPERE), - floatPrecision - ) + superiorGridAgent.expectMsgPF() { + case ProvideGridPowerMessage(exchangedPower) => + exchangedPower.size shouldBe 1 + exchangedPower.headOption match { + case Some(ExchangePower(nodeUuid, p, q)) => + nodeUuid shouldBe slackNodeUuid + p should equalWithTolerance( + Quantities.getQuantity(0.080423711881702500000, MEGAVOLTAMPERE), + floatPrecision + ) + q should equalWithTolerance( + Quantities.getQuantity(-1.45357503915621860000, MEGAVOLTAMPERE), + floatPrecision + ) + case None => + fail("I did not expect to get nothing.") + } + case x => fail( s"Invalid message received when expecting grid power message. Message was $x" 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 2d60f754b6..38cf61fc9d 100644 --- a/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmSupGridSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmSupGridSpec.scala @@ -7,7 +7,6 @@ package edu.ie3.simona.agent.grid import java.util.UUID - import akka.actor.{ActorRef, ActorSystem} import akka.testkit.{ImplicitSender, TestFSMRef} import com.typesafe.config.ConfigFactory @@ -17,6 +16,7 @@ import edu.ie3.simona.agent.grid.GridAgentData.GridAgentInitData import edu.ie3.simona.agent.state.AgentState.{Idle, Uninitialized} import edu.ie3.simona.agent.state.GridAgentState.SimulateGrid import edu.ie3.simona.model.grid.RefSystem +import edu.ie3.simona.ontology.messages.PowerMessage.ProvideGridPowerMessage.ExchangePower import edu.ie3.simona.ontology.messages.PowerMessage.{ ProvideGridPowerMessage, RequestGridPowerMessage @@ -159,8 +159,8 @@ class DBFSAlgorithmSupGridSpec for (sweepNo <- 0 to 1) { val startGridSimulationTriggerId = sweepNo + 2 - val requestedConnectionNodeUuid = - UUID.fromString("9fe5fa33-6d3b-4153-a829-a16f4347bc4e") + val requestedConnectionNodeUuids = + Vector(UUID.fromString("9fe5fa33-6d3b-4153-a829-a16f4347bc4e")) // send the start grid simulation trigger superiorGridAgentFSM ! TriggerWithIdMessage( @@ -173,7 +173,7 @@ class DBFSAlgorithmSupGridSpec expectMsgPF() { case requestGridPowerMessage: RequestGridPowerMessage => requestGridPowerMessage.currentSweepNo shouldBe sweepNo - requestGridPowerMessage.nodeUuid shouldBe requestedConnectionNodeUuid + requestGridPowerMessage.nodeUuids should contain allElementsOf requestedConnectionNodeUuids case x => fail( s"Invalid message received when expecting a request for grid power values! Message was $x" @@ -186,9 +186,13 @@ class DBFSAlgorithmSupGridSpec // / ask sender val askSender = lastSender askSender ! ProvideGridPowerMessage( - requestedConnectionNodeUuid, - Quantities.getQuantity(0, KILOWATT), - Quantities.getQuantity(0, KILOVAR) + requestedConnectionNodeUuids.map { uuid => + ExchangePower( + uuid, + Quantities.getQuantity(0, KILOWATT), + Quantities.getQuantity(0, KILOVAR) + ) + } ) // we expect a completion message here (sweepNo == 0) and that the agent goes back to simulate grid @@ -309,8 +313,8 @@ class DBFSAlgorithmSupGridSpec for (sweepNo <- 0 to maxNumberOfTestSweeps) { val startGridSimulationTriggerId = sweepNo + 4 - val requestedConnectionNodeUuid = - UUID.fromString("9fe5fa33-6d3b-4153-a829-a16f4347bc4e") + val requestedConnectionNodeUuids = + Vector(UUID.fromString("9fe5fa33-6d3b-4153-a829-a16f4347bc4e")) // send the start grid simulation trigger superiorGridAgentFSM ! TriggerWithIdMessage( @@ -323,7 +327,7 @@ class DBFSAlgorithmSupGridSpec expectMsgPF() { case requestGridPowerMessage: RequestGridPowerMessage => requestGridPowerMessage.currentSweepNo shouldBe sweepNo - requestGridPowerMessage.nodeUuid shouldBe requestedConnectionNodeUuid + requestGridPowerMessage.nodeUuids should contain allElementsOf requestedConnectionNodeUuids case x => fail( s"Invalid message received when expecting a request for grid power values! Message was $x" @@ -335,9 +339,13 @@ class DBFSAlgorithmSupGridSpec // / ask sender val askSender = lastSender askSender ! ProvideGridPowerMessage( - requestedConnectionNodeUuid, - deviations(sweepNo)._1, - deviations(sweepNo)._2 + requestedConnectionNodeUuids.map { uuid => + ExchangePower( + uuid, + deviations(sweepNo)._1, + deviations(sweepNo)._2 + ) + } ) // we expect a completion message here and that the agent goes back to simulate grid @@ -461,8 +469,8 @@ class DBFSAlgorithmSupGridSpec for (sweepNo <- 0 to 1) { val startGridSimulationTriggerId = sweepNo + 2 - val requestedConnectionNodeUuid = - UUID.fromString("9fe5fa33-6d3b-4153-a829-a16f4347bc4e") + val requestedConnectionNodeUuids = + Vector(UUID.fromString("9fe5fa33-6d3b-4153-a829-a16f4347bc4e")) // send the start grid simulation trigger superiorGridAgentFSM ! TriggerWithIdMessage( @@ -475,7 +483,7 @@ class DBFSAlgorithmSupGridSpec expectMsgPF() { case requestGridPowerMessage: RequestGridPowerMessage => requestGridPowerMessage.currentSweepNo shouldBe sweepNo - requestGridPowerMessage.nodeUuid shouldBe requestedConnectionNodeUuid + requestGridPowerMessage.nodeUuids should contain allElementsOf requestedConnectionNodeUuids case x => fail( s"Invalid message received when expecting a request for grid power values! Message was $x" @@ -488,9 +496,13 @@ class DBFSAlgorithmSupGridSpec // / ask sender val askSender = lastSender askSender ! ProvideGridPowerMessage( - requestedConnectionNodeUuid, - Quantities.getQuantity(0, KILOWATT), - Quantities.getQuantity(0, KILOVAR) + requestedConnectionNodeUuids.map { uuid => + ExchangePower( + uuid, + Quantities.getQuantity(0, KILOWATT), + Quantities.getQuantity(0, KILOVAR) + ) + } ) // we expect a completion message here and that the agent goes back to simulate grid @@ -592,8 +604,8 @@ class DBFSAlgorithmSupGridSpec for (sweepNo <- 0 to maxNumberOfTestSweeps) { val startGridSimulationTriggerId = sweepNo + 4 - val requestedConnectionNodeUuid = - UUID.fromString("9fe5fa33-6d3b-4153-a829-a16f4347bc4e") + val requestedConnectionNodeUuids = + Vector(UUID.fromString("9fe5fa33-6d3b-4153-a829-a16f4347bc4e")) // send the start grid simulation trigger superiorGridAgentFSM ! TriggerWithIdMessage( @@ -606,7 +618,7 @@ class DBFSAlgorithmSupGridSpec expectMsgPF() { case requestGridPowerMessage: RequestGridPowerMessage => requestGridPowerMessage.currentSweepNo shouldBe sweepNo - requestGridPowerMessage.nodeUuid shouldBe requestedConnectionNodeUuid + requestGridPowerMessage.nodeUuids should contain allElementsOf requestedConnectionNodeUuids case x => fail( s"Invalid message received when expecting a request for grid power values! Message was $x" @@ -618,9 +630,13 @@ class DBFSAlgorithmSupGridSpec // / ask sender val askSender = lastSender askSender ! ProvideGridPowerMessage( - requestedConnectionNodeUuid, - deviations(sweepNo)._1, - deviations(sweepNo)._2 + requestedConnectionNodeUuids.map { uuid => + ExchangePower( + uuid, + deviations(sweepNo)._1, + deviations(sweepNo)._2 + ) + } ) // we expect a completion message here and that the agent goes back to simulate grid From ba6281214d8f2a7b7c1060cd6783cd03f822fc1f Mon Sep 17 00:00:00 2001 From: "Kittl, Chris" Date: Mon, 13 Dec 2021 15:00:58 +0100 Subject: [PATCH 02/36] Add a first changelog --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000000..d30b43d8b5 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,13 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] +### Fixed +- Fix power exchange between grids + - Consolidate request replies for different sub grid gates in one message + - Await and send responses for distinct pairs of sender reference and target node + +[Unreleased]: https://github.com/ie3-institute/simona From 31f9e864565c128e9e8a8e145cfafc481a4d3b40 Mon Sep 17 00:00:00 2001 From: "Kittl, Chris" Date: Wed, 12 Jan 2022 12:17:20 +0100 Subject: [PATCH 03/36] Address reviewer comments --- .../ie3/simona/agent/grid/DBFSAlgorithm.scala | 23 ++----------------- .../ie3/simona/agent/grid/GridAgentData.scala | 4 +--- 2 files changed, 3 insertions(+), 24 deletions(-) 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 a93e79b190..c8419c8bba 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/DBFSAlgorithm.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/DBFSAlgorithm.scala @@ -263,25 +263,16 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { val firstRequestedNodeUuid = requestedNodeUuids.headOption match { case Some(uuid) => uuid case None => - throw new RuntimeException( + throw new DBFSAlgorithmException( "Did receive a grid power request but without specified nodes" ) } - val queryingSubnet = gridAgentBaseData.gridEnv.subnetGateToActorRef - .find(_._1.getSuperiorNode.getUuid == firstRequestedNodeUuid) - .map(_._1.getSuperiorNode.getSubnet) - .getOrElse(-1000) if (gridAgentBaseData.currentSweepNo == requestSweepNo) { log.debug( s"Received request for grid power values for sweepNo {} before my first power flow calc. Stashing away.", requestSweepNo ) - if (gridAgentBaseData.gridEnv.gridModel.subnetNo == 3000) { - log.info( - s"GridAgent 3000 received a grid power request from subnet $queryingSubnet, before power flow has been calculated. Stash it away." - ) - } stash() stay() } else { @@ -290,11 +281,6 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { requestSweepNo, gridAgentBaseData.currentSweepNo ) - if (gridAgentBaseData.gridEnv.gridModel.subnetNo == 3000) { - log.info( - s"GridAgent 3000 received a grid power request from subnet $queryingSubnet, before power flow has been calculated. Initiate new sweep." - ) - } self ! PrepareNextSweepTrigger(currentTick) stash() @@ -314,7 +300,7 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { val firstRequestedNodeUuid = requestedNodeUuids.headOption match { case Some(uuid) => uuid case None => - throw new RuntimeException( + throw new DBFSAlgorithmException( "Did receive a grid power request but without specified nodes" ) } @@ -322,11 +308,6 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { .find(_._1.getSuperiorNode.getUuid == firstRequestedNodeUuid) .map(_._1.getSuperiorNode.getSubnet) match { case Some(requestingSubnetNumber) => - if (gridAgentBaseData.gridEnv.gridModel.subnetNo == 3000) { - log.info( - s"GridAgent 3000 received a grid power request from subnet $requestingSubnetNumber, after power flow has been calculated." - ) - } log.debug( "Received request for grid power values and im READY to provide." ) 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 34db7f8ffc..12f6386a6e 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/GridAgentData.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/GridAgentData.scala @@ -82,10 +82,8 @@ object GridAgentData { ): PowerFlowDoneData = { /* Determine the subnet numbers of all superior grids */ val superiorSubGrids = gridAgentBaseData.gridEnv.subnetGateToActorRef - .filter( - _._1.getSuperiorNode.getSubnet != gridAgentBaseData.gridEnv.gridModel.subnetNo - ) .map(_._1.getSuperiorNode.getSubnet) + .filterNot(_ == gridAgentBaseData.gridEnv.gridModel.subnetNo) .toSet PowerFlowDoneData(gridAgentBaseData, powerFlowResult, superiorSubGrids) } From 6ab7d4b7c0c843c9a387ca8f4a32af813f148258 Mon Sep 17 00:00:00 2001 From: "Kittl, Chris" Date: Tue, 8 Feb 2022 08:40:30 +0100 Subject: [PATCH 04/36] Send FinishGridSimulationTrigger only once to inferior GridAgents --- .../edu/ie3/simona/agent/grid/DBFSAlgorithm.scala | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 c8419c8bba..c452f8c0e7 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/DBFSAlgorithm.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/DBFSAlgorithm.scala @@ -422,12 +422,12 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { gridAgentBaseData: GridAgentBaseData ) => // inform my child grids about the end of this grid simulation - gridAgentBaseData.inferiorGridGates.foreach(inferiorGridGate => { - gridAgentBaseData.gridEnv - .subnetGateToActorRef(inferiorGridGate) ! FinishGridSimulationTrigger( - currentTick - ) - }) + gridAgentBaseData.inferiorGridGates + .map { inferiorGridGate => + gridAgentBaseData.gridEnv.subnetGateToActorRef(inferiorGridGate) + } + .distinct + .foreach(_ ! FinishGridSimulationTrigger(currentTick)) // inform every system participant about the end of this grid simulation gridAgentBaseData.gridEnv.nodeToAssetAgents.foreach { case (_, actors) => From 89cc9742c27b269946c5b6f08db8bcee85326530 Mon Sep 17 00:00:00 2001 From: "Kittl, Chris" Date: Tue, 8 Feb 2022 11:25:21 +0100 Subject: [PATCH 05/36] Fix gradel download plugin call --- gradle/scripts/tscfg.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/scripts/tscfg.gradle b/gradle/scripts/tscfg.gradle index 7f430ff1f7..22e1f69941 100644 --- a/gradle/scripts/tscfg.gradle +++ b/gradle/scripts/tscfg.gradle @@ -5,7 +5,7 @@ task genConfigClass { doLast { def tscfgJarFile = project.file('build/tscfg-' + tscfgVersion + '.jar') if (!tscfgJarFile.exists() || !tscfgJarFile.isFile()) { - download { + download.run { src 'https://github.com/carueda/tscfg/releases/download/v' + tscfgVersion + '/tscfg-' + tscfgVersion + '.jar' dest buildDir } @@ -15,7 +15,7 @@ task genConfigClass { args = [ "build/tscfg-${tscfgVersion}.jar", "--spec", - "src/main/resources/config/simona-config-template.conf", + "src/main/resources/config/config-template.conf", "--scala", "--durations", "--pn", From 73dcc5a2831e267fc9b754b6b73764a407570287 Mon Sep 17 00:00:00 2001 From: "Kittl, Chris" Date: Tue, 8 Feb 2022 13:10:45 +0100 Subject: [PATCH 06/36] Fix config auto-generation and relevant places of usage --- .../ie3/simona/config/ConfigConventions.scala | 5 -- .../ie3/simona/config/ConfigFailFast.scala | 18 ++++- .../ie3/simona/config/RefSystemParser.scala | 10 +-- .../edu/ie3/simona/config/SimonaConfig.scala | 26 ++++++- .../edu/ie3/simona/config/VoltLvlParser.scala | 29 ++----- .../simona/config/ConfigFailFastSpec.scala | 76 +++++++------------ .../simona/config/RefSystemParserSpec.scala | 42 +++++++--- 7 files changed, 106 insertions(+), 100 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/config/ConfigConventions.scala b/src/main/scala/edu/ie3/simona/config/ConfigConventions.scala index 0787563623..f8f99dfc6c 100644 --- a/src/main/scala/edu/ie3/simona/config/ConfigConventions.scala +++ b/src/main/scala/edu/ie3/simona/config/ConfigConventions.scala @@ -13,11 +13,6 @@ import scala.util.matching.Regex * are used in several places) */ object ConfigConventions { - /* - * Regex matches e.g. {MV, 10 kV} and captures "MV" and "10 kV" - */ - val voltLvlRegex: Regex = """\{([\wäöüÄÖÜß-]+),[ ]+(.+)}""".r.unanchored - // regex matches // / 1,2,3,12,5-10,6-10,10-5,10...100,100...1000,10-5 // val gridIdsRegex: Regex = """(\d+-\d+|\d+\.\.\.\d+|^\d{1}$)""".r.unanchored diff --git a/src/main/scala/edu/ie3/simona/config/ConfigFailFast.scala b/src/main/scala/edu/ie3/simona/config/ConfigFailFast.scala index ddc7c37704..98072f2edf 100644 --- a/src/main/scala/edu/ie3/simona/config/ConfigFailFast.scala +++ b/src/main/scala/edu/ie3/simona/config/ConfigFailFast.scala @@ -20,10 +20,13 @@ import edu.ie3.simona.util.ConfigUtil.DatabaseConfigUtil.checkInfluxDb1xParams import edu.ie3.simona.util.ConfigUtil.{CsvConfigUtil, NotifierIdentifier} import edu.ie3.util.scala.ReflectionTools import edu.ie3.util.{StringUtils, TimeUtil} +import tech.units.indriya.quantity.Quantities +import tech.units.indriya.unit.Units import java.security.InvalidParameterException import java.time.temporal.ChronoUnit import java.util.UUID +import javax.measure.quantity.ElectricPotential import scala.util.{Failure, Success, Try} /** Sanity checks for [[SimonaConfig]] that should lead to a fast failure during @@ -385,7 +388,8 @@ case object ConfigFailFast extends LazyLogging { */ private def checkRefSystem(refSystem: RefSystemConfig): Unit = { - val voltLvls = refSystem.voltLvls.getOrElse(List.empty[String]) + val voltLvls = + refSystem.voltLvls.getOrElse(List.empty[SimonaConfig.VoltLvlConfig]) val gridIds = refSystem.gridIds.getOrElse(List.empty[String]) if (voltLvls.isEmpty && gridIds.isEmpty) @@ -396,10 +400,16 @@ case object ConfigFailFast extends LazyLogging { ) voltLvls.foreach { voltLvl => - { - if (!ConfigConventions.voltLvlRegex.matches(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!" + ) + case Failure(exception) => throw new InvalidConfigParameterException( - s"The definition string for voltLvl '$voltLvl' does not comply with the definition {, }!" + s"The given nominal voltage '${voltLvl.vNom}' cannot be parsed to Quantity.", + exception ) } } diff --git a/src/main/scala/edu/ie3/simona/config/RefSystemParser.scala b/src/main/scala/edu/ie3/simona/config/RefSystemParser.scala index 9bf9f5e1b2..640771e93b 100644 --- a/src/main/scala/edu/ie3/simona/config/RefSystemParser.scala +++ b/src/main/scala/edu/ie3/simona/config/RefSystemParser.scala @@ -107,15 +107,7 @@ object RefSystemParser { case Some(voltLvls) => voltLvls.foldLeft(Vector.empty[(VoltageLevel, RefSystem)])( (voltLvlRefSystems, voltLvlDef) => { - val voltLvl = voltLvlDef match { - case ConfigConventions.voltLvlRegex(id, vNom) => - VoltLvlParser.parse(id, vNom) - case invalid => - throw new InvalidConfigParameterException( - s"Got invalid voltage level string $invalid. Has to look like this: {MV, 10 kV}" - ) - } - voltLvlRefSystems :+ (voltLvl, refSystem) + voltLvlRefSystems :+ (VoltLvlParser.from(voltLvlDef), refSystem) } ) case None => List.empty[(VoltageLevel, RefSystem)] diff --git a/src/main/scala/edu/ie3/simona/config/SimonaConfig.scala b/src/main/scala/edu/ie3/simona/config/SimonaConfig.scala index 6283055bbc..29110a0920 100644 --- a/src/main/scala/edu/ie3/simona/config/SimonaConfig.scala +++ b/src/main/scala/edu/ie3/simona/config/SimonaConfig.scala @@ -1,5 +1,5 @@ /* - * © 2021. TU Dortmund University, + * © 2022. TU Dortmund University, * Institute of Energy Systems, Energy Efficiency and Energy Economics, * Research group Distribution grid planning and operation */ @@ -377,7 +377,7 @@ object SimonaConfig { gridIds: scala.Option[scala.List[java.lang.String]], sNom: java.lang.String, vNom: java.lang.String, - voltLvls: scala.Option[scala.List[java.lang.String]] + voltLvls: scala.Option[scala.List[SimonaConfig.VoltLvlConfig]] ) object RefSystemConfig { def apply( @@ -397,11 +397,31 @@ object SimonaConfig { voltLvls = if (c.hasPathOrNull("voltLvls")) scala.Some( - $_L$_str(c.getList("voltLvls"), parentPath, $tsCfgValidator) + $_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 $_reqStr( parentPath: java.lang.String, c: com.typesafe.config.Config, diff --git a/src/main/scala/edu/ie3/simona/config/VoltLvlParser.scala b/src/main/scala/edu/ie3/simona/config/VoltLvlParser.scala index 1077941194..3d3ce38920 100644 --- a/src/main/scala/edu/ie3/simona/config/VoltLvlParser.scala +++ b/src/main/scala/edu/ie3/simona/config/VoltLvlParser.scala @@ -23,31 +23,16 @@ import tech.units.indriya.quantity.Quantities */ object VoltLvlParser { - /** Parses the voltage level from given config element + /** Create a voltage level definition from config entry * - * @param configElement - * Config element to parse + * @param config + * Config entry to parse * @return - * Common voltage level + * A suitable [[VoltageLevel]] */ - def parse(configElement: VoltLvlConfig): VoltageLevel = { - val id = configElement.id - val vNominal = parseNominalVoltage(configElement.vNom) - parse(id, vNominal) - } - - /** Parses a common voltage level from Strings denoting id and nominal voltage - * - * @param id - * Identifier - * @param vNom - * Nominal voltage - * @return - * Common voltage level - */ - def parse(id: String, vNom: String): VoltageLevel = { - val vNominal = parseNominalVoltage(vNom) - parse(id, vNominal) + def from(config: SimonaConfig.VoltLvlConfig): VoltageLevel = { + val vNom = parseNominalVoltage(config.vNom) + parse(config.id, vNom) } /** Looks up a common voltage level with the given parameters diff --git a/src/test/scala/edu/ie3/simona/config/ConfigFailFastSpec.scala b/src/test/scala/edu/ie3/simona/config/ConfigFailFastSpec.scala index decc6d59cd..0854afcf38 100644 --- a/src/test/scala/edu/ie3/simona/config/ConfigFailFastSpec.scala +++ b/src/test/scala/edu/ie3/simona/config/ConfigFailFastSpec.scala @@ -94,6 +94,7 @@ class ConfigFailFastSpec extends UnitSpec with ConfigTestData { } "A configuration with faulty refSystem parameters" should { + val checkRefSystem = PrivateMethod[Unit](Symbol("checkRefSystem")) "throw an InvalidConfigParametersException when gridIds and voltLvls are empty" in { @@ -112,31 +113,6 @@ class ConfigFailFastSpec extends UnitSpec with ConfigTestData { } - "throw an InvalidConfigParametersException when the voltLevel is malformed (e.g. a number)" in { - val refSystemConfigAllEmpty = - ConfigFactory.parseString("""simona.gridConfig.refSystems = [ - | { - | sNom="100 MVA", - | vNom="0.4 kV", - | voltLvls = ["1"] - | } - |]""".stripMargin) - val faultyConfig = - refSystemConfigAllEmpty.withFallback(typesafeConfig).resolve() - val faultySimonaConfig = SimonaConfig(faultyConfig) - - // get the private method for validation - val checkRefSystem = - PrivateMethod[Unit](Symbol("checkRefSystem")) - - intercept[InvalidConfigParameterException] { - faultySimonaConfig.simona.gridConfig.refSystems.foreach(refSystem => - ConfigFailFast invokePrivate checkRefSystem(refSystem) - ) - }.getMessage shouldBe "The definition string for voltLvl '1' does not comply with the definition {, }!" - - } - "throw an InvalidConfigParametersException when the gridId is malformed" in { val malformedGridIds = List("10--100", "MS", "10..100") @@ -155,10 +131,6 @@ class ConfigFailFastSpec extends UnitSpec with ConfigTestData { refSystemConfigAllEmpty.withFallback(typesafeConfig).resolve() val faultySimonaConfig = SimonaConfig(faultyConfig) - // get the private method for validation - val checkRefSystem = - PrivateMethod[Unit](Symbol("checkRefSystem")) - intercept[InvalidConfigParameterException] { faultySimonaConfig.simona.gridConfig.refSystems.foreach( refSystem => @@ -169,6 +141,28 @@ class ConfigFailFastSpec extends UnitSpec with ConfigTestData { }) } + "throw an InvalidConfigParameterException if the nominal voltage of the voltage level is malformed" in { + + val refSystemConfigAllEmpty = + ConfigFactory.parseString("""simona.gridConfig.refSystems = [ + | { + | sNom="100 MVA", + | vNom="0.4 kV", + | voltLvls = [{id = "1", vNom = "foo"}] + | } + |]""".stripMargin) + val faultyConfig = + refSystemConfigAllEmpty.withFallback(typesafeConfig).resolve() + val faultySimonaConfig = SimonaConfig(faultyConfig) + + intercept[InvalidConfigParameterException] { + faultySimonaConfig.simona.gridConfig.refSystems.foreach(refSystem => + ConfigFailFast invokePrivate checkRefSystem(refSystem) + ) + }.getMessage shouldBe "The given nominal voltage 'foo' cannot be parsed to Quantity." + + } + "throw an InvalidConfigParametersException when sNom is invalid" in { val refSystemConfigAllEmpty = ConfigFactory.parseString( @@ -176,7 +170,7 @@ class ConfigFailFastSpec extends UnitSpec with ConfigTestData { | { | sNom="100", | vNom="0.4 kV", - | voltLvls = ["{MS, 10 kV}","{HS, 110 kV}"] + | voltLvls = [{id = "MS", vNom = "10 kV"},{id = "HS", vNom = "110 kV"}] | } |]""".stripMargin ) @@ -184,15 +178,11 @@ class ConfigFailFastSpec extends UnitSpec with ConfigTestData { refSystemConfigAllEmpty.withFallback(typesafeConfig).resolve() val faultySimonaConfig = SimonaConfig(faultyConfig) - // get the private method for validation - val checkRefSystem = - PrivateMethod[Unit](Symbol("checkRefSystem")) - intercept[InvalidConfigParameterException] { faultySimonaConfig.simona.gridConfig.refSystems.foreach(refSystem => ConfigFailFast invokePrivate checkRefSystem(refSystem) ) - }.getMessage shouldBe "Invalid value for sNom from provided refSystem RefSystemConfig(None,100,0.4 kV,Some(List({MS, 10 kV}, {HS, 110 kV}))). Is a valid unit provided?" + }.getMessage shouldBe "Invalid value for sNom from provided refSystem RefSystemConfig(None,100,0.4 kV,Some(List(VoltLvlConfig(MS,10 kV), VoltLvlConfig(HS,110 kV)))). Is a valid unit provided?" } @@ -204,7 +194,7 @@ class ConfigFailFastSpec extends UnitSpec with ConfigTestData { | { | sNom="100 MVA", | vNom="0.4", - | voltLvls = ["{MS, 10 kV}","{HS, 110 kV}"] + | voltLvls = [{id = "MS", vNom = "10 kV"},{id = "HS", vNom = "110 kV"}] | } |]""".stripMargin ) @@ -212,15 +202,11 @@ class ConfigFailFastSpec extends UnitSpec with ConfigTestData { refSystemConfigAllEmpty.withFallback(typesafeConfig).resolve() val faultySimonaConfig = SimonaConfig(faultyConfig) - // get the private method for validation - val checkRefSystem = - PrivateMethod[Unit](Symbol("checkRefSystem")) - intercept[InvalidConfigParameterException] { faultySimonaConfig.simona.gridConfig.refSystems.foreach(refSystem => ConfigFailFast invokePrivate checkRefSystem(refSystem) ) - }.getMessage shouldBe "Invalid value for vNom from provided refSystem RefSystemConfig(None,100 MVA,0.4,Some(List({MS, 10 kV}, {HS, 110 kV}))). Is a valid unit provided?" + }.getMessage shouldBe "Invalid value for vNom from provided refSystem RefSystemConfig(None,100 MVA,0.4,Some(List(VoltLvlConfig(MS,10 kV), VoltLvlConfig(HS,110 kV)))). Is a valid unit provided?" } @@ -231,13 +217,13 @@ class ConfigFailFastSpec extends UnitSpec with ConfigTestData { | { | sNom="100 MVA", | vNom="0.4 kV", - | voltLvls = ["{MS, 10 kV}","{HS, 110 kV}"] + | voltLvls = [{id = "MS", vNom = "10 kV"},{id = "HS", vNom = "110 kV"}] | gridIds = ["1","1-10","10...100"] | }, | { | sNom="1000 MVA", | vNom="10kV", - | voltLvls = ["{HS, 110 kV}","{HoeS, 380 kV}"] + | voltLvls = [{id = "HS", vNom = "110 kV"},{id = "HoeS", vNom = "380 kV"}] | gridIds = ["1-3","3...6","10...100"] | } |]""".stripMargin @@ -246,10 +232,6 @@ class ConfigFailFastSpec extends UnitSpec with ConfigTestData { refSystemConfigAllEmpty.withFallback(typesafeConfig).resolve() val simonaConfig = SimonaConfig(config) - // get the private method for validation - val checkRefSystem = - PrivateMethod[Unit](Symbol("checkRefSystem")) - simonaConfig.simona.gridConfig.refSystems.foreach(refSystem => { ConfigFailFast invokePrivate checkRefSystem(refSystem) }) diff --git a/src/test/scala/edu/ie3/simona/config/RefSystemParserSpec.scala b/src/test/scala/edu/ie3/simona/config/RefSystemParserSpec.scala index a757cc2e1c..c27969ce63 100644 --- a/src/test/scala/edu/ie3/simona/config/RefSystemParserSpec.scala +++ b/src/test/scala/edu/ie3/simona/config/RefSystemParserSpec.scala @@ -10,10 +10,11 @@ import edu.ie3.datamodel.models.voltagelevels.{ CommonVoltageLevel, GermanVoltageLevelUtils } -import edu.ie3.simona.config.SimonaConfig.RefSystemConfig +import edu.ie3.simona.config.SimonaConfig.{RefSystemConfig, VoltLvlConfig} import edu.ie3.simona.exceptions.InvalidConfigParameterException import edu.ie3.simona.model.grid.RefSystem import edu.ie3.simona.test.common.UnitSpec +import tech.units.indriya.quantity.Quantities class RefSystemParserSpec extends UnitSpec { @@ -25,13 +26,20 @@ class RefSystemParserSpec extends UnitSpec { gridIds = Some(List("1", "2-10", "15...20")), sNom = "100 MVA", vNom = "10 kV", - voltLvls = Some(List("{MS, 10 kV}", "{MS, 20 kV}")) + voltLvls = Some( + List(VoltLvlConfig("MS", "10 kV"), VoltLvlConfig("MS", "20 kV")) + ) ), new RefSystemConfig( gridIds = Some(List("100")), sNom = "5000 MVA", vNom = "110 kV", - voltLvls = Some(List("{HS, 110 kV}", "{HoeS, 380 kV}")) + voltLvls = Some( + List( + VoltLvlConfig("HS", "110 kV"), + VoltLvlConfig("HoeS", "380 kV") + ) + ) ), new RefSystemConfig( gridIds = None, @@ -90,7 +98,9 @@ class RefSystemParserSpec extends UnitSpec { gridIds = Some(List("1", "2", "2-10", "15...20")), sNom = "100 MVA", vNom = "10 kV", - voltLvls = Some(List("{MS, 10 kV}", "{MS, 20 kV}")) + voltLvls = Some( + List(VoltLvlConfig("MS", "10 kV"), VoltLvlConfig("MS", "20 kV")) + ) ) ) intercept[InvalidConfigParameterException] { @@ -107,13 +117,17 @@ class RefSystemParserSpec extends UnitSpec { gridIds = None, sNom = "100 MVA", vNom = "10 kV", - voltLvls = Some(List("{MS, 10 kV}", "{MS, 20 kV}")) + voltLvls = Some( + List(VoltLvlConfig("MS", "10 kV"), VoltLvlConfig("MS", "20 kV")) + ) ), new RefSystemConfig( gridIds = None, sNom = "100 MVA", vNom = "10 kV", - voltLvls = Some(List("{MS, 10 kV}", "{MS, 20 kV}")) + voltLvls = Some( + List(VoltLvlConfig("MS", "10 kV"), VoltLvlConfig("MS", "20 kV")) + ) ) ) intercept[InvalidConfigParameterException] { @@ -130,13 +144,17 @@ class RefSystemParserSpec extends UnitSpec { gridIds = Some(List("asd")), sNom = "100 MVA", vNom = "10 kV", - voltLvls = Some(List("{MS, 10 kV}", "{MS, 20 kV}")) + voltLvls = Some( + List(VoltLvlConfig("MS", "10 kV"), VoltLvlConfig("MS", "20 kV")) + ) ), new RefSystemConfig( gridIds = None, sNom = "100 MVA", vNom = "10 kV", - voltLvls = Some(List("{MS, 10 kV}", "{MS, 20 kV}")) + voltLvls = Some( + List(VoltLvlConfig("MS", "10 kV"), VoltLvlConfig("MS", "20 kV")) + ) ) ) intercept[InvalidConfigParameterException] { @@ -155,13 +173,17 @@ class RefSystemParserSpec extends UnitSpec { gridIds = Some(List("1", "2-10", "15...20")), sNom = "100 MVA", vNom = "10 kV", - voltLvls = Some(List("{MS, 10 kV}", "{MS, 20 kV}")) + voltLvls = Some( + List(VoltLvlConfig("MS", "10 kV"), VoltLvlConfig("MS", "20 kV")) + ) ), new RefSystemConfig( gridIds = Some(List("100")), sNom = "5000 MVA", vNom = "110 kV", - voltLvls = Some(List("{HS, 110 kV}", "{HoeS, 380 kV}")) + voltLvls = Some( + List(VoltLvlConfig("HS", "110 kV"), VoltLvlConfig("HoeS", "380 kV")) + ) ) ) From a726da2c6c26b58221866ab7585529c53b8cee59 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Tue, 21 Jun 2022 17:22:27 +0200 Subject: [PATCH 07/36] Naming a lot of unnamed variables, removing some dead code --- .../ie3/simona/agent/grid/DBFSAlgorithm.scala | 129 +++++++++--------- .../ie3/simona/agent/grid/GridAgentData.scala | 14 +- .../simona/agent/grid/PowerFlowSupport.scala | 25 ++-- 3 files changed, 82 insertions(+), 86 deletions(-) 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 ef27961149..2f89439d2d 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/DBFSAlgorithm.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/DBFSAlgorithm.scala @@ -260,17 +260,9 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { // / before power flow calc for this sweep we either have to stash() the message to answer it later (in current sweep) // / or trigger a new run for the next sweepNo case Event( - RequestGridPowerMessage(requestSweepNo, requestedNodeUuids), + RequestGridPowerMessage(requestSweepNo, _), gridAgentBaseData: GridAgentBaseData ) => - val firstRequestedNodeUuid = requestedNodeUuids.headOption match { - case Some(uuid) => uuid - case None => - throw new DBFSAlgorithmException( - "Did receive a grid power request but without specified nodes" - ) - } - if (gridAgentBaseData.currentSweepNo == requestSweepNo) { log.debug( s"Received request for grid power values for sweepNo {} before my first power flow calc. Stashing away.", @@ -308,8 +300,9 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { ) } gridAgentBaseData.gridEnv.subnetGateToActorRef - .find(_._1.getSuperiorNode.getUuid == firstRequestedNodeUuid) - .map(_._1.getSuperiorNode.getSubnet) match { + .map { case (subGridGate, _) => subGridGate.getSuperiorNode } + .find(_.getUuid == firstRequestedNodeUuid) + .map(_.getSubnet) match { case Some(requestingSubnetNumber) => log.debug( "Received request for grid power values and im READY to provide." @@ -1062,7 +1055,8 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { * a map contains a mapping between nodes and the [[ActorRef]] s located @ * those nodes * @param refSystem - * the reference system of the [[GridModel]] of this [[GridAgent]] + * the reference system of the [[edu.ie3.simona.model.grid.GridModel]] of + * this [[GridAgent]] * @param askTimeout * a timeout for the request * @return @@ -1085,53 +1079,49 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { Some( Future .sequence( - nodeToAssetAgents - .flatten(nodeUuidWithActorRefs => { - val assetActorRefs = nodeUuidWithActorRefs._2 - val nodeUuid = nodeUuidWithActorRefs._1 - assetActorRefs.map(assetAgent => { - - val (eInPu, fInPU): ( - Quantity[Dimensionless], - Quantity[Dimensionless] - ) = - sweepValueStore match { - case Some(sweepValueStore) => - val assetNodeVoltageInSi = refSystem.vInSi( - sweepValueStore.sweepData - .find(_.nodeUuid == nodeUuid) - .getOrElse( - throw new DBFSAlgorithmException( - s"Provided Sweep value store contains no data for node with id $nodeUuid" - ) + nodeToAssetAgents.flatten { case (nodeUuid, assetActorRefs) => + assetActorRefs.map(assetAgent => { + + val (eInPu, fInPU): ( + Quantity[Dimensionless], + Quantity[Dimensionless] + ) = + sweepValueStore match { + case Some(sweepValueStore) => + val (pInSi, qInSi) = refSystem.vInSi( + sweepValueStore.sweepData + .find(_.nodeUuid == nodeUuid) + .getOrElse( + throw new DBFSAlgorithmException( + s"Provided Sweep value store contains no data for node with id $nodeUuid" ) - .stateData - .voltage - ) - ( - refSystem.vInPu(assetNodeVoltageInSi._1), - refSystem.vInPu(assetNodeVoltageInSi._2) - ) - case None => - ( - Quantities.getQuantity(1, PU), - Quantities.getQuantity(0, PU) - ) - } - - (assetAgent ? RequestAssetPowerMessage( - currentTick, - QuantityUtil.asComparable(eInPu), - QuantityUtil.asComparable(fInPU) - )).map { - case providedPowerValuesMessage: AssetPowerChangedMessage => - (assetAgent, Some(providedPowerValuesMessage)) - case assetPowerUnchangedMessage: AssetPowerUnchangedMessage => - (assetAgent, Some(assetPowerUnchangedMessage)) - }.mapTo[ActorPowerRequestResponse] - }) + ) + .stateData + .voltage + ) + ( + refSystem.vInPu(pInSi), + refSystem.vInPu(qInSi) + ) + case None => + ( + Quantities.getQuantity(1, PU), + Quantities.getQuantity(0, PU) + ) + } + + (assetAgent ? RequestAssetPowerMessage( + currentTick, + QuantityUtil.asComparable(eInPu), + QuantityUtil.asComparable(fInPU) + )).map { + case providedPowerValuesMessage: AssetPowerChangedMessage => + (assetAgent, Some(providedPowerValuesMessage)) + case assetPowerUnchangedMessage: AssetPowerUnchangedMessage => + (assetAgent, Some(assetPowerUnchangedMessage)) + }.mapTo[ActorPowerRequestResponse] }) - .toVector + }.toVector ) .map(ReceivedAssetPowerValues) .pipeTo(self) @@ -1172,17 +1162,23 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { .map { subGridGate => subGridGateToActorRef(subGridGate) -> subGridGate } - .groupBy(_._1) // Group the gates by target actor, so that only one request is sent per grid agent + .groupMap { + // Group the gates by target actor, so that only one request is sent per grid agent + case (inferiorGridAgentRef, _) => + inferiorGridAgentRef + } { case (_, inferiorGridGates) => + inferiorGridGates.getSuperiorNode.getUuid + } .map { case (inferiorGridAgentRef, inferiorGridGates) => (inferiorGridAgentRef ? RequestGridPowerMessage( currentSweepNo, - inferiorGridGates.map(_._2.getSuperiorNode.getUuid).distinct + inferiorGridGates.distinct )).map { case provideGridPowerMessage: ProvideGridPowerMessage => - (inferiorGridAgentRef, Option(provideGridPowerMessage)) + (inferiorGridAgentRef, Some(provideGridPowerMessage)) case FailedPowerFlow => (inferiorGridAgentRef, Some(FailedPowerFlow)) - }.mapTo[ActorPowerRequestResponse] + } } .toVector ) @@ -1237,11 +1233,12 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { None } - /** Create an instance of [[PowerFlowResults]] and send it to all listener - * Note: in the future this one could become a bottleneck for power flow - * calculation timesteps. For performance improvements one might consider - * putting this into a future with pipeTo. One has to consider how to deal - * with unfinished futures in shutdown phase then + /** Create an instance of + * [[edu.ie3.simona.event.ResultEvent.PowerFlowResultEvent]] and send it to + * all listener Note: in the future this one could become a bottleneck for + * power flow calculation timesteps. For performance improvements one might + * consider putting this into a future with pipeTo. One has to consider how + * to deal with unfinished futures in shutdown phase then * * @param gridAgentBaseData * the grid agent base data 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 12f6386a6e..a9c1109d92 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/GridAgentData.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/GridAgentData.scala @@ -19,7 +19,6 @@ import edu.ie3.simona.agent.grid.ReceivedValues.{ ReceivedSlackValues } import edu.ie3.simona.model.grid.{GridModel, RefSystem} -import edu.ie3.simona.ontology.messages.PowerMessage import edu.ie3.simona.ontology.messages.PowerMessage.{ FailedPowerFlow, PowerResponseMessage, @@ -82,7 +81,7 @@ object GridAgentData { ): PowerFlowDoneData = { /* Determine the subnet numbers of all superior grids */ val superiorSubGrids = gridAgentBaseData.gridEnv.subnetGateToActorRef - .map(_._1.getSuperiorNode.getSubnet) + .map { case (subGridGate, _) => subGridGate.getSuperiorNode.getSubnet } .filterNot(_ == gridAgentBaseData.gridEnv.gridModel.subnetNo) .toSet PowerFlowDoneData(gridAgentBaseData, powerFlowResult, superiorSubGrids) @@ -203,7 +202,9 @@ object GridAgentData { // we expect power values from inferior grids and assets val assetAndGridPowerValuesReady = receivedValueStore.nodeToReceivedPower.values.forall(vector => - vector.forall(actorRefOption => actorRefOption._2.isDefined) + vector.forall { case (_, powerResponseOpt) => + powerResponseOpt.isDefined + } ) // we expect slack voltages only from our superior grids (if any) val slackVoltageValuesReady = @@ -304,9 +305,7 @@ object GridAgentData { nodeToReceivedPower: Map[UUID, Vector[ActorPowerRequestResponse]], senderRef: ActorRef, replace: Boolean - ): Option[ - (UUID, Vector[(ActorRef, Option[PowerMessage.PowerResponseMessage])]) - ] = { + ): Option[UUID] = { nodeToReceivedPower .find { case (_, receivedPowerMessages) => receivedPowerMessages.exists { case (ref, maybePowerResponse) => @@ -317,6 +316,7 @@ object GridAgentData { maybePowerResponse.isDefined) } } + .map { case (uuid, _) => uuid } } /** Identify and update the vector of already received information. @@ -351,7 +351,6 @@ object GridAgentData { s"from $senderRef which is not in my power values nodes map or which cannot be replaced!" ) ) - ._1 case FailedPowerFlow => uuid(nodeToReceived, senderRef, replace) .getOrElse( @@ -360,7 +359,6 @@ object GridAgentData { s"from $senderRef which is not in my power values nodes map or which cannot be replaced!" ) ) - ._1 case unknownMsg => throw new RuntimeException( s"$actorName Unknown message received. Can't process message $unknownMsg." diff --git a/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala b/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala index e05afe3ffb..65026fd97f 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala @@ -32,6 +32,7 @@ import javax.measure.Quantity import javax.measure.quantity.{Dimensionless, ElectricPotential} import tech.units.indriya.quantity.Quantities +import scala.collection.mutable import scala.util.{Failure, Success, Try} /** Support and helper methods for power flow calculations provided by @@ -98,7 +99,7 @@ trait PowerFlowSupport { .get(nodeModel.uuid) match { case Some(actorRefsWithPower) => val (p, q) = actorRefsWithPower - .map(_._2) + .map { case (_, powerMsg) => powerMsg } .collect { case Some(providePowerMessage: ProvidePowerMessage) => providePowerMessage @@ -116,9 +117,9 @@ trait PowerFlowSupport { Quantities.getQuantity(0, mainRefSystemPowerUnit), Quantities.getQuantity(0, mainRefSystemPowerUnit) ) - )((pqSum, powerMessage) => { - (pqSum._1.add(powerMessage.p), pqSum._2.add(powerMessage.q)) - }) + ) { case ((pSum, qSum), powerMessage) => + (pSum.add(powerMessage.p), qSum.add(powerMessage.q)) + } new Complex( gridMainRefSystem.pInPu(p).getValue.doubleValue(), @@ -200,7 +201,7 @@ trait PowerFlowSupport { val nodeStateData = sweepValueStoreData.stateData val targetVoltage = if (nodeStateData.nodeType == NodeType.SL) { val receivedSlackVoltage = receivedSlackValues.values - .flatMap(_._2) + .flatMap { case (_, slackVoltageMsg) => slackVoltageMsg } .find(_.nodeUuid == sweepValueStoreData.nodeUuid) .getOrElse( throw new RuntimeException( @@ -245,15 +246,16 @@ trait PowerFlowSupport { validResult: ValidNewtonRaphsonPFResult, gridModel: GridModel ): String = { - val debugString = new StringBuilder("Power flow result: ") + val debugString = new mutable.StringBuilder("Power flow result: ") validResult.nodeData.foreach(nodeStateData => { - // get idx - val idx = nodeStateData.index + // get node index + val nodeIndex = nodeStateData.index // get nodeUUID val uuid = gridModel.nodeUuidToIndexMap - .find(_._2 == idx) + .find { case (_, index) => index == nodeIndex } + .map { case (uuid, _) => uuid } .getOrElse(throw new RuntimeException("NODE NOT FOUND REMOVE THIS ")) - ._1 + // get nodeId from UUID val nodeId = gridModel.gridComponents.nodes .find(_.uuid == uuid) @@ -459,8 +461,7 @@ trait PowerFlowSupport { (adaptedOperatingPoint.appended(adaptedNodePreset), false) case ((adaptedOperatingPoint, firstSlack), nodePreset) => (adaptedOperatingPoint.appended(nodePreset), firstSlack) - } - ._1 + } match { case (operatingPoint, _) => operatingPoint } Try { powerFlow.calculate( From 910a61a45fad177a884f5636aac3543fe917f43d Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Tue, 21 Jun 2022 17:48:21 +0200 Subject: [PATCH 08/36] Small code improvements --- .../edu/ie3/simona/agent/grid/DBFSAlgorithm.scala | 12 +++++------- .../scala/edu/ie3/simona/config/ConfigFailFast.scala | 1 - 2 files changed, 5 insertions(+), 8 deletions(-) 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 2f89439d2d..f5cc127191 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/DBFSAlgorithm.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/DBFSAlgorithm.scala @@ -292,13 +292,11 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { ) ) => /* Determine the subnet number of the grid agent, that has sent the request */ - val firstRequestedNodeUuid = requestedNodeUuids.headOption match { - case Some(uuid) => uuid - case None => - throw new DBFSAlgorithmException( - "Did receive a grid power request but without specified nodes" - ) - } + val firstRequestedNodeUuid = requestedNodeUuids.headOption.getOrElse( + throw new DBFSAlgorithmException( + "Did receive a grid power request but without specified nodes" + ) + ) gridAgentBaseData.gridEnv.subnetGateToActorRef .map { case (subGridGate, _) => subGridGate.getSuperiorNode } .find(_.getUuid == firstRequestedNodeUuid) diff --git a/src/main/scala/edu/ie3/simona/config/ConfigFailFast.scala b/src/main/scala/edu/ie3/simona/config/ConfigFailFast.scala index a56c0a5178..4eb817b5f5 100644 --- a/src/main/scala/edu/ie3/simona/config/ConfigFailFast.scala +++ b/src/main/scala/edu/ie3/simona/config/ConfigFailFast.scala @@ -32,7 +32,6 @@ import tech.units.indriya.unit.Units import java.time.temporal.ChronoUnit import java.util.UUID -import javax.measure.quantity.ElectricPotential import scala.util.{Failure, Success, Try} /** Sanity checks for [[SimonaConfig]] that should lead to a fast failure during From 617cfa1e6a98cf14e1f98b34a2613c8b4422a7bd Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Tue, 21 Jun 2022 18:18:20 +0200 Subject: [PATCH 09/36] More code improvements --- .../ie3/simona/agent/grid/DBFSAlgorithm.scala | 28 ++++++++----------- 1 file changed, 12 insertions(+), 16 deletions(-) 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 f5cc127191..06680547bd 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/DBFSAlgorithm.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/DBFSAlgorithm.scala @@ -45,14 +45,14 @@ import edu.ie3.simona.ontology.messages.VoltageMessage.{ } import edu.ie3.simona.ontology.trigger.Trigger._ import edu.ie3.simona.util.TickUtil._ +import edu.ie3.util.quantities.PowerSystemUnits import edu.ie3.util.quantities.PowerSystemUnits._ -import edu.ie3.util.quantities.{PowerSystemUnits, QuantityUtil} import tech.units.indriya.quantity.Quantities import java.time.{Duration, ZonedDateTime} import java.util.UUID import javax.measure.Quantity -import javax.measure.quantity.{Dimensionless, ElectricPotential} +import javax.measure.quantity.ElectricPotential import scala.concurrent.{ExecutionContext, Future} /** Trait that is normally mixed into every [[GridAgent]] to enable distributed @@ -172,10 +172,7 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { // we either have voltages ready calculated (not the first sweep) or we don't have them here // -> return calculated value or target voltage as physical value - val (slackE, slackF): ( - Quantity[ElectricPotential], - Quantity[ElectricPotential] - ) = + val (slackE, slackF) = (gridAgentBaseData.sweepValueStores.get(currentSweepNo) match { case Some(result) => Some(result, currentSweepNo) @@ -1080,10 +1077,7 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { nodeToAssetAgents.flatten { case (nodeUuid, assetActorRefs) => assetActorRefs.map(assetAgent => { - val (eInPu, fInPU): ( - Quantity[Dimensionless], - Quantity[Dimensionless] - ) = + val (eInPu, fInPU) = sweepValueStore match { case Some(sweepValueStore) => val (pInSi, qInSi) = refSystem.vInSi( @@ -1110,8 +1104,8 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { (assetAgent ? RequestAssetPowerMessage( currentTick, - QuantityUtil.asComparable(eInPu), - QuantityUtil.asComparable(fInPU) + eInPu, + fInPU )).map { case providedPowerValuesMessage: AssetPowerChangedMessage => (assetAgent, Some(providedPowerValuesMessage)) @@ -1158,19 +1152,21 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { .sequence( inferiorGridGates .map { subGridGate => - subGridGateToActorRef(subGridGate) -> subGridGate + subGridGateToActorRef( + subGridGate + ) -> subGridGate.getSuperiorNode.getUuid } .groupMap { // Group the gates by target actor, so that only one request is sent per grid agent case (inferiorGridAgentRef, _) => inferiorGridAgentRef } { case (_, inferiorGridGates) => - inferiorGridGates.getSuperiorNode.getUuid + inferiorGridGates } - .map { case (inferiorGridAgentRef, inferiorGridGates) => + .map { case (inferiorGridAgentRef, inferiorGridGateNodes) => (inferiorGridAgentRef ? RequestGridPowerMessage( currentSweepNo, - inferiorGridGates.distinct + inferiorGridGateNodes.distinct )).map { case provideGridPowerMessage: ProvideGridPowerMessage => (inferiorGridAgentRef, Some(provideGridPowerMessage)) From e9645e40f911383a7fdeca6913d190999f363c5b Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Tue, 21 Jun 2022 22:49:40 +0200 Subject: [PATCH 10/36] Consolidate ReceivedValues and ReceivedValuesStore field types --- .../ie3/simona/agent/grid/DBFSAlgorithm.scala | 61 ++++++++----------- .../ie3/simona/agent/grid/GridAgentData.scala | 52 ++++++---------- .../simona/agent/grid/PowerFlowSupport.scala | 2 +- .../simona/agent/grid/ReceivedValues.scala | 11 ++-- .../agent/grid/ReceivedValuesStore.scala | 24 +++++--- 5 files changed, 67 insertions(+), 83 deletions(-) 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 06680547bd..8109b58e40 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/DBFSAlgorithm.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/DBFSAlgorithm.scala @@ -45,13 +45,11 @@ import edu.ie3.simona.ontology.messages.VoltageMessage.{ } import edu.ie3.simona.ontology.trigger.Trigger._ import edu.ie3.simona.util.TickUtil._ -import edu.ie3.util.quantities.PowerSystemUnits import edu.ie3.util.quantities.PowerSystemUnits._ import tech.units.indriya.quantity.Quantities import java.time.{Duration, ZonedDateTime} import java.util.UUID -import javax.measure.Quantity import javax.measure.quantity.ElectricPotential import scala.concurrent.{ExecutionContext, Future} @@ -224,7 +222,7 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { uuid == nodeUuid && isSlack } .map(_.vTarget) - .getOrElse(Quantities.getQuantity(1d, PowerSystemUnits.PU)) + .getOrElse(Quantities.getQuantity(1d, PU)) val vSlack = vTarget .multiply( gridAgentBaseData.gridEnv.gridModel.mainRefSystem.nominalVoltage @@ -335,8 +333,8 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { case _ => /* TODO: As long as there are no multiple slack nodes, provide "real" power only for the slack node */ ( - Quantities.getQuantity(0d, PowerSystemUnits.MEGAWATT), - Quantities.getQuantity(0d, PowerSystemUnits.MEGAVAR) + Quantities.getQuantity(0d, MEGAWATT), + Quantities.getQuantity(0d, MEGAVAR) ) } .getOrElse { @@ -561,12 +559,8 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { // if yes, we do another PF with adapted values // if no, we are done with the pf and ready to report to our parent grid val changed = receivedPowerValues.values.exists { - case (_, providePowerResponseMsgOpt) => - providePowerResponseMsgOpt.exists { - case _: AssetPowerChangedMessage => true - case _ => false - } - case _ => false + case (_, _: AssetPowerChangedMessage) => true + case _ => false } if (changed) { @@ -1108,10 +1102,10 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { fInPU )).map { case providedPowerValuesMessage: AssetPowerChangedMessage => - (assetAgent, Some(providedPowerValuesMessage)) + (assetAgent, providedPowerValuesMessage) case assetPowerUnchangedMessage: AssetPowerUnchangedMessage => - (assetAgent, Some(assetPowerUnchangedMessage)) - }.mapTo[ActorPowerRequestResponse] + (assetAgent, assetPowerUnchangedMessage) + } }) }.toVector ) @@ -1169,9 +1163,9 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { inferiorGridGateNodes.distinct )).map { case provideGridPowerMessage: ProvideGridPowerMessage => - (inferiorGridAgentRef, Some(provideGridPowerMessage)) + (inferiorGridAgentRef, provideGridPowerMessage) case FailedPowerFlow => - (inferiorGridAgentRef, Some(FailedPowerFlow)) + (inferiorGridAgentRef, FailedPowerFlow) } } .toVector @@ -1206,25 +1200,22 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { s"asking superior grids for slack voltage values: {}", superiorGridGates ) - if (superiorGridGates.nonEmpty) - Some( - Future - .sequence( - superiorGridGates.map(superiorGridGate => { - val superiorGridAgent = subGridGateToActorRef(superiorGridGate) - (superiorGridAgent ? RequestSlackVoltageMessage( - currentSweepNo, - superiorGridGate.getSuperiorNode.getUuid - )).map(providedSlackValues => - (superiorGridAgent, Option(providedSlackValues)) - ).mapTo[(ActorRef, Option[ProvideSlackVoltageMessage])] - }) - ) - .map(ReceivedSlackValues) - .pipeTo(self) - ) - else - None + Option.when(superiorGridGates.nonEmpty) { + Future + .sequence( + superiorGridGates.map(superiorGridGate => { + val superiorGridAgent = subGridGateToActorRef(superiorGridGate) + (superiorGridAgent ? RequestSlackVoltageMessage( + currentSweepNo, + superiorGridGate.getSuperiorNode.getUuid + )).map { case providedSlackValues: ProvideSlackVoltageMessage => + (superiorGridAgent, providedSlackValues) + } + }) + ) + .map(ReceivedSlackValues) + .pipeTo(self) + } } /** Create an instance of 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 a9c1109d92..3f2d140889 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/GridAgentData.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/GridAgentData.scala @@ -6,7 +6,6 @@ package edu.ie3.simona.agent.grid -import java.util.UUID import akka.actor.ActorRef import akka.event.LoggingAdapter import edu.ie3.datamodel.graph.SubGridGate @@ -14,10 +13,10 @@ import edu.ie3.datamodel.models.input.container.SubGridContainer import edu.ie3.powerflow.model.PowerFlowResult import edu.ie3.powerflow.model.PowerFlowResult.SuccessFullPowerFlowResult.ValidNewtonRaphsonPFResult import edu.ie3.simona.agent.grid.ReceivedValues.{ - ActorPowerRequestResponse, ReceivedPowerValues, ReceivedSlackValues } +import edu.ie3.simona.agent.grid.ReceivedValuesStore.NodeToReceivedPower import edu.ie3.simona.model.grid.{GridModel, RefSystem} import edu.ie3.simona.ontology.messages.PowerMessage.{ FailedPowerFlow, @@ -26,6 +25,8 @@ import edu.ie3.simona.ontology.messages.PowerMessage.{ ProvidePowerMessage } +import java.util.UUID + sealed trait GridAgentData /** Contains all state data of [[GridAgent]] @@ -239,22 +240,11 @@ object GridAgentData { val updatedNodeToReceivedPowersMap = receivedPowerValues.values.foldLeft( receivedValueStore.nodeToReceivedPower ) { - case ( - nodeToReceivedPowerValuesMapWithAddedPowerResponse, - (senderRef, Some(providePowerMessage: ProvidePowerMessage)) - ) => - /* This is a message with only one nodal power */ - updateNodalReceivedVector( - providePowerMessage, - nodeToReceivedPowerValuesMapWithAddedPowerResponse, - senderRef, - replace - ) case ( nodeToReceivedPowerValuesMapWithAddedPowerResponse, ( senderRef, - Some(provideGridPowerMessage: ProvideGridPowerMessage) + provideGridPowerMessage: ProvideGridPowerMessage ) ) => /* Go over all includes messages and add them. */ @@ -272,13 +262,16 @@ object GridAgentData { replace ) } - case (_, (senderRef, Some(unsupported))) => - throw new RuntimeException( - s"Received an unsupported type of power provision message from '$senderRef', which I cannot add to the register of received messages: $unsupported" - ) - case (_, (senderRef, None)) => - throw new RuntimeException( - s"Received a 'None' as provided power from '$senderRef'. Provision of 'None' is not supported." + case ( + nodeToReceivedPowerValuesMapWithAddedPowerResponse, + (senderRef, powerResponseMessage) + ) => + // some other singular power response message + updateNodalReceivedVector( + powerResponseMessage, + nodeToReceivedPowerValuesMapWithAddedPowerResponse, + senderRef, + replace ) } this.copy( @@ -302,10 +295,10 @@ object GridAgentData { * @return */ private def uuid( - nodeToReceivedPower: Map[UUID, Vector[ActorPowerRequestResponse]], + nodeToReceivedPower: NodeToReceivedPower, senderRef: ActorRef, replace: Boolean - ): Option[UUID] = { + ): Option[UUID] = nodeToReceivedPower .find { case (_, receivedPowerMessages) => receivedPowerMessages.exists { case (ref, maybePowerResponse) => @@ -317,7 +310,6 @@ object GridAgentData { } } .map { case (uuid, _) => uuid } - } /** Identify and update the vector of already received information. * @@ -335,9 +327,7 @@ object GridAgentData { */ private def updateNodalReceivedVector( powerResponse: PowerResponseMessage, - nodeToReceived: Map[UUID, Vector[ - (ActorRef, Option[PowerResponseMessage]) - ]], + nodeToReceived: NodeToReceivedPower, senderRef: ActorRef, replace: Boolean ): Map[UUID, Vector[(ActorRef, Option[PowerResponseMessage])]] = { @@ -403,7 +393,7 @@ object GridAgentData { ) { case ( nodeToSlackVoltageUpdated, - (senderRef, maybeSlackValues @ Some(slackValues)) + (senderRef, slackValues) ) => val nodeUuid: UUID = slackValues.nodeUuid @@ -411,7 +401,7 @@ object GridAgentData { .get(nodeUuid) match { case Some(None) => /* Slack voltage is expected and not yet received */ - nodeToSlackVoltageUpdated + (nodeUuid -> maybeSlackValues) + nodeToSlackVoltageUpdated + (nodeUuid -> Some(slackValues)) case Some(Some(_)) => throw new RuntimeException( s"Already received slack value for node $nodeUuid!" @@ -421,10 +411,6 @@ object GridAgentData { s"Received slack value for node $nodeUuid from $senderRef which is not in my slack values nodes list!" ) } - case (_, (senderRef, None)) => - throw new RuntimeException( - s"Received an empty voltage message from $senderRef" - ) } this.copy( receivedValueStore = receivedValueStore.copy( diff --git a/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala b/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala index 65026fd97f..ad1dbd9098 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala @@ -201,7 +201,7 @@ trait PowerFlowSupport { val nodeStateData = sweepValueStoreData.stateData val targetVoltage = if (nodeStateData.nodeType == NodeType.SL) { val receivedSlackVoltage = receivedSlackValues.values - .flatMap { case (_, slackVoltageMsg) => slackVoltageMsg } + .map { case (_, slackVoltageMsg) => slackVoltageMsg } .find(_.nodeUuid == sweepValueStoreData.nodeUuid) .getOrElse( throw new RuntimeException( diff --git a/src/main/scala/edu/ie3/simona/agent/grid/ReceivedValues.scala b/src/main/scala/edu/ie3/simona/agent/grid/ReceivedValues.scala index 48047822a5..7c578c858a 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/ReceivedValues.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/ReceivedValues.scala @@ -15,12 +15,10 @@ import edu.ie3.simona.ontology.messages.VoltageMessage.ProvideSlackVoltageMessag */ sealed trait ReceivedValues -case object ReceivedValues { +object ReceivedValues { - type ActorPowerRequestResponse = - (ActorRef, Option[PowerResponseMessage]) - type ActorSlackVoltageRequestResponse = - (ActorRef, Option[ProvideSlackVoltageMessage]) + type ActorPowerRequestResponse = (ActorRef, PowerResponseMessage) + type ActorSlackVoltageRequestResponse = (ActorRef, ProvideSlackVoltageMessage) sealed trait ReceivedPowerValues extends ReceivedValues { def values: Vector[ActorPowerRequestResponse] @@ -29,6 +27,7 @@ case object ReceivedValues { /** Wrapper for received asset power values (p, q) * * @param values + * the asset power values and their senders */ final case class ReceivedAssetPowerValues( values: Vector[ActorPowerRequestResponse] @@ -37,6 +36,7 @@ case object ReceivedValues { /** Wrapper for received grid power values (p, q) * * @param values + * the grid power values and their senders */ final case class ReceivedGridPowerValues( values: Vector[ActorPowerRequestResponse] @@ -45,6 +45,7 @@ case object ReceivedValues { /** Wrapper for received slack voltage values (v) * * @param values + * the slack voltage values and their senders */ final case class ReceivedSlackValues( values: Vector[ActorSlackVoltageRequestResponse] diff --git a/src/main/scala/edu/ie3/simona/agent/grid/ReceivedValuesStore.scala b/src/main/scala/edu/ie3/simona/agent/grid/ReceivedValuesStore.scala index 391032e071..69b88a6bd3 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/ReceivedValuesStore.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/ReceivedValuesStore.scala @@ -6,14 +6,20 @@ package edu.ie3.simona.agent.grid -import java.util.UUID - import akka.actor.ActorRef import edu.ie3.datamodel.graph.SubGridGate -import edu.ie3.simona.agent.grid.ReceivedValues.ActorPowerRequestResponse -import edu.ie3.simona.ontology.messages.PowerMessage.ProvidePowerMessage +import edu.ie3.simona.agent.grid.ReceivedValuesStore.{ + NodeToReceivedPower, + NodeToReceivedSlackVoltage +} +import edu.ie3.simona.ontology.messages.PowerMessage.{ + PowerResponseMessage, + ProvidePowerMessage +} import edu.ie3.simona.ontology.messages.VoltageMessage.ProvideSlackVoltageMessage +import java.util.UUID + /** Value store that contains all data that should be received by the * [[GridAgent]] from other agents. The mapping is structured as the uuid of a * node to a tuple of either a vector of actorRefs to @@ -34,14 +40,14 @@ import edu.ie3.simona.ontology.messages.VoltageMessage.ProvideSlackVoltageMessag * [[GridAgent]] s if any */ final case class ReceivedValuesStore private ( - nodeToReceivedPower: Map[UUID, Vector[ActorPowerRequestResponse]], - nodeToReceivedSlackVoltage: Map[UUID, Option[ProvideSlackVoltageMessage]] + nodeToReceivedPower: NodeToReceivedPower, + nodeToReceivedSlackVoltage: NodeToReceivedSlackVoltage ) -case object ReceivedValuesStore { +object ReceivedValuesStore { type NodeToReceivedPower = - Map[UUID, Vector[(ActorRef, Option[ProvidePowerMessage])]] + Map[UUID, Vector[(ActorRef, Option[PowerResponseMessage])]] type NodeToReceivedSlackVoltage = Map[UUID, Option[ProvideSlackVoltageMessage]] @@ -109,7 +115,7 @@ case object ReceivedValuesStore { subOrdinateToReceivedPower, (inferiorSubGridRef, couplingNodeUuid) ) => - /* Check, if there is yet something expected for the given coupling node and add reference to the subordinate + /* Check, if there is already something expected for the given coupling node and add reference to the subordinate * grid agent */ val actorRefToMessage = subOrdinateToReceivedPower .getOrElse( From 6d0dd292c6e748292421b9ae43a9754bad7bb6c2 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Tue, 21 Jun 2022 22:50:59 +0200 Subject: [PATCH 11/36] Possibly fixing codacy issue --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a94b0c2cd..e5ab452b1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,8 +49,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix treatment of non-InitializeTrigger triggers in initialization within SimScheduler [#237](https://github.com/ie3-institute/simona/issues/237) - Fix breaking SIMONA caused by introducing temperature dependant load profiles in PSDM [#255](https://github.com/ie3-institute/simona/issues/255) - Fix power exchange between grids [#22](https://github.com/ie3-institute/simona/issues/22) - - Consolidate request replies for different sub grid gates in one message - - Await and send responses for distinct pairs of sender reference and target node + - Consolidate request replies for different sub grid gates in one message + - Await and send responses for distinct pairs of sender reference and target node ### Removed - Remove workaround for tscfg tmp directory [#178](https://github.com/ie3-institute/simona/issues/178) From 2ad52c263970518ade7ae98e79db51e284b34aa2 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Wed, 22 Jun 2022 09:50:58 +0200 Subject: [PATCH 12/36] Refactoring --- .../ie3/simona/agent/grid/GridAgentData.scala | 62 +++++++++---------- 1 file changed, 31 insertions(+), 31 deletions(-) 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 3f2d140889..1a8105c5f2 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/GridAgentData.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/GridAgentData.scala @@ -280,37 +280,6 @@ object GridAgentData { ) } - /** Find the uuid of the grid node the provided actor sender ref is located - * on. - * - * @param nodeToReceivedPower - * a mapping of a grid node uuid to all actors and their optionally - * already provided power responses - * @param senderRef - * the actor whose node uuid should be determined - * @param replace - * if true, it is checked if the sender has already provided power - * values, which should be replaced, if false, it is checked if the - * sender has no yet provided power values - * @return - */ - private def uuid( - nodeToReceivedPower: NodeToReceivedPower, - senderRef: ActorRef, - replace: Boolean - ): Option[UUID] = - nodeToReceivedPower - .find { case (_, receivedPowerMessages) => - receivedPowerMessages.exists { case (ref, maybePowerResponse) => - ref == senderRef && - (if (!replace) - maybePowerResponse.isEmpty - else - maybePowerResponse.isDefined) - } - } - .map { case (uuid, _) => uuid } - /** Identify and update the vector of already received information. * * @param powerResponse @@ -375,6 +344,37 @@ object GridAgentData { .updated(nodeUuid, receivedVector) } + /** Find the uuid of the grid node the provided actor sender ref is located + * on. + * + * @param nodeToReceivedPower + * a mapping of a grid node uuid to all actors and their optionally + * already provided power responses + * @param senderRef + * the actor whose node uuid should be determined + * @param replace + * if true, it is checked if the sender has already provided power + * values, which should be replaced, if false, it is checked if the + * sender has no yet provided power values + * @return + */ + private def uuid( + nodeToReceivedPower: NodeToReceivedPower, + senderRef: ActorRef, + replace: Boolean + ): Option[UUID] = + nodeToReceivedPower + .find { case (_, receivedPowerMessages) => + receivedPowerMessages.exists { case (ref, maybePowerResponse) => + ref == senderRef && + (if (!replace) + maybePowerResponse.isEmpty + else + maybePowerResponse.isDefined) + } + } + .map { case (uuid, _) => uuid } + /** Update this [[GridAgentBaseData]] with [[ReceivedSlackValues]] and * return a copy of this [[GridAgentBaseData]] for further processing * From f38ac3150be66fec42f2ee57ddc271a45769d197 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Wed, 29 Jun 2022 16:44:52 +0200 Subject: [PATCH 13/36] Applying code style improvements --- .../edu/ie3/simona/agent/grid/GridAgentData.scala | 6 +++--- .../edu/ie3/simona/agent/grid/PowerFlowSupport.scala | 11 +++-------- 2 files changed, 6 insertions(+), 11 deletions(-) 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 d214620337..311a3a80e3 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/GridAgentData.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/GridAgentData.scala @@ -255,7 +255,7 @@ object GridAgentData { nodeToReceivedPowerValuesMapWithAddedExchangedPower, exchangedPower ) => - updateNodalReceivedVector( + updateNodalReceivedPower( exchangedPower, nodeToReceivedPowerValuesMapWithAddedExchangedPower, senderRef, @@ -267,7 +267,7 @@ object GridAgentData { (senderRef, powerResponseMessage) ) => // some other singular power response message - updateNodalReceivedVector( + updateNodalReceivedPower( powerResponseMessage, nodeToReceivedPowerValuesMapWithAddedPowerResponse, senderRef, @@ -294,7 +294,7 @@ object GridAgentData { * The nodal uuid as well as the updated collection of received * information */ - private def updateNodalReceivedVector( + private def updateNodalReceivedPower( powerResponse: PowerResponseMessage, nodeToReceived: NodeToReceivedPower, senderRef: ActorRef, diff --git a/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala b/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala index ad1dbd9098..61cf39228f 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala @@ -82,8 +82,7 @@ trait PowerFlowSupport { val mainRefSystemPowerUnit = gridMainRefSystem.nominalPower.getUnit - nodes.foldLeft(Array.empty[PresetData])((operatingPoint, nodeModel) => { - + nodes.toArray.map { nodeModel => // note: currently we only support pq nodes as we not distinguish between pq/pv nodes - // when slack emulators or pv-node assets are added this needs to be considered here val nodeType = if (nodeModel.isSlack) NodeType.SL else NodeType.PQ @@ -159,12 +158,8 @@ trait PowerFlowSupport { else 1.0) } - val presetNodeData = - PresetData(nodeIdx, nodeType, apparentPower, targetVoltageInPu.abs) - - operatingPoint :+ presetNodeData - - }) + PresetData(nodeIdx, nodeType, apparentPower, targetVoltageInPu.abs) + } } /** Composes the current operation point needed by From 314d8d5d208e848b0b57602039352a2137f4f1ac Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Wed, 29 Jun 2022 19:55:58 +0200 Subject: [PATCH 14/36] Simplifying ReceivedValuesStore --- .../ie3/simona/agent/grid/ReceivedValuesStore.scala | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/agent/grid/ReceivedValuesStore.scala b/src/main/scala/edu/ie3/simona/agent/grid/ReceivedValuesStore.scala index bfabf0fd55..312ea28814 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/ReceivedValuesStore.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/ReceivedValuesStore.scala @@ -103,19 +103,15 @@ object ReceivedValuesStore { (uuid, actorRefs.map(actorRef => actorRef -> None).toMap) } - /* Add everything, that I expect from my sub ordinate grid agents. - * Build distinct pairs of sending actor reference and target node. - * Convert to sequence first, since the map operation conflates - * key/value pairs with the same key */ - inferiorSubGridGateToActorRef.toSeq + /* Add everything, that I expect from my sub ordinate grid agents. */ + inferiorSubGridGateToActorRef .map { case (gate, reference) => - reference -> gate.getSuperiorNode.getUuid + gate.getSuperiorNode.getUuid -> reference } - .distinct .foldLeft(assetsToReceivedPower) { case ( subOrdinateToReceivedPower, - inferiorSubGridRef -> couplingNodeUuid + couplingNodeUuid -> inferiorSubGridRef ) => /* Check, if there is already something expected for the given coupling node * and add reference to the subordinate grid agent */ From 2ad69c43703ebae16bd47987476a3dc668c1ea31 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Wed, 29 Jun 2022 19:56:45 +0200 Subject: [PATCH 15/36] Further improving DBFS tests, removing synchronous tests --- .../agent/grid/DBFSAlgorithmCenGridSpec.scala | 907 +++++------------- .../agent/grid/DBFSAlgorithmSupGridSpec.scala | 327 +------ .../test/common/model/grid/DbfsTestGrid.scala | 58 +- 3 files changed, 264 insertions(+), 1028 deletions(-) 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 016c2e4a47..3c33db78c9 100644 --- a/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmCenGridSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmCenGridSpec.scala @@ -8,17 +8,12 @@ package edu.ie3.simona.agent.grid import akka.actor.{ActorRef, ActorSystem} import akka.pattern.ask -import akka.testkit.{ImplicitSender, TestFSMRef, TestProbe} +import akka.testkit.{ImplicitSender, TestProbe} import akka.util.Timeout import com.typesafe.config.ConfigFactory import edu.ie3.simona.agent.EnvironmentRefs import edu.ie3.simona.agent.grid.GridAgentData.GridAgentInitData -import edu.ie3.simona.agent.state.AgentState -import edu.ie3.simona.agent.state.AgentState.{Idle, Uninitialized} -import edu.ie3.simona.agent.state.GridAgentState.{ - HandlePowerFlowCalculations, - SimulateGrid -} +import edu.ie3.simona.agent.state.GridAgentState.SimulateGrid import edu.ie3.simona.model.grid.RefSystem import edu.ie3.simona.ontology.messages.PowerMessage.ProvideGridPowerMessage.ExchangePower import edu.ie3.simona.ontology.messages.PowerMessage.{ @@ -46,14 +41,11 @@ import edu.ie3.simona.test.common.{ UnitSpec } import edu.ie3.util.quantities.PowerSystemUnits._ -import org.scalatest.Ignore import tech.units.indriya.quantity.Quantities import java.util.UUID -import java.util.concurrent.{TimeUnit, TimeoutException} -import scala.concurrent.{Await, Future} -import scala.concurrent.duration._ -import scala.util.{Failure, Success, Try} +import java.util.concurrent.TimeUnit +import scala.concurrent.Await /** Test to ensure the functions that a [[GridAgent]] in center position should * be able to do if the DBFSAlgorithm is used. The scheduler, the weather @@ -85,8 +77,22 @@ class DBFSAlgorithmCenGridSpec private val primaryService = TestProbe("primaryService") private val weatherService = TestProbe("weatherService") - private val superiorGridAgent = TestProbe("superiorGridAgent") - private val inferiorGridAgent = TestProbe("inferiorGridAgent") + private val superiorGridAgent = TestProbe("superiorGridAgent_1000") + private val inferiorAndNodes11 = ( + TestProbe("inferiorGridAgent_11"), + Vector(UUID.fromString("78c5d473-e01b-44c4-afd2-e4ff3c4a5d7c")) + ) + private val inferiorAndNodes12 = ( + TestProbe("inferiorGridAgent_12"), + Vector(UUID.fromString("e364ef00-e6ca-46b1-ba2b-bb73c0c6fee0")) + ) + private val inferiorAndNodes13 = ( + TestProbe("inferiorGridAgent_13"), + Vector( + UUID.fromString("d44ba8ed-81db-4a22-a40d-f7c0d0808a75"), + UUID.fromString("47ef9983-8fcf-4713-be90-093fc27864ae") + ) + ) private val environmentRefs = EnvironmentRefs( scheduler = scheduler.ref, @@ -95,29 +101,31 @@ class DBFSAlgorithmCenGridSpec evDataService = None ) - "A GridAgent actor in center position with FSM test" should { + "A GridAgent actor in center position with async test" should { - val centerGridAgent = TestFSMRef( - new GridAgent( - environmentRefs, - simonaConfig, - listener = Iterable.empty[ActorRef] + val centerGridAgent = + system.actorOf( + GridAgent.props( + environmentRefs, + simonaConfig, + listener = Iterable.empty[ActorRef] + ) ) - ) - - s"be in state $Uninitialized after startup" in { - centerGridAgent.stateName shouldBe Uninitialized - } s"initialize itself when it receives a $InitializeGridAgentTrigger with corresponding data" in { val triggerId = 0 - // this subnet has 1 superior grid (HöS) and 4 inferior grids (MS). Map the gates to test probes accordingly + // this subnet has 1 superior grid (HöS) and 3 inferior grids (MS). Map the gates to test probes accordingly val subGridGateToActorRef = hvSubGridGates.map { - case gate - if gate.getInferiorNode.getSubnet == hvGridContainer.getSubnet => + case gate if gate.getInferiorSubGrid == hvGridContainer.getSubnet => gate -> superiorGridAgent.ref - case gate => gate -> inferiorGridAgent.ref + case gate => + val actor = gate.getInferiorSubGrid match { + case 11 => inferiorAndNodes11._1 + case 12 => inferiorAndNodes12._1 + case 13 => inferiorAndNodes13._1 + } + gate -> actor.ref }.toMap val gridAgentInitData = @@ -151,9 +159,6 @@ class DBFSAlgorithmCenGridSpec ) ) - // grid agent state should be idle afterwards - centerGridAgent.stateName shouldBe Idle - } s"go to $SimulateGrid when it receives an activity start trigger" in { @@ -181,28 +186,14 @@ class DBFSAlgorithmCenGridSpec s"Invalid message received when expecting a completion message after activity start trigger. Message was $x" ) } - - // grid agent stat should be simulate grid afterwards - centerGridAgent.stateName shouldBe SimulateGrid - } - /* Test is currently ignored. Reason: The TestFSMRef set up above explicitly sets the dispatcher of the actor - * context to `CurrentThreadDispatcher`, which runs on the single, calling thread and thereby may also lead to - * dead locks in certain circumstances (obviously we found one). As the refactoring to akka typed is foreseen and - * refactoring of the test is needed as well, we will ignore this test until then */ - s"start the simulation when a $StartGridSimulationTrigger is send" ignore { + s"start the simulation when a $StartGridSimulationTrigger is send" in { val startGridSimulationTriggerId = 2 val firstSweepNo = 0 val slackNodeUuid = UUID.fromString("9fe5fa33-6d3b-4153-a829-a16f4347bc4e") - val inferiorGridNodeUuids = Vector( - UUID.fromString("d44ba8ed-81db-4a22-a40d-f7c0d0808a75"), - UUID.fromString("e364ef00-e6ca-46b1-ba2b-bb73c0c6fee0"), - UUID.fromString("78c5d473-e01b-44c4-afd2-e4ff3c4a5d7c"), - UUID.fromString("47ef9983-8fcf-4713-be90-093fc27864ae") - ) // send the start grid simulation trigger scheduler.send( @@ -214,31 +205,29 @@ class DBFSAlgorithmCenGridSpec ) ) - /* We expect one grid power request message, as all four sub grids are mapped onto one actor reference */ - val firstGridPowerRequests = inferiorGridAgent - .receiveWhile() { case msg @ RequestGridPowerMessage(_, _) => - msg -> inferiorGridAgent.lastSender - } - .toMap - - /* We receive one message with all requests for all inferior grid agents. This is, because the requests are - * grouped by the actor, they are sent to. Here, the test itself serves as the dummy for all inferior grid - * agents */ - firstGridPowerRequests.size shouldBe 1 - firstGridPowerRequests.keys.headOption match { - case Some(RequestGridPowerMessage(_, nodeUuids)) => - nodeUuids should contain theSameElementsAs inferiorGridNodeUuids - case None => fail("Did expect to receive something") - } + /* We expect one grid power request message per inferior grid */ + + inferiorAndNodes11._1 + .expectMsgType[RequestGridPowerMessage] + .nodeUuids shouldBe inferiorAndNodes11._2 + val firstPowerRequestSender11 = inferiorAndNodes11._1.lastSender + + inferiorAndNodes12._1 + .expectMsgType[RequestGridPowerMessage] + .nodeUuids shouldBe inferiorAndNodes12._2 + val firstPowerRequestSender12 = inferiorAndNodes12._1.lastSender + + inferiorAndNodes13._1 + .expectMsgType[RequestGridPowerMessage] + .nodeUuids should contain allElementsOf inferiorAndNodes13._2 + val firstPowerRequestSender13 = inferiorAndNodes13._1.lastSender // we expect 1 request for slack voltage values // (slack values are requested by our agent under test from the superior grid) val firstSlackVoltageRequest = superiorGridAgent.expectMsgPF() { case request @ RequestSlackVoltageMessage(sweepNo, nodeId) => sweepNo shouldBe firstSweepNo - nodeId shouldBe UUID.fromString( - "9fe5fa33-6d3b-4153-a829-a16f4347bc4e" - ) + nodeId shouldBe slackNodeUuid (request, superiorGridAgent.lastSender) case x => fail( @@ -247,575 +236,126 @@ class DBFSAlgorithmCenGridSpec } // 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 4 inferior grid agents - inferiorGridNodeUuids.foreach { nodeUuid => - inferiorGridAgent.send( + // we simulate this behaviour now by doing the same for our three inferior grid agents + + inferiorAndNodes11._2.foreach { nodeUuid => + inferiorAndNodes11._1.send( centerGridAgent, RequestSlackVoltageMessage(firstSweepNo, nodeUuid) ) } - // 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) - val providedSlackVoltages = inferiorGridAgent.receiveWhile() { - case provideSlackVoltageMessage: ProvideSlackVoltageMessage => - provideSlackVoltageMessage - case x => - fail( - s"Invalid message received when expecting slack voltage provision message. Message was $x" - ) - } - - providedSlackVoltages.size shouldBe 4 - providedSlackVoltages should contain allOf ( - ProvideSlackVoltageMessage( - firstSweepNo, - UUID.fromString("d44ba8ed-81db-4a22-a40d-f7c0d0808a75"), - Quantities.getQuantity(110, KILOVOLT), - Quantities.getQuantity(0, KILOVOLT) - ), - ProvideSlackVoltageMessage( - firstSweepNo, - UUID.fromString("e364ef00-e6ca-46b1-ba2b-bb73c0c6fee0"), - Quantities.getQuantity(110, KILOVOLT), - Quantities.getQuantity(0, KILOVOLT) - ), - ProvideSlackVoltageMessage( - firstSweepNo, - UUID.fromString("78c5d473-e01b-44c4-afd2-e4ff3c4a5d7c"), - Quantities.getQuantity(110, KILOVOLT), - Quantities.getQuantity(0, KILOVOLT) - ), - ProvideSlackVoltageMessage( - firstSweepNo, - UUID.fromString("47ef9983-8fcf-4713-be90-093fc27864ae"), - Quantities.getQuantity(110, KILOVOLT), - Quantities.getQuantity(0, KILOVOLT) - ) - ) - - // we now answer the request of our centerGridAgent - // with 1 fake grid power message and 1 fake slack voltage message - firstGridPowerRequests.foreach { case (requestMessage, askSender) => - val requestNodeUuids = requestMessage.nodeUuids - val exchangePowers = requestNodeUuids.map { uuid => - ExchangePower( - uuid, - Quantities.getQuantity(0, KILOWATT), - Quantities.getQuantity(0, KILOVAR) - ) - } - inferiorGridAgent.send( - askSender, - ProvideGridPowerMessage( - exchangePowers - ) - ) - } - - val slackRequestNodeUuid = firstSlackVoltageRequest match { - case (requestMessage, slackAskSender) => - val slackRequestNodeUuid = requestMessage.nodeUuid - val slackRequestSweepNo = requestMessage.currentSweepNo - superiorGridAgent.send( - slackAskSender, - ProvideSlackVoltageMessage( - slackRequestSweepNo, - slackRequestNodeUuid, - Quantities.getQuantity(380, KILOVOLT), - Quantities.getQuantity(0, KILOVOLT) - ) - ) - slackRequestNodeUuid - } - - // we expect to end up in SimulateGrid but we have to pass HandlePowerFlowCalculations beforehand - // hence we wait until we reached this condition - Try { - Await.result( - Future { - val statesPassed = scala.collection.mutable.Set.empty[AgentState] - while (statesPassed.size < 2) - statesPassed.add(centerGridAgent.stateName) - statesPassed - }(scala.concurrent.ExecutionContext.Implicits.global), - Duration("1 minute") - ) - } match { - case Success(statesPassed) => - statesPassed should contain allOf (HandlePowerFlowCalculations, SimulateGrid) - case Failure(_: TimeoutException) => - fail( - "Actor did not pass the correct amount of foreseen states in reasonable time." - ) - case Failure(ex) => fail("Test failed with unknown error.", ex) - } - - // our test agent should now be ready to provide the grid power values, hence we ask for them and expect a - // corresponding response - superiorGridAgent.send( - centerGridAgent, - RequestGridPowerMessage( - firstSweepNo, - Vector(slackNodeUuid) - ) - ) - - superiorGridAgent.expectMsgPF(Duration(15, TimeUnit.SECONDS)) { - case ProvideGridPowerMessage(exchangedPowers) => - exchangedPowers.size shouldBe 1 - exchangedPowers.headOption match { - case Some(ExchangePower(nodeUuid, p, q)) => - nodeUuid shouldBe slackNodeUuid - p should equalWithTolerance( - Quantities.getQuantity(0.080423711881702500000, MEGAVOLTAMPERE), - floatPrecision - ) - q should equalWithTolerance( - Quantities.getQuantity(-1.45357503915666260000, MEGAVOLTAMPERE), - floatPrecision - ) - case None => - fail("Did not expect to get nothing") - } - case x => - fail( - s"Invalid message received when expecting grid power values message. Message was $x" - ) - } - - // we start a second sweep by asking for next sweep values which should trigger the whole procedure again - val secondSweepNo = firstSweepNo + 1 - superiorGridAgent.send( - centerGridAgent, - RequestGridPowerMessage( - secondSweepNo, - Vector(slackNodeUuid) - ) - ) - - // the agent now should ask for updated slack voltages from the superior grid - val secondSlackVoltageRequest = superiorGridAgent.expectMsgPF() { - case request @ RequestSlackVoltageMessage(sweepNo, nodeId) => - sweepNo shouldBe secondSweepNo - nodeId shouldBe UUID.fromString( - "9fe5fa33-6d3b-4153-a829-a16f4347bc4e" - ) - (request, superiorGridAgent.lastSender) - case x => - fail( - s"Invalid message received when expecting slack voltage request message. Message was $x" - ) - } - - // the agent should then go to HandlePowerFlowCalculations and wait for the response on updated slack value - centerGridAgent.stateName shouldBe HandlePowerFlowCalculations - - // the superior grid would answer with updated slack voltage values - val secondSlackAskSender = secondSlackVoltageRequest._2 - superiorGridAgent.send( - secondSlackAskSender, - ProvideSlackVoltageMessage( - secondSweepNo, - slackRequestNodeUuid, - Quantities.getQuantity(380, KILOVOLT), - Quantities.getQuantity(0, KILOVOLT) + inferiorAndNodes12._2.foreach { nodeUuid => + inferiorAndNodes12._1.send( + centerGridAgent, + RequestSlackVoltageMessage(firstSweepNo, nodeUuid) ) - ) - - // after the intermediate power flow calculation - // we expect 4 requests for grid power values as we have 4 inferior grids - val secondGridPowerRequests = inferiorGridAgent - .receiveWhile() { case msg @ RequestGridPowerMessage(_, _) => - msg -> inferiorGridAgent.lastSender - } - .toMap - - /* We receive one message with all requests for all inferior grid agents. This is, because the requests are - * grouped by the actor, they are sent to. Here, the test itself serves as the dummy for all inferior grid - * agents */ - secondGridPowerRequests.size shouldBe 1 - secondGridPowerRequests.keys.headOption match { - case Some(RequestGridPowerMessage(_, nodeUuids)) => - nodeUuids should contain theSameElementsAs inferiorGridNodeUuids - case None => fail("Did expect to receive something") } - // the agent should then go back to SimulateGrid and wait for the powers of the inferior grid - awaitAssert(centerGridAgent.stateName shouldBe SimulateGrid) - - // 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 4 inferior grid agents - inferiorGridNodeUuids.foreach { nodeUuid => - inferiorGridAgent.send( + inferiorAndNodes13._2.foreach { nodeUuid => + inferiorAndNodes13._1.send( centerGridAgent, RequestSlackVoltageMessage(firstSweepNo, nodeUuid) ) } - // as we are in the second sweep, all provided slack voltages should be unequal + // 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) - val secondProvidedSlackVoltages = inferiorGridAgent.receiveWhile() { - case provideSlackVoltageMessage: ProvideSlackVoltageMessage => - provideSlackVoltageMessage - case x => - fail( - s"Invalid message received when expecting slack voltage provision message. Message was $x" - ) - } - - secondProvidedSlackVoltages.size shouldBe 4 - val secondExpectedResults = List( - ProvideSlackVoltageMessage( - firstSweepNo, - UUID.fromString("d44ba8ed-81db-4a22-a40d-f7c0d0808a75"), - Quantities.getQuantity(110.1277081582144170, KILOVOLT), - Quantities.getQuantity(-0.011124597905979507, KILOVOLT) - ), - ProvideSlackVoltageMessage( - firstSweepNo, - UUID.fromString("e364ef00-e6ca-46b1-ba2b-bb73c0c6fee0"), - Quantities.getQuantity(110.1422124824355620, KILOVOLT), - Quantities.getQuantity(-0.014094294956794604, KILOVOLT) - ), - ProvideSlackVoltageMessage( - firstSweepNo, - UUID.fromString("78c5d473-e01b-44c4-afd2-e4ff3c4a5d7c"), - Quantities.getQuantity(110.1196117051188620, KILOVOLT), - Quantities.getQuantity(-0.009318349620959118, KILOVOLT) - ), - ProvideSlackVoltageMessage( - firstSweepNo, - UUID.fromString("47ef9983-8fcf-4713-be90-093fc27864ae"), - Quantities.getQuantity(110.147346134387320, KILOVOLT), - Quantities.getQuantity(-0.015819259689252657, KILOVOLT) - ) - ) - - secondProvidedSlackVoltages.foreach(providedSlackVoltage => { - val expectedResult = secondExpectedResults - .find(expectedResult => - expectedResult.nodeUuid == providedSlackVoltage.nodeUuid - ) - .getOrElse( - fail( - s"Unable to find expected nodeUuid ${providedSlackVoltage.nodeUuid}" - ) - ) - expectedResult.currentSweepNo shouldBe providedSlackVoltage.currentSweepNo - - if ( - !(expectedResult.e.getValue - .doubleValue() - providedSlackVoltage.e.getValue - .doubleValue() < 1e-12) - ) - fail( - s"Real part of node ${expectedResult.nodeUuid} is ${providedSlackVoltage.e} but the expected result is ${expectedResult.e}" + inferiorAndNodes11._2.foreach { nodeUuid => + inferiorAndNodes11._1.expectMsg( + ProvideSlackVoltageMessage( + firstSweepNo, + nodeUuid, + Quantities.getQuantity(110, KILOVOLT), + Quantities.getQuantity(0, KILOVOLT) ) - - if ( - !(expectedResult.f.getValue - .doubleValue() - providedSlackVoltage.e.getValue - .doubleValue() < 1e-12) ) - fail( - s"Real part of node ${expectedResult.nodeUuid} is ${providedSlackVoltage.f} but the expected result is ${expectedResult.f}" - ) - - }) + } - // we now answer the request of our centerGridAgent - // with 1 fake grid power messages - secondGridPowerRequests.foreach { case (requestMessage, askSender) => - val requestNodeUuid = requestMessage.nodeUuids - inferiorGridAgent.send( - askSender, - ProvideGridPowerMessage( - requestNodeUuid.map { uuid => - ExchangePower( - uuid, - Quantities.getQuantity(0, KILOWATT), - Quantities.getQuantity(0, KILOVAR) - ) - } + inferiorAndNodes12._2.foreach { nodeUuid => + inferiorAndNodes12._1.expectMsg( + ProvideSlackVoltageMessage( + firstSweepNo, + nodeUuid, + Quantities.getQuantity(110, KILOVOLT), + Quantities.getQuantity(0, KILOVOLT) ) ) } - // we expect to end up in SimulateGrid but we have to pass HandlePowerFlowCalculations beforehand - // hence we wait until we reached this condition - Try { - Await.result( - Future { - val statesPassed = scala.collection.mutable.Set.empty[AgentState] - while (statesPassed.size < 2) - statesPassed.add(centerGridAgent.stateName) - statesPassed - }(scala.concurrent.ExecutionContext.Implicits.global), - Duration("1 minute") - ) - } match { - case Success(statesPassed) => - statesPassed should contain allOf (HandlePowerFlowCalculations, SimulateGrid) - case Failure(_: TimeoutException) => - fail( - "Actor did not pass the correct amount of foreseen states in reasonable time." + inferiorAndNodes13._2.foreach { nodeUuid => + inferiorAndNodes13._1.expectMsg( + ProvideSlackVoltageMessage( + firstSweepNo, + nodeUuid, + Quantities.getQuantity(110, KILOVOLT), + Quantities.getQuantity(0, KILOVOLT) ) - case Failure(ex) => fail("Test failed with unknown error.", ex) - } - - // as the akka testkit does the unstash handling incorrectly, we need to send a second request for grid power messages - superiorGridAgent.send( - centerGridAgent, - RequestGridPowerMessage( - secondSweepNo, - Vector(slackNodeUuid) ) - ) - - superiorGridAgent.expectMsgPF() { - case ProvideGridPowerMessage(exchangedPower) => - exchangedPower.size shouldBe 1 - exchangedPower.headOption match { - case Some(ExchangePower(nodeUuid, p, q)) => - nodeUuid shouldBe slackNodeUuid - p should equalWithTolerance( - Quantities.getQuantity(0.080423711881452700000, MEGAVOLTAMPERE), - floatPrecision - ) - q should equalWithTolerance( - Quantities.getQuantity(-1.45357503915621860000, MEGAVOLTAMPERE), - floatPrecision - ) - case None => - fail("I did not expect to get nothing.") - } - case x => - fail( - s"Invalid message received when expecting grid power message. Message was $x" - ) } - } - } - - "A GridAgent actor in center position with async test" should { - - val centerGridAgent = - system.actorOf( - GridAgent.props( - environmentRefs, - simonaConfig, - listener = Iterable.empty[ActorRef] - ) - ) - - s"initialize itself when it receives a $InitializeGridAgentTrigger with corresponding data" in { - val triggerId = 0 - - // this subnet has 1 superior grid (HöS) and 4 inferior grids (MS) - val subGridGateToActorRef = hvSubGridGates.map { - case gate - if gate.getInferiorNode.getSubnet == hvGridContainer.getSubnet => - gate -> superiorGridAgent.ref - case gate => gate -> inferiorGridAgent.ref - }.toMap - - val gridAgentInitData = - GridAgentInitData( - hvGridContainer, - subGridGateToActorRef, - RefSystem("2000 MVA", "110 kV") - ) - - // send init data to agent and expect a CompletionMessage - implicit val timeout: Timeout = Timeout(1, TimeUnit.SECONDS) - val expectedCompletionMessage = - Await.result( - centerGridAgent ? TriggerWithIdMessage( - InitializeGridAgentTrigger(gridAgentInitData), - triggerId, - centerGridAgent - ), - timeout.duration - ) + // we now answer the request of our centerGridAgent + // with 3 fake grid power messages and 1 fake slack voltage message - expectedCompletionMessage shouldBe CompletionMessage( - 0, - Some( - Vector( - ScheduleTriggerMessage( - ActivityStartTrigger(3600), - centerGridAgent + inferiorAndNodes11._1.send( + firstPowerRequestSender11, + ProvideGridPowerMessage( + inferiorAndNodes11._2.map(nodeUuid => + ExchangePower( + nodeUuid, + Quantities.getQuantity(0, KILOWATT), + Quantities.getQuantity(0, KILOVAR) ) ) ) ) - } - - s"go to $SimulateGrid when it receives an activity start trigger" in { - - val activityStartTriggerId = 1 - - scheduler.send( - centerGridAgent, - TriggerWithIdMessage( - ActivityStartTrigger(3600), - activityStartTriggerId, - centerGridAgent - ) - ) - - scheduler.expectMsgPF() { - case CompletionMessage( - triggerId, - Some(Vector(ScheduleTriggerMessage(triggerToBeScheduled, _))) - ) => - triggerId shouldBe 1 - triggerToBeScheduled shouldBe StartGridSimulationTrigger(3600) - case x => - fail( - s"Invalid message received when expecting a completion message after activity start trigger. Message was $x" + inferiorAndNodes12._1.send( + firstPowerRequestSender12, + ProvideGridPowerMessage( + inferiorAndNodes12._2.map(nodeUuid => + ExchangePower( + nodeUuid, + Quantities.getQuantity(0, KILOWATT), + Quantities.getQuantity(0, KILOVAR) + ) ) - } - } - - s"start the simulation when a $StartGridSimulationTrigger is send" in { - - val startGridSimulationTriggerId = 2 - val firstSweepNo = 0 - val slackNodeUuid = - UUID.fromString("9fe5fa33-6d3b-4153-a829-a16f4347bc4e") - val inferiorGridNodeUuids = Vector( - UUID.fromString("d44ba8ed-81db-4a22-a40d-f7c0d0808a75"), - UUID.fromString("e364ef00-e6ca-46b1-ba2b-bb73c0c6fee0"), - UUID.fromString("78c5d473-e01b-44c4-afd2-e4ff3c4a5d7c"), - UUID.fromString("47ef9983-8fcf-4713-be90-093fc27864ae") - ) - - // send the start grid simulation trigger - scheduler.send( - centerGridAgent, - TriggerWithIdMessage( - StartGridSimulationTrigger(3600), - startGridSimulationTriggerId, - centerGridAgent ) ) - /* We expect one grid power request message, as all four sub grids are mapped onto one actor reference */ - val firstGridPowerRequests = inferiorGridAgent - .receiveWhile() { case msg: RequestGridPowerMessage => - msg -> inferiorGridAgent.lastSender - } - .toMap - - /* We receive one message with all requests for all inferior grid agents. This is, because the requests are - * grouped by the actor, they are sent to. Here, the test itself serves as the dummy for all inferior grid - * agents */ - firstGridPowerRequests.size shouldBe 1 - firstGridPowerRequests.keys.headOption match { - case Some(RequestGridPowerMessage(_, nodeUuids)) => - nodeUuids should contain theSameElementsAs inferiorGridNodeUuids - case None => fail("Did expect to receive something") - } - - // we expect 1 request for slack voltage values - // (slack values are requested by our agent under test from the superior grid) - val firstSlackVoltageRequest = superiorGridAgent.expectMsgPF() { - case request @ RequestSlackVoltageMessage(sweepNo, nodeId) => - sweepNo shouldBe firstSweepNo - nodeId shouldBe UUID.fromString( - "9fe5fa33-6d3b-4153-a829-a16f4347bc4e" - ) - (request, superiorGridAgent.lastSender) - case x => - fail( - s"Invalid message received when expecting slack voltage request message. Message was $x" - ) - } - - // 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 4 inferior grid agents - inferiorGridNodeUuids.foreach { nodeUuid => - inferiorGridAgent.send( - centerGridAgent, - RequestSlackVoltageMessage(firstSweepNo, nodeUuid) - ) - } - - // 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) - val providedSlackVoltages = inferiorGridAgent.receiveWhile() { - case provideSlackVoltageMessage: ProvideSlackVoltageMessage => - provideSlackVoltageMessage - case x => - fail( - s"Invalid message received when expecting slack voltage provision message. Message was $x" - ) - } - - providedSlackVoltages.size shouldBe 4 - providedSlackVoltages should contain allOf ( - ProvideSlackVoltageMessage( - firstSweepNo, - UUID.fromString("d44ba8ed-81db-4a22-a40d-f7c0d0808a75"), - Quantities.getQuantity(110, KILOVOLT), - Quantities.getQuantity(0, KILOVOLT) - ), - ProvideSlackVoltageMessage( - firstSweepNo, - UUID.fromString("e364ef00-e6ca-46b1-ba2b-bb73c0c6fee0"), - Quantities.getQuantity(110, KILOVOLT), - Quantities.getQuantity(0, KILOVOLT) - ), - ProvideSlackVoltageMessage( - firstSweepNo, - UUID.fromString("78c5d473-e01b-44c4-afd2-e4ff3c4a5d7c"), - Quantities.getQuantity(110, KILOVOLT), - Quantities.getQuantity(0, KILOVOLT) - ), - ProvideSlackVoltageMessage( - firstSweepNo, - UUID.fromString("47ef9983-8fcf-4713-be90-093fc27864ae"), - Quantities.getQuantity(110, KILOVOLT), - Quantities.getQuantity(0, KILOVOLT) - ) - ) - // we now answer the request of our centerGridAgent - // with 4 fake grid power messages and 1 fake slack voltage message - firstGridPowerRequests.foreach { case (requestMessage, askSender) => - val requestNodeUuid = requestMessage.nodeUuids - askSender ! ProvideGridPowerMessage( - requestNodeUuid.map { uuid => + inferiorAndNodes13._1.send( + firstPowerRequestSender13, + ProvideGridPowerMessage( + inferiorAndNodes13._2.map(nodeUuid => ExchangePower( - uuid, + nodeUuid, Quantities.getQuantity(0, KILOWATT), Quantities.getQuantity(0, KILOVAR) ) - } + ) ) - } + ) val slackRequestNodeUuid = firstSlackVoltageRequest match { case (voltageRequest, slackAskSender) => val slackRequestNodeUuid = voltageRequest.nodeUuid val slackRequestSweepNo = voltageRequest.currentSweepNo - slackAskSender ! ProvideSlackVoltageMessage( - slackRequestSweepNo, - slackRequestNodeUuid, - Quantities.getQuantity(380, KILOVOLT), - Quantities.getQuantity(0, KILOVOLT) + superiorGridAgent.send( + slackAskSender, + ProvideSlackVoltageMessage( + slackRequestSweepNo, + slackRequestNodeUuid, + Quantities.getQuantity(380, KILOVOLT), + Quantities.getQuantity(0, KILOVOLT) + ) ) slackRequestNodeUuid } // our test agent should now be ready to provide the grid power values, hence we ask for them and expect a // corresponding response + superiorGridAgent.send( centerGridAgent, RequestGridPowerMessage( @@ -861,9 +401,7 @@ class DBFSAlgorithmCenGridSpec val secondSlackVoltageRequest = superiorGridAgent.expectMsgPF() { case request @ RequestSlackVoltageMessage(sweepNo, nodeId) => sweepNo shouldBe secondSweepNo - nodeId shouldBe UUID.fromString( - "9fe5fa33-6d3b-4153-a829-a16f4347bc4e" - ) + nodeId shouldBe slackNodeUuid (request, superiorGridAgent.lastSender) case x => fail( @@ -885,29 +423,40 @@ class DBFSAlgorithmCenGridSpec // after the intermediate power flow calculation // We expect one grid power request message, as all four sub grids are mapped onto one actor reference - val secondGridPowerRequests = inferiorGridAgent - .receiveWhile() { case msg: RequestGridPowerMessage => - msg -> inferiorGridAgent.lastSender - } - .toMap - - /* We receive one message with all requests for all inferior grid agents. This is, because the requests are - * grouped by the actor, they are sent to. Here, the test itself serves as the dummy for all inferior grid - * agents */ - secondGridPowerRequests.size shouldBe 1 - secondGridPowerRequests.keys.headOption match { - case Some(RequestGridPowerMessage(_, nodeUuids)) => - nodeUuids should contain theSameElementsAs inferiorGridNodeUuids - case None => fail("Did expect to receive something") - } + inferiorAndNodes11._1 + .expectMsgType[RequestGridPowerMessage] + .nodeUuids shouldBe inferiorAndNodes11._2 + val secondPowerRequestSender11 = inferiorAndNodes11._1.lastSender - // the agent should then go back to SimulateGrid and wait for the powers of the inferior grid - // awaitAssert(centerGridAgent.stateName shouldBe SimulateGrid) + inferiorAndNodes12._1 + .expectMsgType[RequestGridPowerMessage] + .nodeUuids shouldBe inferiorAndNodes12._2 + val secondPowerRequestSender12 = inferiorAndNodes12._1.lastSender + + inferiorAndNodes13._1 + .expectMsgType[RequestGridPowerMessage] + .nodeUuids should contain allElementsOf inferiorAndNodes13._2 + val secondPowerRequestSender13 = inferiorAndNodes13._1.lastSender // 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 4 inferior grid agents - inferiorGridNodeUuids.foreach { nodeUuid => - inferiorGridAgent.send( + + inferiorAndNodes11._2.foreach { nodeUuid => + inferiorAndNodes11._1.send( + centerGridAgent, + RequestSlackVoltageMessage(firstSweepNo, nodeUuid) + ) + } + + inferiorAndNodes12._2.foreach { nodeUuid => + inferiorAndNodes12._1.send( + centerGridAgent, + RequestSlackVoltageMessage(firstSweepNo, nodeUuid) + ) + } + + inferiorAndNodes13._2.foreach { nodeUuid => + inferiorAndNodes13._1.send( centerGridAgent, RequestSlackVoltageMessage(firstSweepNo, nodeUuid) ) @@ -916,90 +465,98 @@ class DBFSAlgorithmCenGridSpec // 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) - val secondProvidedSlackVoltages = inferiorGridAgent.receiveWhile() { - case provideSlackVoltageMessage: ProvideSlackVoltageMessage => - provideSlackVoltageMessage - case x => - fail( - s"Invalid message received when expecting slack voltage provision message. Message was $x" - ) + + inferiorAndNodes11._2.foreach { expectedNodeUuid => + val received = + inferiorAndNodes11._1.expectMsgType[ProvideSlackVoltageMessage] + + received.currentSweepNo shouldBe firstSweepNo + received.nodeUuid shouldBe expectedNodeUuid + received.e should equalWithTolerance( + Quantities.getQuantity(110.1196117051188620, KILOVOLT) + ) + received.f should equalWithTolerance( + Quantities.getQuantity(-0.009318349620959118, KILOVOLT) + ) } - secondProvidedSlackVoltages.size shouldBe 4 - val secondExpectedResults = List( - ProvideSlackVoltageMessage( - firstSweepNo, - UUID.fromString("d44ba8ed-81db-4a22-a40d-f7c0d0808a75"), - Quantities.getQuantity(110.1277081582144170, KILOVOLT), - Quantities.getQuantity(-0.011124597905979507, KILOVOLT) - ), - ProvideSlackVoltageMessage( - firstSweepNo, - UUID.fromString("e364ef00-e6ca-46b1-ba2b-bb73c0c6fee0"), - Quantities.getQuantity(110.1422124824355620, KILOVOLT), + inferiorAndNodes12._2.foreach { expectedNodeUuid => + val received = + inferiorAndNodes12._1.expectMsgType[ProvideSlackVoltageMessage] + + received.currentSweepNo shouldBe firstSweepNo + received.nodeUuid shouldBe expectedNodeUuid + received.e should equalWithTolerance( + Quantities.getQuantity(110.1422124824355620, KILOVOLT) + ) + received.f should equalWithTolerance( Quantities.getQuantity(-0.014094294956794604, KILOVOLT) - ), - ProvideSlackVoltageMessage( - firstSweepNo, - UUID.fromString("78c5d473-e01b-44c4-afd2-e4ff3c4a5d7c"), - Quantities.getQuantity(110.1196117051188620, KILOVOLT), - Quantities.getQuantity(-0.009318349620959118, KILOVOLT) - ), - ProvideSlackVoltageMessage( - firstSweepNo, - UUID.fromString("47ef9983-8fcf-4713-be90-093fc27864ae"), - Quantities.getQuantity(110.147346134387320, KILOVOLT), - Quantities.getQuantity(-0.015819259689252657, KILOVOLT) ) - ) + } - secondProvidedSlackVoltages.foreach(providedSlackVoltage => { - val expectedResult = secondExpectedResults - .find(expectedResult => - expectedResult.nodeUuid == providedSlackVoltage.nodeUuid + inferiorAndNodes13._1.expectMsgType[ProvideSlackVoltageMessage] match { + case received if received.nodeUuid.equals(node3a.getUuid) => + received.currentSweepNo shouldBe firstSweepNo + received.nodeUuid shouldBe node3a.getUuid + received.e should equalWithTolerance( + Quantities.getQuantity(110.147346134387320, KILOVOLT) ) - .getOrElse( - fail( - s"Unable to find expected nodeUuid ${providedSlackVoltage.nodeUuid}" - ) + received.f should equalWithTolerance( + Quantities.getQuantity(-0.015819259689252657, KILOVOLT) ) - expectedResult.currentSweepNo shouldBe providedSlackVoltage.currentSweepNo - if ( - !(expectedResult.e.getValue - .doubleValue() - providedSlackVoltage.e.getValue - .doubleValue() < 1e-12) - ) - fail( - s"Real part of node ${expectedResult.nodeUuid} is ${providedSlackVoltage.e} but the expected result is ${expectedResult.e}" + case received if received.nodeUuid.equals(node3b.getUuid) => + received.currentSweepNo shouldBe firstSweepNo + received.nodeUuid shouldBe node3b.getUuid + received.e should equalWithTolerance( + Quantities.getQuantity(110.1277081582144170, KILOVOLT) ) - - if ( - !(expectedResult.f.getValue - .doubleValue() - providedSlackVoltage.e.getValue - .doubleValue() < 1e-12) - ) - fail( - s"Real part of node ${expectedResult.nodeUuid} is ${providedSlackVoltage.f} but the expected result is ${expectedResult.f}" + received.f should equalWithTolerance( + Quantities.getQuantity(-0.011124597905979507, KILOVOLT) ) - }) + case received => + fail(s"Msg with unknown node UUID ${received.nodeUuid} was received") + } // we now answer the request of our centerGridAgent // with 1 fake grid power message - secondGridPowerRequests.foreach { case (request, askSender) => - val requestNodeUuid = request.nodeUuids - inferiorGridAgent.send( - askSender, - ProvideGridPowerMessage( - requestNodeUuid.map { uuid => - ExchangePower( - uuid, - Quantities.getQuantity(0, KILOWATT), - Quantities.getQuantity(0, KILOVAR) - ) - } + inferiorAndNodes11._1.send( + secondPowerRequestSender11, + ProvideGridPowerMessage( + inferiorAndNodes11._2.map(nodeUuid => + ExchangePower( + nodeUuid, + Quantities.getQuantity(0, KILOWATT), + Quantities.getQuantity(0, KILOVAR) + ) ) ) - } + ) + + inferiorAndNodes12._1.send( + secondPowerRequestSender12, + ProvideGridPowerMessage( + inferiorAndNodes12._2.map(nodeUuid => + ExchangePower( + nodeUuid, + Quantities.getQuantity(0, KILOWATT), + Quantities.getQuantity(0, KILOVAR) + ) + ) + ) + ) + + inferiorAndNodes13._1.send( + secondPowerRequestSender13, + ProvideGridPowerMessage( + inferiorAndNodes13._2.map(nodeUuid => + ExchangePower( + nodeUuid, + Quantities.getQuantity(0, KILOWATT), + Quantities.getQuantity(0, KILOVAR) + ) + ) + ) + ) // we expect that the GridAgent unstashes the messages and return a value for our power request superiorGridAgent.expectMsgPF() { 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 38cf61fc9d..f2047d428a 100644 --- a/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmSupGridSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmSupGridSpec.scala @@ -6,14 +6,12 @@ package edu.ie3.simona.agent.grid -import java.util.UUID import akka.actor.{ActorRef, ActorSystem} -import akka.testkit.{ImplicitSender, TestFSMRef} +import akka.testkit.ImplicitSender import com.typesafe.config.ConfigFactory import edu.ie3.datamodel.graph.SubGridGate import edu.ie3.simona.agent.EnvironmentRefs import edu.ie3.simona.agent.grid.GridAgentData.GridAgentInitData -import edu.ie3.simona.agent.state.AgentState.{Idle, Uninitialized} import edu.ie3.simona.agent.state.GridAgentState.SimulateGrid import edu.ie3.simona.model.grid.RefSystem import edu.ie3.simona.ontology.messages.PowerMessage.ProvideGridPowerMessage.ExchangePower @@ -41,6 +39,8 @@ import edu.ie3.simona.test.common.{ import edu.ie3.util.quantities.PowerSystemUnits._ import tech.units.indriya.quantity.Quantities +import java.util.UUID + /** Test to ensure the functions that a [[GridAgent]] in superior position * should be able to do if the DBFSAlgorithm is used. The scheduler, the * weather service as well as the [[GridAgent]] inferior to the superior @@ -69,327 +69,6 @@ class DBFSAlgorithmSupGridSpec evDataService = None ) - "A GridAgent actor in superior position with FSM test" should { - - val superiorGridAgentFSM = TestFSMRef( - new GridAgent( - environmentRefs, - simonaConfig, - listener = Iterable.empty[ActorRef] - ) - ) - - s"be in state $Uninitialized after startup" in { - superiorGridAgentFSM.stateName shouldBe Uninitialized - } - - s"initialize itself when it receives a $InitializeGridAgentTrigger with corresponding data" in { - val triggerId = 0 - - // hs test actor (below ehv/sup actor) - val hsActorRef = this.testActor - - val subGridGateToActorRef: Map[SubGridGate, ActorRef] = - ehvSubGridGates.map(gate => gate -> hsActorRef).toMap - - val gridAgentInitData = - GridAgentInitData( - ehvGridContainer, - subGridGateToActorRef, - RefSystem("5000 MVA", "380 kV") - ) - - // send init data to agent - superiorGridAgentFSM ! TriggerWithIdMessage( - InitializeGridAgentTrigger(gridAgentInitData), - triggerId, - superiorGridAgentFSM - ) - - // grid agent state should be idle afterwards - superiorGridAgentFSM.stateName shouldBe Idle - - expectMsgPF() { - case CompletionMessage( - triggerId, - Some(Vector(ScheduleTriggerMessage(triggerToBeScheduled, _))) - ) => - triggerId shouldBe 0 - triggerToBeScheduled shouldBe ActivityStartTrigger(3600) - case x => - fail( - s"Invalid message received when expecting a completion message after an init trigger. Message was $x" - ) - } - - } - - s"go to $SimulateGrid when it receives an activity start trigger" in { - - val activityStartTriggerId = 1 - - superiorGridAgentFSM ! TriggerWithIdMessage( - ActivityStartTrigger(3600), - activityStartTriggerId, - superiorGridAgentFSM - ) - - // grid agent stat should be simulate grid afterwards - superiorGridAgentFSM.stateName shouldBe SimulateGrid - - // we expect a completion message - expectMsgPF() { - case CompletionMessage( - triggerId, - Some(Vector(ScheduleTriggerMessage(triggerToBeScheduled, _))) - ) => - triggerId shouldBe 1 - triggerToBeScheduled shouldBe StartGridSimulationTrigger(3600) - case x => - fail( - s"Invalid message received when expecting a completion message after a start activity trigger. Message was $x" - ) - } - - } - - s"start the simulation, do 2 sweeps and should end afterwards when no deviation on nodal " + - s"power is recognized in the superior grid when a $StartGridSimulationTrigger is send" in { - - for (sweepNo <- 0 to 1) { - - val startGridSimulationTriggerId = sweepNo + 2 - val requestedConnectionNodeUuids = - Vector(UUID.fromString("9fe5fa33-6d3b-4153-a829-a16f4347bc4e")) - - // send the start grid simulation trigger - superiorGridAgentFSM ! TriggerWithIdMessage( - StartGridSimulationTrigger(3600), - startGridSimulationTriggerId, - superiorGridAgentFSM - ) - - // we expect a request for grid power values here for sweepNo $sweepNo - expectMsgPF() { - case requestGridPowerMessage: RequestGridPowerMessage => - requestGridPowerMessage.currentSweepNo shouldBe sweepNo - requestGridPowerMessage.nodeUuids should contain allElementsOf requestedConnectionNodeUuids - case x => - fail( - s"Invalid message received when expecting a request for grid power values! Message was $x" - ) - - } - - // we return with a fake grid power message - // / as we are using the ask pattern, we cannot send it to the grid agent directly but have to send it to the - // / ask sender - val askSender = lastSender - askSender ! ProvideGridPowerMessage( - requestedConnectionNodeUuids.map { uuid => - ExchangePower( - uuid, - Quantities.getQuantity(0, KILOWATT), - Quantities.getQuantity(0, KILOVAR) - ) - } - ) - - // we expect a completion message here (sweepNo == 0) and that the agent goes back to simulate grid - // and waits until the newly scheduled StartGridSimulationTrigger is send - expectMsgPF() { - /* should happen for sweepNo == 0, receiver is scheduler */ - case CompletionMessage( - 2, - Some( - Vector( - ScheduleTriggerMessage( - StartGridSimulationTrigger(3600), - _ - ) - ) - ) - ) => - // might take some time until we are in SimulateGrid again - awaitAssert(superiorGridAgentFSM.stateName shouldBe SimulateGrid) - case FinishGridSimulationTrigger(3600) => - /* should happen for sweepNo == 1, receiver is hsActor */ - // if this message is send out by the sup agent, it is in state SimulateGrid - - // depending on the host this test is running, the sup agent might already have changed state to Idle again - // (follows after sending this FinishGridSimulationTrigger message to the hsActor) - // IMHO it's not safe to check for this state here, because if the receive process is only a few nanoseconds - // to slow, the test will fail - // furthermore, as it is expected to be send a completion message after the FinishGridSimulationTrigger - // in the state SimulateGrid too, it should be enough to check if we receive this completion message, because then - // the agent implicitly has to have been in state SimulateGrid. (see DBFSAlgorithm:317-362 for understanding) - // I comment this out for now, but leave the "dead code" with the explanation here as well (for now). - // If this solves the issue, we can remove this in the long term. - // awaitAssert(superiorGridAgentFSM.stateName shouldBe SimulateGrid, interval = 1.nanos) - /* we expect another completion message then to be send to the scheduler, receiver is scheduler then */ - expectMsgPF() { - case CompletionMessage( - 3, - Some( - Vector( - ScheduleTriggerMessage(ActivityStartTrigger(7200), _) - ) - ) - ) => - // after doing cleanup stuff, our agent should go back to idle again - awaitAssert(superiorGridAgentFSM.stateName shouldBe Idle) - case x => - fail( - s"Invalid message received when expecting a completion message for simulate grid after cleanup! Message was $x" - ) - } - case x => - fail( - s"Invalid message received when expecting a completion message for simulate grid! Message was $x" - ) - } - } - - } - - s"start the simulation, do 5 sweeps and should end afterwards when a deviation on nodal power is " + - s"recognized after the first two sweeps in the superior when a $StartGridSimulationTrigger is send" in { - - // configuration of the test - val maxNumberOfTestSweeps = 4 - // / array that holds the deviations that should be recognized - // // size must be maxNumberOfTestSweeps + 1 and the last two elements MUST be equal, while all other has to be - // // bigger in difference of p OR q than the epsilon provided in simonaConfig (see above @ head of the test) - val deviations = - Array( - ( - Quantities.getQuantity(0, KILOWATT), - Quantities.getQuantity(0, KILOVAR) - ), - ( - Quantities.getQuantity(100, KILOWATT), - Quantities.getQuantity(100, KILOVAR) - ), - ( - Quantities.getQuantity(0, KILOWATT), - Quantities.getQuantity(100, KILOVAR) - ), - ( - Quantities.getQuantity(0, KILOWATT), - Quantities.getQuantity(0, KILOVAR) - ), - ( - Quantities.getQuantity(0, KILOWATT), - Quantities.getQuantity(0, KILOVAR) - ) - ) - - // bring agent in simulate grid state - val activityStartTriggerId = 1 - - superiorGridAgentFSM ! TriggerWithIdMessage( - ActivityStartTrigger(3600), - activityStartTriggerId, - superiorGridAgentFSM - ) - - // we expect a completion message - expectMsgPF() { - case CompletionMessage( - triggerId, - Some(Vector(ScheduleTriggerMessage(triggerToBeScheduled, _))) - ) => - triggerId shouldBe 1 - triggerToBeScheduled shouldBe StartGridSimulationTrigger(3600) - case x => - fail( - s"Invalid message received when expecting a completion message after a start activity trigger. Message was $x" - ) - } - - // grid agent state should be simulate grid afterwards - superiorGridAgentFSM.stateName shouldBe SimulateGrid - - // go on with testing the sweep behaviour - for (sweepNo <- 0 to maxNumberOfTestSweeps) { - - val startGridSimulationTriggerId = sweepNo + 4 - val requestedConnectionNodeUuids = - Vector(UUID.fromString("9fe5fa33-6d3b-4153-a829-a16f4347bc4e")) - - // send the start grid simulation trigger - superiorGridAgentFSM ! TriggerWithIdMessage( - StartGridSimulationTrigger(3600), - startGridSimulationTriggerId, - superiorGridAgentFSM - ) - - // we expect a request for grid power values here for sweepNo $sweepNo - expectMsgPF() { - case requestGridPowerMessage: RequestGridPowerMessage => - requestGridPowerMessage.currentSweepNo shouldBe sweepNo - requestGridPowerMessage.nodeUuids should contain allElementsOf requestedConnectionNodeUuids - case x => - fail( - s"Invalid message received when expecting a request for grid power values! Message was $x" - ) - } - - // we return with a fake grid power message - // / as we are using the ask pattern, we cannot send it to the grid agent directly but have to send it to the - // / ask sender - val askSender = lastSender - askSender ! ProvideGridPowerMessage( - requestedConnectionNodeUuids.map { uuid => - ExchangePower( - uuid, - deviations(sweepNo)._1, - deviations(sweepNo)._2 - ) - } - ) - - // we expect a completion message here and that the agent goes back to simulate grid - // and waits until the newly scheduled StartGridSimulationTrigger is send - expectMsgPF() { - case CompletionMessage( - _, - Some( - Vector( - ScheduleTriggerMessage( - StartGridSimulationTrigger(3600), - _ - ) - ) - ) - ) => - case FinishGridSimulationTrigger(3600) => - // when we received a FinishGridSimulationTrigger (as inferior grid agent) - // we expect another completion message then as well (scheduler view) - expectMsgPF() { - case CompletionMessage( - _, - Some( - Vector( - ScheduleTriggerMessage(ActivityStartTrigger(7200), _) - ) - ) - ) => - // after doing cleanup stuff, our agent should go back to idle again - awaitAssert(superiorGridAgentFSM.stateName shouldBe Idle) - case x => - fail( - s"Invalid message received when expecting a completion message for simulate grid after cleanup! Message was $x" - ) - } - case x => - fail( - s"Invalid message received when expecting a completion message for simulate grid! Message was $x" - ) - } - } - } - } - "A GridAgent actor in superior position with async test" should { val superiorGridAgentFSM = system.actorOf( 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 15d1bca092..ea42b810b9 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 @@ -46,7 +46,7 @@ import scala.jdk.CollectionConverters._ */ trait DbfsTestGrid extends SubGridGateMokka { // 4 HS nodes, 1 slack HöS node - val node1 = new NodeInput( + private val node1 = new NodeInput( UUID.fromString("78c5d473-e01b-44c4-afd2-e4ff3c4a5d7c"), "HS_NET1_Station_1", OperatorInput.NO_OPERATOR_ASSIGNED, @@ -57,7 +57,7 @@ trait DbfsTestGrid extends SubGridGateMokka { GermanVoltageLevelUtils.HV, 1 ) - val node2 = new NodeInput( + private val node2 = new NodeInput( UUID.fromString("e364ef00-e6ca-46b1-ba2b-bb73c0c6fee0"), "HS_NET1_Station_2", OperatorInput.NO_OPERATOR_ASSIGNED, @@ -68,7 +68,7 @@ trait DbfsTestGrid extends SubGridGateMokka { GermanVoltageLevelUtils.HV, 1 ) - val node3 = new NodeInput( + protected val node3a = new NodeInput( UUID.fromString("47ef9983-8fcf-4713-be90-093fc27864ae"), "HS_NET1_Station_3", OperatorInput.NO_OPERATOR_ASSIGNED, @@ -79,7 +79,7 @@ trait DbfsTestGrid extends SubGridGateMokka { GermanVoltageLevelUtils.HV, 1 ) - val node4 = new NodeInput( + protected val node3b = new NodeInput( UUID.fromString("d44ba8ed-81db-4a22-a40d-f7c0d0808a75"), "HS_NET1_Station_4", OperatorInput.NO_OPERATOR_ASSIGNED, @@ -90,7 +90,7 @@ trait DbfsTestGrid extends SubGridGateMokka { GermanVoltageLevelUtils.HV, 1 ) - val slackNode = new NodeInput( + private val slackNode = new NodeInput( UUID.fromString("9fe5fa33-6d3b-4153-a829-a16f4347bc4e"), "HS_NET1_Station_1_380", OperatorInput.NO_OPERATOR_ASSIGNED, @@ -104,7 +104,7 @@ trait DbfsTestGrid extends SubGridGateMokka { /* Mocking table of nodes of underlying grids * - * MS4_01 @ 14 -> 1129b00d-3d89-4a4a-8ae1-2a56041b95aa @ 14 + * MS3_01 @ 13 -> 1129b00d-3d89-4a4a-8ae1-2a56041b95aa @ 13 * MS2_01 @ 12 -> 139c435d-e550-48d8-b590-ee897621f42a @ 12 * MS1_01 @ 11 -> 1676e48c-5353-4f06-b671-c579cf6a7072 @ 11 * MS3_01 @ 13 -> 9237e237-01e9-446f-899f-c3b5cf69d288 @ 13 @@ -155,35 +155,35 @@ trait DbfsTestGrid extends SubGridGateMokka { Quantities.getQuantity(110.0, KILOVOLT) ) - val line1 = new LineInput( + private val line1 = new LineInput( UUID.fromString("b6dff9c3-cebb-4aea-9f12-0556bdbf35dc"), "LTG_HS_NET1_Station_3-HS_NET1_Station_4", OperatorInput.NO_OPERATOR_ASSIGNED, OperationTime.notLimited(), - node3, - node4, + node3a, + node3b, 1, lineType1, Quantities.getQuantity(20, KILOMETRE), - GridAndGeoUtils.buildSafeLineStringBetweenNodes(node3, node4), + GridAndGeoUtils.buildSafeLineStringBetweenNodes(node3a, node3b), OlmCharacteristicInput.CONSTANT_CHARACTERISTIC ) - val line2 = new LineInput( + private val line2 = new LineInput( UUID.fromString("c15ec4ad-3ff3-43f0-bc72-ee9a76f53afd"), "LTG_HS_NET1_Station_3-HS_NET1_Station_2", OperatorInput.NO_OPERATOR_ASSIGNED, OperationTime.notLimited(), - node3, + node3a, node2, 1, lineType2, Quantities.getQuantity(20.0, KILOMETRE), - GridAndGeoUtils.buildSafeLineStringBetweenNodes(node3, node2), + GridAndGeoUtils.buildSafeLineStringBetweenNodes(node3a, node2), OlmCharacteristicInput.CONSTANT_CHARACTERISTIC ) - val line3 = new LineInput( + private val line3 = new LineInput( UUID.fromString("8440825c-24c9-4b3d-9e94-a6bfb9643a6b"), "LTG_HS_NET1_Station_1-HS_NET1_Station_2", OperatorInput.NO_OPERATOR_ASSIGNED, @@ -197,31 +197,31 @@ trait DbfsTestGrid extends SubGridGateMokka { OlmCharacteristicInput.CONSTANT_CHARACTERISTIC ) - val line4 = new LineInput( + private val line4 = new LineInput( UUID.fromString("e0ca3891-1757-4dea-ac9d-8f1194da453e"), "LTG_HS_NET1_Station_1-HS_NET1_Station_3", OperatorInput.NO_OPERATOR_ASSIGNED, OperationTime.notLimited(), node1, - node3, + node3a, 1, lineType4, Quantities.getQuantity(40, KILOMETRE), - GridAndGeoUtils.buildSafeLineStringBetweenNodes(node1, node3), + GridAndGeoUtils.buildSafeLineStringBetweenNodes(node1, node3a), OlmCharacteristicInput.CONSTANT_CHARACTERISTIC ) - val line5 = new LineInput( + private val line5 = new LineInput( UUID.fromString("147ae685-4fc7-406c-aca6-afb2bc6e19fc"), "LTG_HS_NET1_Station_4-HS_NET1_Station_1", OperatorInput.NO_OPERATOR_ASSIGNED, OperationTime.notLimited(), - node4, + node3b, node1, 1, lineType2, Quantities.getQuantity(30, KILOMETRE), - GridAndGeoUtils.buildSafeLineStringBetweenNodes(node4, node1), + GridAndGeoUtils.buildSafeLineStringBetweenNodes(node3b, node1), OlmCharacteristicInput.CONSTANT_CHARACTERISTIC ) @@ -244,7 +244,7 @@ trait DbfsTestGrid extends SubGridGateMokka { 5 ) - val transformer1 = new Transformer2WInput( + private val transformer1 = new Transformer2WInput( UUID.fromString("6e9d912b-b652-471b-84d2-6ed571e53a7b"), "HöS-Trafo_S1", OperatorInput.NO_OPERATOR_ASSIGNED, @@ -257,8 +257,8 @@ trait DbfsTestGrid extends SubGridGateMokka { false ) - val (hvGridContainer, hvSubGridGates) = { - val nodes = Set(node1, node2, node3, node4, slackNode) + protected val (hvGridContainer, hvSubGridGates) = { + val nodes = Set(node1, node2, node3a, node3b, slackNode) val lines = Set(line1, line2, line3, line4, line5) val transformers = Set(transformer1) val rawGridElements = new RawGridElements( @@ -297,25 +297,25 @@ trait DbfsTestGrid extends SubGridGateMokka { ) ) ++ Vector( build2wSubGridGate( - UUID.fromString("d44ba8ed-81db-4a22-a40d-f7c0d0808a75"), + node3b.getUuid, 1, UUID.fromString("1129b00d-3d89-4a4a-8ae1-2a56041b95aa"), - 14 + 13 ), build2wSubGridGate( - UUID.fromString("e364ef00-e6ca-46b1-ba2b-bb73c0c6fee0"), + node2.getUuid, 1, UUID.fromString("139c435d-e550-48d8-b590-ee897621f42a"), 12 ), build2wSubGridGate( - UUID.fromString("78c5d473-e01b-44c4-afd2-e4ff3c4a5d7c"), + node1.getUuid, 1, UUID.fromString("1676e48c-5353-4f06-b671-c579cf6a7072"), 11 ), build2wSubGridGate( - UUID.fromString("47ef9983-8fcf-4713-be90-093fc27864ae"), + node3a.getUuid, 1, UUID.fromString("9237e237-01e9-446f-899f-c3b5cf69d288"), 13 @@ -334,7 +334,7 @@ trait DbfsTestGrid extends SubGridGateMokka { ) } - val (ehvGridContainer, ehvSubGridGates) = { + protected val (ehvGridContainer, ehvSubGridGates) = { val nodes = Set(slackNode) val lines = Set.empty[LineInput] val transformers = Set.empty[Transformer2WInput] From 14dd3b8b77481c8e58a89f61ef3de119710f9667 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Thu, 30 Jun 2022 11:49:36 +0200 Subject: [PATCH 16/36] Simplifying DbfsTestGrid a tiny bit --- .../test/common/model/grid/DbfsTestGrid.scala | 28 +++++++------------ 1 file changed, 10 insertions(+), 18 deletions(-) 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 ea42b810b9..68c38c908c 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 @@ -287,15 +287,15 @@ trait DbfsTestGrid extends SubGridGateMokka { ) /* Sub grid gates are the apparent gates to superior grids + artificial one to underlying grids */ - val subGridGates: Vector[SubGridGate] = - (rawGridElements.getTransformer2Ws.asScala.map( + val subGridGates: Seq[SubGridGate] = + rawGridElements.getTransformer2Ws.asScala.toSeq.map( SubGridGate.fromTransformer2W ) ++ rawGridElements.getTransformer3Ws.asScala.flatMap(transformer => - Vector( + Seq( SubGridGate.fromTransformer3W(transformer, ConnectorPort.B), SubGridGate.fromTransformer3W(transformer, ConnectorPort.C) ) - ) ++ Vector( + ) ++ Seq( build2wSubGridGate( node3b.getUuid, 1, @@ -320,7 +320,7 @@ trait DbfsTestGrid extends SubGridGateMokka { UUID.fromString("9237e237-01e9-446f-899f-c3b5cf69d288"), 13 ) - )).toVector + ) ( new SubGridContainer( @@ -336,12 +336,10 @@ trait DbfsTestGrid extends SubGridGateMokka { protected val (ehvGridContainer, ehvSubGridGates) = { val nodes = Set(slackNode) - val lines = Set.empty[LineInput] - val transformers = Set.empty[Transformer2WInput] val rawGridElements = new RawGridElements( nodes.asJava, - lines.asJava, - transformers.asJava, + Set.empty[LineInput].asJava, + Set.empty[Transformer2WInput].asJava, Set.empty[Transformer3WInput].asJava, Set.empty[SwitchInput].asJava, Set.empty[MeasurementUnitInput].asJava @@ -363,16 +361,10 @@ trait DbfsTestGrid extends SubGridGateMokka { Set.empty[LineGraphicInput].asJava ) - val subGridGates: Vector[SubGridGate] = - (Set(transformer1).map( + val subGridGates: Seq[SubGridGate] = + Seq(transformer1).map( SubGridGate.fromTransformer2W - ) ++ rawGridElements.getTransformer3Ws.asScala - .flatMap(transformer => - Vector( - SubGridGate.fromTransformer3W(transformer, ConnectorPort.B), - SubGridGate.fromTransformer3W(transformer, ConnectorPort.C) - ) - )).toVector + ) ( new SubGridContainer( From ebb559ebb3775eb5bc39c469e33daf2c97bed618 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Fri, 1 Jul 2022 11:22:29 +0200 Subject: [PATCH 17/36] Solving CodeSmells in test, plus some minor improvements --- .../ie3/simona/agent/grid/DBFSAlgorithm.scala | 2 +- .../ontology/messages/PowerMessage.scala | 23 +- .../agent/grid/DBFSAlgorithmCenGridSpec.scala | 234 ++++++++---------- .../test/common/model/grid/DbfsTestGrid.scala | 4 +- 4 files changed, 120 insertions(+), 143 deletions(-) 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 20f7439296..5b2fea9602 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/DBFSAlgorithm.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/DBFSAlgorithm.scala @@ -1133,7 +1133,7 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { private def askInferiorGridsForPowers( currentSweepNo: Int, subGridGateToActorRef: Map[SubGridGate, ActorRef], - inferiorGridGates: Vector[SubGridGate], + inferiorGridGates: Seq[SubGridGate], askTimeout: Duration ): Option[Future[ReceivedPowerValues]] = { implicit val timeout: AkkaTimeout = AkkaTimeout.create(askTimeout) diff --git a/src/main/scala/edu/ie3/simona/ontology/messages/PowerMessage.scala b/src/main/scala/edu/ie3/simona/ontology/messages/PowerMessage.scala index 8248357023..81567adf6c 100644 --- a/src/main/scala/edu/ie3/simona/ontology/messages/PowerMessage.scala +++ b/src/main/scala/edu/ie3/simona/ontology/messages/PowerMessage.scala @@ -28,9 +28,15 @@ object PowerMessage { def q: ComparableQuantity[Power] } - /** Request the power values for the requested tick from an AssetAgent + /** Request the power values for the requested tick from an AssetAgent and + * provide the latest nodal voltage * * @param currentTick + * The tick that power values are requested for + * @param eInPu + * Real part of the complex, dimensionless nodal voltage + * @param fInPu + * Imaginary part of the complex, dimensionless nodal voltage */ final case class RequestAssetPowerMessage( currentTick: Long, @@ -41,7 +47,9 @@ object PowerMessage { /** Provide power values as a reply on an [[RequestAssetPowerMessage]] * * @param p + * Unchanged active power * @param q + * Unchanged reactive power */ final case class AssetPowerChangedMessage( override val p: ComparableQuantity[Power], @@ -53,9 +61,9 @@ object PowerMessage { * values for [[p]] and [[q]] has been send again as in the previous request * * @param p - * active power from the previous request + * Active power from the previous request * @param q - * reactive power from the previous request + * Reactive power from the previous request */ final case class AssetPowerUnchangedMessage( override val p: ComparableQuantity[Power], @@ -64,12 +72,13 @@ object PowerMessage { final case class RequestGridPowerMessage( currentSweepNo: Int, - nodeUuids: Vector[UUID] + nodeUuids: Seq[UUID] ) extends PowerRequestMessage final case class ProvideGridPowerMessage( - nodalResidualPower: Vector[ExchangePower] + nodalResidualPower: Seq[ExchangePower] ) extends PowerResponseMessage + object ProvideGridPowerMessage { /** Defining the exchanged power at one interconnection point @@ -77,9 +86,9 @@ object PowerMessage { * @param nodeUuid * Unique identifier of the node, at which this residual power did appear * @param p - * active power from the previous request + * Active power from the previous request * @param q - * reactive power from the previous request + * Reactive power from the previous request */ final case class ExchangePower( nodeUuid: UUID, 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 3c33db78c9..385cfedf0e 100644 --- a/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmCenGridSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmCenGridSpec.scala @@ -12,6 +12,7 @@ import akka.testkit.{ImplicitSender, TestProbe} import akka.util.Timeout import com.typesafe.config.ConfigFactory import edu.ie3.simona.agent.EnvironmentRefs +import edu.ie3.simona.agent.grid.DBFSAlgorithmCenGridSpec.GAActorAndModel import edu.ie3.simona.agent.grid.GridAgentData.GridAgentInitData import edu.ie3.simona.agent.state.GridAgentState.SimulateGrid import edu.ie3.simona.model.grid.RefSystem @@ -78,20 +79,16 @@ class DBFSAlgorithmCenGridSpec private val weatherService = TestProbe("weatherService") private val superiorGridAgent = TestProbe("superiorGridAgent_1000") - private val inferiorAndNodes11 = ( - TestProbe("inferiorGridAgent_11"), - Vector(UUID.fromString("78c5d473-e01b-44c4-afd2-e4ff3c4a5d7c")) - ) - private val inferiorAndNodes12 = ( - TestProbe("inferiorGridAgent_12"), - Vector(UUID.fromString("e364ef00-e6ca-46b1-ba2b-bb73c0c6fee0")) - ) - private val inferiorAndNodes13 = ( + + private val inferiorGrid11 = + GAActorAndModel(TestProbe("inferiorGridAgent_11"), Seq(node1.getUuid)) + + private val inferiorGrid12 = + GAActorAndModel(TestProbe("inferiorGridAgent_12"), Seq(node2.getUuid)) + + private val inferiorGrid13 = GAActorAndModel( TestProbe("inferiorGridAgent_13"), - Vector( - UUID.fromString("d44ba8ed-81db-4a22-a40d-f7c0d0808a75"), - UUID.fromString("47ef9983-8fcf-4713-be90-093fc27864ae") - ) + Seq(node3a.getUuid, node3b.getUuid) ) private val environmentRefs = EnvironmentRefs( @@ -121,9 +118,9 @@ class DBFSAlgorithmCenGridSpec gate -> superiorGridAgent.ref case gate => val actor = gate.getInferiorSubGrid match { - case 11 => inferiorAndNodes11._1 - case 12 => inferiorAndNodes12._1 - case 13 => inferiorAndNodes13._1 + case 11 => inferiorGrid11 + case 12 => inferiorGrid12 + case 13 => inferiorGrid13 } gate -> actor.ref }.toMap @@ -207,20 +204,11 @@ class DBFSAlgorithmCenGridSpec /* We expect one grid power request message per inferior grid */ - inferiorAndNodes11._1 - .expectMsgType[RequestGridPowerMessage] - .nodeUuids shouldBe inferiorAndNodes11._2 - val firstPowerRequestSender11 = inferiorAndNodes11._1.lastSender + val firstPowerRequestSender11 = inferiorGrid11.expectGridPowerMessageAsk() - inferiorAndNodes12._1 - .expectMsgType[RequestGridPowerMessage] - .nodeUuids shouldBe inferiorAndNodes12._2 - val firstPowerRequestSender12 = inferiorAndNodes12._1.lastSender + val firstPowerRequestSender12 = inferiorGrid12.expectGridPowerMessageAsk() - inferiorAndNodes13._1 - .expectMsgType[RequestGridPowerMessage] - .nodeUuids should contain allElementsOf inferiorAndNodes13._2 - val firstPowerRequestSender13 = inferiorAndNodes13._1.lastSender + val firstPowerRequestSender13 = inferiorGrid13.expectGridPowerMessageAsk() // we expect 1 request for slack voltage values // (slack values are requested by our agent under test from the superior grid) @@ -238,54 +226,35 @@ class DBFSAlgorithmCenGridSpec // 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 - inferiorAndNodes11._2.foreach { nodeUuid => - inferiorAndNodes11._1.send( - centerGridAgent, - RequestSlackVoltageMessage(firstSweepNo, nodeUuid) - ) - } + inferiorGrid11.requestSlackVoltage(centerGridAgent, firstSweepNo) - inferiorAndNodes12._2.foreach { nodeUuid => - inferiorAndNodes12._1.send( - centerGridAgent, - RequestSlackVoltageMessage(firstSweepNo, nodeUuid) - ) - } + inferiorGrid12.requestSlackVoltage(centerGridAgent, firstSweepNo) - inferiorAndNodes13._2.foreach { nodeUuid => - inferiorAndNodes13._1.send( - centerGridAgent, - RequestSlackVoltageMessage(firstSweepNo, nodeUuid) - ) - } + 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) - inferiorAndNodes11._2.foreach { nodeUuid => - inferiorAndNodes11._1.expectMsg( - ProvideSlackVoltageMessage( - firstSweepNo, - nodeUuid, - Quantities.getQuantity(110, KILOVOLT), - Quantities.getQuantity(0, KILOVOLT) - ) + inferiorGrid11.gaProbe.expectMsg( + ProvideSlackVoltageMessage( + firstSweepNo, + node1.getUuid, + Quantities.getQuantity(110, KILOVOLT), + Quantities.getQuantity(0, KILOVOLT) ) - } + ) - inferiorAndNodes12._2.foreach { nodeUuid => - inferiorAndNodes12._1.expectMsg( - ProvideSlackVoltageMessage( - firstSweepNo, - nodeUuid, - Quantities.getQuantity(110, KILOVOLT), - Quantities.getQuantity(0, KILOVOLT) - ) + inferiorGrid12.gaProbe.expectMsg( + ProvideSlackVoltageMessage( + firstSweepNo, + node2.getUuid, + Quantities.getQuantity(110, KILOVOLT), + Quantities.getQuantity(0, KILOVOLT) ) - } + ) - inferiorAndNodes13._2.foreach { nodeUuid => - inferiorAndNodes13._1.expectMsg( + inferiorGrid13.nodeUuids.foreach { nodeUuid => + inferiorGrid13.gaProbe.expectMsg( ProvideSlackVoltageMessage( firstSweepNo, nodeUuid, @@ -298,10 +267,10 @@ class DBFSAlgorithmCenGridSpec // we now answer the request of our centerGridAgent // with 3 fake grid power messages and 1 fake slack voltage message - inferiorAndNodes11._1.send( + inferiorGrid11.gaProbe.send( firstPowerRequestSender11, ProvideGridPowerMessage( - inferiorAndNodes11._2.map(nodeUuid => + inferiorGrid11.nodeUuids.map(nodeUuid => ExchangePower( nodeUuid, Quantities.getQuantity(0, KILOWATT), @@ -311,10 +280,10 @@ class DBFSAlgorithmCenGridSpec ) ) - inferiorAndNodes12._1.send( + inferiorGrid12.gaProbe.send( firstPowerRequestSender12, ProvideGridPowerMessage( - inferiorAndNodes12._2.map(nodeUuid => + inferiorGrid12.nodeUuids.map(nodeUuid => ExchangePower( nodeUuid, Quantities.getQuantity(0, KILOWATT), @@ -324,10 +293,10 @@ class DBFSAlgorithmCenGridSpec ) ) - inferiorAndNodes13._1.send( + inferiorGrid13.gaProbe.send( firstPowerRequestSender13, ProvideGridPowerMessage( - inferiorAndNodes13._2.map(nodeUuid => + inferiorGrid13.nodeUuids.map(nodeUuid => ExchangePower( nodeUuid, Quantities.getQuantity(0, KILOWATT), @@ -423,78 +392,54 @@ class DBFSAlgorithmCenGridSpec // after the intermediate power flow calculation // We expect one grid power request message, as all four sub grids are mapped onto one actor reference - inferiorAndNodes11._1 - .expectMsgType[RequestGridPowerMessage] - .nodeUuids shouldBe inferiorAndNodes11._2 - val secondPowerRequestSender11 = inferiorAndNodes11._1.lastSender - inferiorAndNodes12._1 - .expectMsgType[RequestGridPowerMessage] - .nodeUuids shouldBe inferiorAndNodes12._2 - val secondPowerRequestSender12 = inferiorAndNodes12._1.lastSender + val secondPowerRequestSender11 = + inferiorGrid11.expectGridPowerMessageAsk() - inferiorAndNodes13._1 - .expectMsgType[RequestGridPowerMessage] - .nodeUuids should contain allElementsOf inferiorAndNodes13._2 - val secondPowerRequestSender13 = inferiorAndNodes13._1.lastSender + val secondPowerRequestSender12 = + inferiorGrid12.expectGridPowerMessageAsk() + + val secondPowerRequestSender13 = + inferiorGrid13.expectGridPowerMessageAsk() // 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 4 inferior grid agents - inferiorAndNodes11._2.foreach { nodeUuid => - inferiorAndNodes11._1.send( - centerGridAgent, - RequestSlackVoltageMessage(firstSweepNo, nodeUuid) - ) - } + inferiorGrid11.requestSlackVoltage(centerGridAgent, firstSweepNo) - inferiorAndNodes12._2.foreach { nodeUuid => - inferiorAndNodes12._1.send( - centerGridAgent, - RequestSlackVoltageMessage(firstSweepNo, nodeUuid) - ) - } + inferiorGrid12.requestSlackVoltage(centerGridAgent, firstSweepNo) - inferiorAndNodes13._2.foreach { nodeUuid => - inferiorAndNodes13._1.send( - centerGridAgent, - RequestSlackVoltageMessage(firstSweepNo, nodeUuid) - ) - } + inferiorGrid13.requestSlackVoltage(centerGridAgent, firstSweepNo) // 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) - inferiorAndNodes11._2.foreach { expectedNodeUuid => - val received = - inferiorAndNodes11._1.expectMsgType[ProvideSlackVoltageMessage] - - received.currentSweepNo shouldBe firstSweepNo - received.nodeUuid shouldBe expectedNodeUuid - received.e should equalWithTolerance( - Quantities.getQuantity(110.1196117051188620, KILOVOLT) - ) - received.f should equalWithTolerance( - Quantities.getQuantity(-0.009318349620959118, KILOVOLT) - ) + inside(inferiorGrid11.gaProbe.expectMsgType[ProvideSlackVoltageMessage]) { + case ProvideSlackVoltageMessage(currentSweepNo, nodeUuid, e, f) => + currentSweepNo shouldBe firstSweepNo + nodeUuid shouldBe node1.getUuid + e should equalWithTolerance( + Quantities.getQuantity(110.1196117051188620, KILOVOLT) + ) + f should equalWithTolerance( + Quantities.getQuantity(-0.009318349620959118, KILOVOLT) + ) } - inferiorAndNodes12._2.foreach { expectedNodeUuid => - val received = - inferiorAndNodes12._1.expectMsgType[ProvideSlackVoltageMessage] - - received.currentSweepNo shouldBe firstSweepNo - received.nodeUuid shouldBe expectedNodeUuid - received.e should equalWithTolerance( - Quantities.getQuantity(110.1422124824355620, KILOVOLT) - ) - received.f should equalWithTolerance( - Quantities.getQuantity(-0.014094294956794604, KILOVOLT) - ) + inside(inferiorGrid12.gaProbe.expectMsgType[ProvideSlackVoltageMessage]) { + case ProvideSlackVoltageMessage(currentSweepNo, nodeUuid, e, f) => + currentSweepNo shouldBe firstSweepNo + nodeUuid shouldBe node2.getUuid + e should equalWithTolerance( + Quantities.getQuantity(110.1422124824355620, KILOVOLT) + ) + f should equalWithTolerance( + Quantities.getQuantity(-0.014094294956794604, KILOVOLT) + ) } - inferiorAndNodes13._1.expectMsgType[ProvideSlackVoltageMessage] match { + inferiorGrid13.gaProbe.expectMsgType[ProvideSlackVoltageMessage] match { case received if received.nodeUuid.equals(node3a.getUuid) => received.currentSweepNo shouldBe firstSweepNo received.nodeUuid shouldBe node3a.getUuid @@ -519,10 +464,10 @@ class DBFSAlgorithmCenGridSpec // we now answer the request of our centerGridAgent // with 1 fake grid power message - inferiorAndNodes11._1.send( + inferiorGrid11.gaProbe.send( secondPowerRequestSender11, ProvideGridPowerMessage( - inferiorAndNodes11._2.map(nodeUuid => + inferiorGrid11.nodeUuids.map(nodeUuid => ExchangePower( nodeUuid, Quantities.getQuantity(0, KILOWATT), @@ -532,10 +477,10 @@ class DBFSAlgorithmCenGridSpec ) ) - inferiorAndNodes12._1.send( + inferiorGrid12.gaProbe.send( secondPowerRequestSender12, ProvideGridPowerMessage( - inferiorAndNodes12._2.map(nodeUuid => + inferiorGrid12.nodeUuids.map(nodeUuid => ExchangePower( nodeUuid, Quantities.getQuantity(0, KILOWATT), @@ -545,10 +490,10 @@ class DBFSAlgorithmCenGridSpec ) ) - inferiorAndNodes13._1.send( + inferiorGrid13.gaProbe.send( secondPowerRequestSender13, ProvideGridPowerMessage( - inferiorAndNodes13._2.map(nodeUuid => + inferiorGrid13.nodeUuids.map(nodeUuid => ExchangePower( nodeUuid, Quantities.getQuantity(0, KILOWATT), @@ -586,3 +531,26 @@ class DBFSAlgorithmCenGridSpec } } } + +object DBFSAlgorithmCenGridSpec extends UnitSpec { + final case class GAActorAndModel(gaProbe: TestProbe, nodeUuids: Seq[UUID]) { + def ref: ActorRef = gaProbe.ref + + def expectGridPowerMessageAsk(): ActorRef = { + gaProbe + .expectMsgType[RequestGridPowerMessage] + .nodeUuids should contain allElementsOf nodeUuids + + gaProbe.lastSender + } + + def requestSlackVoltage(receiver: ActorRef, sweepNo: Int): Unit = { + nodeUuids.foreach { node => + gaProbe.send( + receiver, + RequestSlackVoltageMessage(sweepNo, node) + ) + } + } + } +} 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 68c38c908c..4452103a90 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 @@ -46,7 +46,7 @@ import scala.jdk.CollectionConverters._ */ trait DbfsTestGrid extends SubGridGateMokka { // 4 HS nodes, 1 slack HöS node - private val node1 = new NodeInput( + protected val node1 = new NodeInput( UUID.fromString("78c5d473-e01b-44c4-afd2-e4ff3c4a5d7c"), "HS_NET1_Station_1", OperatorInput.NO_OPERATOR_ASSIGNED, @@ -57,7 +57,7 @@ trait DbfsTestGrid extends SubGridGateMokka { GermanVoltageLevelUtils.HV, 1 ) - private val node2 = new NodeInput( + protected val node2 = new NodeInput( UUID.fromString("e364ef00-e6ca-46b1-ba2b-bb73c0c6fee0"), "HS_NET1_Station_2", OperatorInput.NO_OPERATOR_ASSIGNED, From 7300b2b7412d54a3bb852dd9cece1b0207768856 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Fri, 8 Jul 2022 10:34:51 +0200 Subject: [PATCH 18/36] Apply suggestions from code review Co-authored-by: t-ober <63147366+t-ober@users.noreply.github.com> --- CHANGELOG.md | 2 +- src/main/scala/edu/ie3/simona/agent/grid/DBFSAlgorithm.scala | 2 +- src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala | 2 +- .../scala/edu/ie3/simona/agent/grid/ReceivedValuesStore.scala | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5ab452b1c..16bf8819d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,7 +48,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed some unreachable code [#167](https://github.com/ie3-institute/simona/issues/167) - Fix treatment of non-InitializeTrigger triggers in initialization within SimScheduler [#237](https://github.com/ie3-institute/simona/issues/237) - Fix breaking SIMONA caused by introducing temperature dependant load profiles in PSDM [#255](https://github.com/ie3-institute/simona/issues/255) -- Fix power exchange between grids [#22](https://github.com/ie3-institute/simona/issues/22) +- Fix handling of multiple connections between subgrids [#22](https://github.com/ie3-institute/simona/issues/22) - Consolidate request replies for different sub grid gates in one message - Await and send responses for distinct pairs of sender reference and target node 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 5b2fea9602..2be40c1787 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/DBFSAlgorithm.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/DBFSAlgorithm.scala @@ -353,7 +353,7 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { // update the sweep value store and clear all received maps // note: normally it is expected that this has to be done after power flow calculations but for the sake - // of having it only once in the code we put this here. Otherwise it would have to been putted before EVERY + // of having it only once in the code we put this here. Otherwise it would have to been put before EVERY // return with a valid power flow result (currently happens already in two situations) val updatedGridAgentBaseData = if (stillPendingRequestAnswers.isEmpty) { diff --git a/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala b/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala index 61cf39228f..abe2d2ebd9 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala @@ -442,7 +442,7 @@ trait PowerFlowSupport { val powerFlow = NewtonRaphsonPF(epsilon, maxIterations, admittanceMatrix) - /* Currently, only one slack node per sub net is allowed. In case a model has more than one, set all others to + /* Currently, only one slack node per sub grid is allowed. In case a model has more than one, set all others to * PQ nodes. ATTENTION: This does not cover the power flow situation correctly! */ val adaptedOperatingPoint = operatingPoint .foldLeft((Array.empty[PresetData], true)) { diff --git a/src/main/scala/edu/ie3/simona/agent/grid/ReceivedValuesStore.scala b/src/main/scala/edu/ie3/simona/agent/grid/ReceivedValuesStore.scala index 312ea28814..a8d7b5f7f5 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/ReceivedValuesStore.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/ReceivedValuesStore.scala @@ -103,7 +103,7 @@ object ReceivedValuesStore { (uuid, actorRefs.map(actorRef => actorRef -> None).toMap) } - /* Add everything, that I expect from my sub ordinate grid agents. */ + /* Add everything, that I expect from my subordinate grid agents. */ inferiorSubGridGateToActorRef .map { case (gate, reference) => gate.getSuperiorNode.getUuid -> reference From 3f571a374e0c2f7ae40f9fc3173aef0c7c31cd2f Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Fri, 8 Jul 2022 11:11:26 +0200 Subject: [PATCH 19/36] Addressing some reviewer comments --- CHANGELOG.md | 1 + .../ie3/simona/agent/grid/DBFSAlgorithm.scala | 8 ++------ .../ontology/messages/PowerMessage.scala | 19 +++++++++++++++++-- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 16bf8819d1..70d13b910a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Made SimonaConfig.BaseRuntimeConfig serializable [#36](https://github.com/ie3-institute/simona/issues/36) - Adapt to new simonaAPI snapshot [#95](https://github.com/ie3-institute/simona/issues/95) - Update Sphinx to 4.5.0 as well as extensions [#214](https://github.com/ie3-institute/simona/issues/214) +- Improved code quality in and around DBFS algorithm [#265](https://github.com/ie3-institute/simona/issues/265) ### Fixed - Location of `vn_simona` test grid (was partially in Berlin and Dortmund) [#72](https://github.com/ie3-institute/simona/issues/72) 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 2be40c1787..00642a42c2 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/DBFSAlgorithm.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/DBFSAlgorithm.scala @@ -296,11 +296,7 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { .map { case (subGridGate, _) => subGridGate.getSuperiorNode } .find(_.getUuid == firstRequestedNodeUuid) .map(_.getSubnet) match { - case Some(requestingSubnetNumber) => - log.debug( - "Received request for grid power values and im READY to provide." - ) - + case Some(requestingSubgridNumber) => powerFlowResult match { case validNewtonRaphsonPFResult: ValidNewtonRaphsonPFResult => val exchangePowers = requestedNodeUuids @@ -349,7 +345,7 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { /* Determine the remaining replies */ val stillPendingRequestAnswers = - pendingRequestAnswers.filterNot(_ == requestingSubnetNumber) + pendingRequestAnswers.filterNot(_ == requestingSubgridNumber) // update the sweep value store and clear all received maps // note: normally it is expected that this has to be done after power flow calculations but for the sake diff --git a/src/main/scala/edu/ie3/simona/ontology/messages/PowerMessage.scala b/src/main/scala/edu/ie3/simona/ontology/messages/PowerMessage.scala index 81567adf6c..707b83d8f3 100644 --- a/src/main/scala/edu/ie3/simona/ontology/messages/PowerMessage.scala +++ b/src/main/scala/edu/ie3/simona/ontology/messages/PowerMessage.scala @@ -44,7 +44,7 @@ object PowerMessage { fInPu: ComparableQuantity[Dimensionless] ) extends PowerRequestMessage - /** Provide power values as a reply on an [[RequestAssetPowerMessage]] + /** Provide power values as a reply to a [[RequestAssetPowerMessage]] * * @param p * Unchanged active power @@ -56,7 +56,7 @@ object PowerMessage { override val q: ComparableQuantity[Power] ) extends ProvidePowerMessage - /** Provide values as a reply on a [[RequestAssetPowerMessage]]. In contrast + /** Provide values as a reply to a [[RequestAssetPowerMessage]]. In contrast * to [[AssetPowerChangedMessage]], this message indicates that the same * values for [[p]] and [[q]] has been send again as in the previous request * @@ -70,11 +70,23 @@ object PowerMessage { override val q: ComparableQuantity[Power] ) extends ProvidePowerMessage + /** Request complex power at the nodes that the inferior sub grid shares with + * the sender's sub grid + * @param currentSweepNo + * The current sweep + * @param nodeUuids + * The UUIDs of the nodes that are bordering the sender's grid + */ final case class RequestGridPowerMessage( currentSweepNo: Int, nodeUuids: Seq[UUID] ) extends PowerRequestMessage + /** Provide complex power at the nodes that the sender's sub grid shares with + * the superior sub grid, as a reply to a [[RequestGridPowerMessage]]. + * @param nodalResidualPower + * The complex powers of the shared nodes + */ final case class ProvideGridPowerMessage( nodalResidualPower: Seq[ExchangePower] ) extends PowerResponseMessage @@ -97,6 +109,9 @@ object PowerMessage { ) extends ProvidePowerMessage } + /** Indicate that the power flow calculation failed, as a reply to a + * [[RequestGridPowerMessage]]. + */ final case object FailedPowerFlow extends PowerResponseMessage } From c89ee1b132821194037d1018bd2be232477da00d Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Fri, 8 Jul 2022 11:26:24 +0200 Subject: [PATCH 20/36] Replacing subnet with subgrid in a lot of places --- .../ie3/simona/agent/grid/DBFSAlgorithm.scala | 16 +++++------ .../ie3/simona/agent/grid/GridAgentData.scala | 28 +++++++++---------- .../agent/grid/GridAgentDataHelper.scala | 28 +++++++++---------- .../simona/agent/grid/GridEnvironment.scala | 4 +-- .../simona/agent/grid/PowerFlowSupport.scala | 2 +- .../agent/grid/GridAgentDataHelperSpec.scala | 4 +-- 6 files changed, 41 insertions(+), 41 deletions(-) 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 00642a42c2..82bf9b8a10 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/DBFSAlgorithm.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/DBFSAlgorithm.scala @@ -95,7 +95,7 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { // 2. inferior grids p/q values askInferiorGridsForPowers( gridAgentBaseData.currentSweepNo, - gridAgentBaseData.gridEnv.subnetGateToActorRef, + gridAgentBaseData.gridEnv.subgridGateToActorRef, gridAgentBaseData.inferiorGridGates, gridAgentBaseData.powerFlowParams.sweepTimeout ) @@ -103,7 +103,7 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { // 3. superior grids slack voltage askSuperiorGridsForSlackVoltages( gridAgentBaseData.currentSweepNo, - gridAgentBaseData.gridEnv.subnetGateToActorRef, + gridAgentBaseData.gridEnv.subgridGateToActorRef, gridAgentBaseData.superiorGridGates, gridAgentBaseData.powerFlowParams.sweepTimeout ) @@ -286,13 +286,13 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { pendingRequestAnswers ) ) => - /* Determine the subnet number of the grid agent, that has sent the request */ + /* Determine the subgrid number of the grid agent, that has sent the request */ val firstRequestedNodeUuid = requestedNodeUuids.headOption.getOrElse( throw new DBFSAlgorithmException( "Did receive a grid power request but without specified nodes" ) ) - gridAgentBaseData.gridEnv.subnetGateToActorRef + gridAgentBaseData.gridEnv.subgridGateToActorRef .map { case (subGridGate, _) => subGridGate.getSuperiorNode } .find(_.getUuid == firstRequestedNodeUuid) .map(_.getSubnet) match { @@ -375,7 +375,7 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { case None => /* It is not possible to determine, who has asked */ log.error( - "I got a grid power request from a subnet I don't know. Can't answer it properly." + "I got a grid power request from a subgrid I don't know. Can't answer it properly." ) stay() replying FailedPowerFlow using gridAgentBaseData } @@ -391,7 +391,7 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { // request the updated slack voltages from the superior grid askSuperiorGridsForSlackVoltages( gridAgentBaseData.currentSweepNo, - gridAgentBaseData.gridEnv.subnetGateToActorRef, + gridAgentBaseData.gridEnv.subgridGateToActorRef, gridAgentBaseData.superiorGridGates, gridAgentBaseData.powerFlowParams.sweepTimeout ) @@ -409,7 +409,7 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { // inform my child grids about the end of this grid simulation gridAgentBaseData.inferiorGridGates .map { - gridAgentBaseData.gridEnv.subnetGateToActorRef(_) + gridAgentBaseData.gridEnv.subgridGateToActorRef(_) } .distinct .foreach(_ ! FinishGridSimulationTrigger(currentTick)) @@ -670,7 +670,7 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { val askForInferiorGridPowersOpt = askInferiorGridsForPowers( gridAgentBaseData.currentSweepNo, - gridAgentBaseData.gridEnv.subnetGateToActorRef, + gridAgentBaseData.gridEnv.subgridGateToActorRef, gridAgentBaseData.inferiorGridGates, gridAgentBaseData.powerFlowParams.sweepTimeout ) 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 311a3a80e3..5af13166c7 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/GridAgentData.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/GridAgentData.scala @@ -54,9 +54,9 @@ object GridAgentData { refSystem: RefSystem ) extends GridAgentData with GridAgentDataHelper { - override protected val subnetGates: Vector[SubGridGate] = + override protected val subgridGates: Vector[SubGridGate] = subGridGateToActorRef.keys.toVector - override protected val subnetId: Int = subGridContainer.getSubnet + override protected val subgridId: Int = subGridContainer.getSubnet } /** State data indicating that a power flow has been executed. @@ -66,8 +66,8 @@ object GridAgentData { * @param powerFlowResult * result of the executed power flow * @param pendingRequestAnswers - * Set of subnet numbers of those [[GridAgent]] s, that still don't have - * their request answered, yet + * Set of subgrid numbers of [[GridAgent]]s that don't have their request + * answered, yet */ final case class PowerFlowDoneData private ( gridAgentBaseData: GridAgentBaseData, @@ -80,8 +80,8 @@ object GridAgentData { gridAgentBaseData: GridAgentBaseData, powerFlowResult: PowerFlowResult ): PowerFlowDoneData = { - /* Determine the subnet numbers of all superior grids */ - val superiorSubGrids = gridAgentBaseData.gridEnv.subnetGateToActorRef + /* Determine the subgrid numbers of all superior grids */ + val superiorSubGrids = gridAgentBaseData.gridEnv.subgridGateToActorRef .map { case (subGridGate, _) => subGridGate.getSuperiorNode.getSubnet } .filterNot(_ == gridAgentBaseData.gridEnv.gridModel.subnetNo) .toSet @@ -97,7 +97,7 @@ object GridAgentData { def apply( gridModel: GridModel, - subnetGateToActorRef: Map[SubGridGate, ActorRef], + subgridGateToActorRef: Map[SubGridGate, ActorRef], nodeToAssetAgents: Map[UUID, Set[ActorRef]], superiorGridNodeUuids: Vector[UUID], inferiorGridGates: Vector[SubGridGate], @@ -112,11 +112,11 @@ object GridAgentData { Int, SweepValueStore ] // initialization is assumed to be always with no sweep data - val inferiorGridGateToActorRef = subnetGateToActorRef.filter { + val inferiorGridGateToActorRef = subgridGateToActorRef.filter { case (gate, _) => inferiorGridGates.contains(gate) } GridAgentBaseData( - GridEnvironment(gridModel, subnetGateToActorRef, nodeToAssetAgents), + GridEnvironment(gridModel, subgridGateToActorRef, nodeToAssetAgents), powerFlowParams, currentSweepNo, ReceivedValuesStore.empty( @@ -154,7 +154,7 @@ object GridAgentData { gridAgentBaseData.copy( receivedValueStore = ReceivedValuesStore.empty( gridAgentBaseData.gridEnv.nodeToAssetAgents, - gridAgentBaseData.gridEnv.subnetGateToActorRef.filter { + gridAgentBaseData.gridEnv.subgridGateToActorRef.filter { case (gate, _) => inferiorGridGates.contains(gate) }, superiorGridNodeUuids @@ -195,9 +195,9 @@ object GridAgentData { ) extends GridAgentData with GridAgentDataHelper { - override protected val subnetGates: Vector[SubGridGate] = - gridEnv.subnetGateToActorRef.keys.toVector - override protected val subnetId: Int = gridEnv.gridModel.subnetNo + override protected val subgridGates: Vector[SubGridGate] = + gridEnv.subgridGateToActorRef.keys.toVector + override protected val subgridId: Int = gridEnv.gridModel.subnetNo val allRequestedDataReceived: Boolean = { // we expect power values from inferior grids and assets @@ -448,7 +448,7 @@ object GridAgentData { sweepValueStores = updatedSweepValueStore, receivedValueStore = ReceivedValuesStore.empty( gridEnv.nodeToAssetAgents, - gridEnv.subnetGateToActorRef.filter { case (gate, _) => + gridEnv.subgridGateToActorRef.filter { case (gate, _) => inferiorGridGates.contains(gate) }, superiorGridNodeUuids diff --git a/src/main/scala/edu/ie3/simona/agent/grid/GridAgentDataHelper.scala b/src/main/scala/edu/ie3/simona/agent/grid/GridAgentDataHelper.scala index f3931913c6..dae3187e0e 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/GridAgentDataHelper.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/GridAgentDataHelper.scala @@ -15,43 +15,43 @@ import edu.ie3.datamodel.graph.SubGridGate */ private[grid] trait GridAgentDataHelper { - protected val subnetGates: Vector[SubGridGate] - protected val subnetId: Int + protected val subgridGates: Vector[SubGridGate] + protected val subgridId: Int // methods definition def superiorGridIds: Vector[String] = - subnetGates.collect { - case gate: SubGridGate if gate.getInferiorSubGrid == subnetId => + subgridGates.collect { + case gate: SubGridGate if gate.getInferiorSubGrid == subgridId => gate.getSuperiorSubGrid.toString } def inferiorGridIds: Vector[String] = - subnetGates.collect { - case gate: SubGridGate if gate.getSuperiorSubGrid == subnetId => + subgridGates.collect { + case gate: SubGridGate if gate.getSuperiorSubGrid == subgridId => gate.getInferiorSubGrid.toString } def superiorGridNodeUuids: Vector[UUID] = - subnetGates.collect { - case gate: SubGridGate if gate.getInferiorSubGrid == subnetId => + subgridGates.collect { + case gate: SubGridGate if gate.getInferiorSubGrid == subgridId => gate.getSuperiorNode.getUuid } def inferiorGridNodeUuids: Vector[UUID] = - subnetGates.collect { - case gate: SubGridGate if gate.getSuperiorSubGrid == subnetId => + subgridGates.collect { + case gate: SubGridGate if gate.getSuperiorSubGrid == subgridId => gate.getInferiorNode.getUuid } def superiorGridGates: Vector[SubGridGate] = - subnetGates.collect { - case gate: SubGridGate if gate.getInferiorSubGrid == subnetId => + subgridGates.collect { + case gate: SubGridGate if gate.getInferiorSubGrid == subgridId => gate } def inferiorGridGates: Vector[SubGridGate] = - subnetGates.collect { - case gate: SubGridGate if gate.getSuperiorSubGrid == subnetId => + subgridGates.collect { + case gate: SubGridGate if gate.getSuperiorSubGrid == subgridId => gate } diff --git a/src/main/scala/edu/ie3/simona/agent/grid/GridEnvironment.scala b/src/main/scala/edu/ie3/simona/agent/grid/GridEnvironment.scala index 9b4710588d..18d4470c7a 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/GridEnvironment.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/GridEnvironment.scala @@ -17,7 +17,7 @@ import edu.ie3.simona.model.grid.GridModel * * @param gridModel * [[GridModel]] with all asset information - * @param subnetGateToActorRef + * @param subgridGateToActorRef * a mapping of all [[SubGridGate]] s to their corresponding [[ActorRef]] s * @param nodeToAssetAgents * a mapping of all node uuids to a set of asset [[ActorRef]] s at those @@ -25,6 +25,6 @@ import edu.ie3.simona.model.grid.GridModel */ final case class GridEnvironment( gridModel: GridModel, - subnetGateToActorRef: Map[SubGridGate, ActorRef], + subgridGateToActorRef: Map[SubGridGate, ActorRef], nodeToAssetAgents: Map[UUID, Set[ActorRef]] ) diff --git a/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala b/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala index abe2d2ebd9..649a9c13e6 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala @@ -481,7 +481,7 @@ trait PowerFlowSupport { case Success(result) => result case Failure(exception) => throw new DBFSAlgorithmException( - s"Power flow calculation in subnet ${gridModel.subnetNo} failed.", + s"Power flow calculation in subgrid ${gridModel.subnetNo} failed.", exception ) } diff --git a/src/test/scala/edu/ie3/simona/agent/grid/GridAgentDataHelperSpec.scala b/src/test/scala/edu/ie3/simona/agent/grid/GridAgentDataHelperSpec.scala index b120684e1e..76a438267d 100644 --- a/src/test/scala/edu/ie3/simona/agent/grid/GridAgentDataHelperSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/grid/GridAgentDataHelperSpec.scala @@ -15,8 +15,8 @@ import edu.ie3.simona.test.common.model.grid.SubGridGateMokka object GridAgentDataHelperSpec { final case class TestGridData( - subnetId: Int, - subnetGates: Vector[SubGridGate] + subgridId: Int, + subgridGates: Vector[SubGridGate] ) extends GridAgentDataHelper { def getSuperiorGridGates: Vector[SubGridGate] = From aebcc061c7b898a41a8227abc6af53866b6265dd Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Fri, 8 Jul 2022 19:54:50 +0200 Subject: [PATCH 21/36] Variable renaming --- .../edu/ie3/simona/agent/grid/ReceivedValuesStore.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/agent/grid/ReceivedValuesStore.scala b/src/main/scala/edu/ie3/simona/agent/grid/ReceivedValuesStore.scala index a8d7b5f7f5..e27589053d 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/ReceivedValuesStore.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/ReceivedValuesStore.scala @@ -110,19 +110,19 @@ object ReceivedValuesStore { } .foldLeft(assetsToReceivedPower) { case ( - subOrdinateToReceivedPower, + subordinateToReceivedPower, couplingNodeUuid -> inferiorSubGridRef ) => /* Check, if there is already something expected for the given coupling node * and add reference to the subordinate grid agent */ - val actorRefToMessage = subOrdinateToReceivedPower + val actorRefToMessage = subordinateToReceivedPower .getOrElse( couplingNodeUuid, Map.empty[ActorRef, Option[ProvidePowerMessage]] ) + (inferiorSubGridRef -> None) /* Update the existing map */ - subOrdinateToReceivedPower + (couplingNodeUuid -> actorRefToMessage) + subordinateToReceivedPower + (couplingNodeUuid -> actorRefToMessage) } } From a42da7c35589b710572775f43bc98e50af4d1aa8 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Fri, 8 Jul 2022 21:42:59 +0200 Subject: [PATCH 22/36] Refactoring SweepValueStore to use a map instead of a vector --- .../ie3/simona/agent/grid/DBFSAlgorithm.scala | 57 ++++++++++-------- .../agent/grid/GridResultsSupport.scala | 46 +++++++------- .../simona/agent/grid/PowerFlowSupport.scala | 60 +++++++++---------- .../simona/agent/grid/SweepValueStore.scala | 55 ++++++----------- .../agent/grid/GridResultsSupportSpec.scala | 15 ++--- 5 files changed, 106 insertions(+), 127 deletions(-) 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 82bf9b8a10..a0c015ff8f 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/DBFSAlgorithm.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/DBFSAlgorithm.scala @@ -187,15 +187,13 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { .get(currentSweepNo - 1) .map((_, currentSweepNo - 1)) }).map { case (result, sweepNo) => - // get nodeUUID - result.sweepData.find(_.nodeUuid == nodeUuid) match { - case Some(sweepValueStoreData) => - val slackVoltageInPu = sweepValueStoreData.stateData.voltage + result.sweepData.get(nodeUuid) match { + case Some(stateData) => val mainRefSystem = gridAgentBaseData.gridEnv.gridModel.mainRefSystem ( - mainRefSystem.vInSi(slackVoltageInPu.real), - mainRefSystem.vInSi(slackVoltageInPu.imag) + mainRefSystem.vInSi(stateData.voltage.real), + mainRefSystem.vInSi(stateData.voltage.imag) ) case None => throw new DBFSAlgorithmException( @@ -810,31 +808,39 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { } else { log.debug("Sweep value store is not empty. Check for deviation ...") + val previousSweepData = updatedGridAgentBaseData.sweepValueStores + .getOrElse( + currentSweepNo - 1, + throw new DBFSAlgorithmException( + s"No data for previous sweep with no ${currentSweepNo - 1} available!" + ) + ) + .sweepData + val currentSweepData = updatedGridAgentBaseData.sweepValueStores + .getOrElse( + currentSweepNo, + throw new DBFSAlgorithmException( + s"No data for current sweep with no $currentSweepNo available!" + ) + ) + .sweepData + + // define a set order of nodes, so that the order of previous and current state data matches + val nodeUuids = previousSweepData.keys.toSeq + // calculate deviation vector for all nodes val previousSweepNodePower: DenseVector[Complex] = DenseVector( - updatedGridAgentBaseData.sweepValueStores - .getOrElse( - currentSweepNo - 1, - throw new DBFSAlgorithmException( - s"No data for previous sweep with no ${currentSweepNo - 1} available!" - ) - ) - .sweepData - .map(_.stateData.power) + nodeUuids + .flatMap(previousSweepData.get) + .map(_.power) .toArray ) val currentSweepNodePower: DenseVector[Complex] = DenseVector( - updatedGridAgentBaseData.sweepValueStores - .getOrElse( - currentSweepNo, - throw new DBFSAlgorithmException( - s"No data for current sweep with no $currentSweepNo available!" - ) - ) - .sweepData - .map(_.stateData.power) + nodeUuids + .flatMap(currentSweepData.get) + .map(_.power) .toArray ) @@ -1072,13 +1078,12 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { case Some(sweepValueStore) => val (eInSi, fInSi) = refSystem.vInSi( sweepValueStore.sweepData - .find(_.nodeUuid == nodeUuid) .getOrElse( + nodeUuid, throw new DBFSAlgorithmException( s"Provided Sweep value store contains no data for node with id $nodeUuid" ) ) - .stateData .voltage ) ( 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 7a0440bfea..9879327175 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/GridResultsSupport.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/GridResultsSupport.scala @@ -19,7 +19,6 @@ import edu.ie3.datamodel.models.result.connector.{ } import edu.ie3.powerflow.model.NodeData.StateData import edu.ie3.simona.agent.grid.GridResultsSupport.PartialTransformer3wResult -import edu.ie3.simona.agent.grid.SweepValueStore.SweepValueStoreData import edu.ie3.simona.event.ResultEvent.PowerFlowResultEvent import edu.ie3.simona.model.grid.Transformer3wModel.yij import edu.ie3.simona.model.grid.Transformer3wPowerFlowCase.{ @@ -66,12 +65,8 @@ private[grid] trait GridResultsSupport { sweepValueStore: SweepValueStore )(implicit timestamp: ZonedDateTime): PowerFlowResultEvent = { // no sanity check for duplicated uuid result data as we expect valid data at this point - implicit val sweepValueStoreData: Map[UUID, SweepValueStoreData] = + implicit val sweepValueStoreData: Map[UUID, StateData] = sweepValueStore.sweepData - .map(sweepValueStoreData => - sweepValueStoreData.nodeUuid -> sweepValueStoreData - ) - .toMap implicit val iNominal: ComparableQuantity[ElectricCurrent] = grid.mainRefSystem.nominalCurrent @@ -88,8 +83,9 @@ private[grid] trait GridResultsSupport { .filterNot { case (uuid, _) => transformerNodesToIgnore.contains(uuid) } - .values - .map(calcNodeResult(_, timestamp)), + .map { case (nodeUuid, stateData) => + calcNodeResult(nodeUuid, stateData, timestamp) + }, grid.gridComponents.switches.map(calcSwitchResult(_, timestamp)), buildLineResults(grid.gridComponents.lines), buildTransformer2wResults(grid.gridComponents.transformers), @@ -112,7 +108,7 @@ private[grid] trait GridResultsSupport { * a set of [[LineResult]] s */ private def buildLineResults(lines: Set[LineModel])(implicit - sweepValueStoreData: Map[UUID, SweepValueStoreData], + sweepValueStoreData: Map[UUID, StateData], iNominal: ComparableQuantity[ElectricCurrent], timestamp: ZonedDateTime ): Set[LineResult] = { @@ -124,8 +120,8 @@ private[grid] trait GridResultsSupport { Some( calcLineResult( lineModel, - nodeAStateData.stateData, - nodeBStateData.stateData, + nodeAStateData, + nodeBStateData, iNominal, timestamp ) @@ -159,7 +155,7 @@ private[grid] trait GridResultsSupport { */ private def buildTransformer2wResults(transformers: Set[TransformerModel])( implicit - sweepValueStoreData: Map[UUID, SweepValueStoreData], + sweepValueStoreData: Map[UUID, StateData], iNominal: ComparableQuantity[ElectricCurrent], timestamp: ZonedDateTime ): Set[Transformer2WResult] = { @@ -171,8 +167,8 @@ private[grid] trait GridResultsSupport { Some( calcTransformer2wResult( trafo2w, - hvNodeStateData.stateData, - lvNodeStateData.stateData, + hvNodeStateData, + lvNodeStateData, iNominal, timestamp ) @@ -206,7 +202,7 @@ private[grid] trait GridResultsSupport { */ def buildTransformer3wResults(transformers3w: Set[Transformer3wModel])( implicit - sweepValueStoreData: Map[UUID, SweepValueStoreData], + sweepValueStoreData: Map[UUID, StateData], iNominal: ComparableQuantity[ElectricCurrent], timestamp: ZonedDateTime ): Set[PartialTransformer3wResult] = transformers3w.flatMap { trafo3w => @@ -229,8 +225,8 @@ private[grid] trait GridResultsSupport { Some( calcTransformer3wResult( trafo3w, - upperNodeStateData.stateData, - internalNodeStateData.stateData, + upperNodeStateData, + internalNodeStateData, iNominal, timestamp ) @@ -248,28 +244,30 @@ private[grid] trait GridResultsSupport { } } - /** Creates an instance of [[NodeResult]] based on the provided - * [[SweepValueStoreData]] + /** Creates an instance of [[NodeResult]] based on the provided node UUID and + * node [[StateData]] * - * @param sweepValueStoreData - * the sweep value store with the node results + * @param nodeUuid + * the node uuid + * @param nodeStateData + * the node state data * @param timestamp * the timestamp of the result * @return * instance of [[NodeResult]] based on the provided data */ protected def calcNodeResult( - sweepValueStoreData: SweepValueStoreData, + nodeUuid: UUID, + nodeStateData: StateData, timestamp: ZonedDateTime ): NodeResult = { - val nodeStateData = sweepValueStoreData.stateData val vMag = nodeStateData.voltage.abs val vAng = asin(nodeStateData.voltage.imag / vMag).toDegrees new NodeResult( timestamp, - sweepValueStoreData.nodeUuid, + nodeUuid, Quantities.getQuantity(vMag, PowerSystemUnits.PU), Quantities.getQuantity(vAng, PowerSystemUnits.DEGREE_GEOM) ) diff --git a/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala b/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala index 649a9c13e6..06e3d1ff4d 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala @@ -171,7 +171,7 @@ trait PowerFlowSupport { * * @param receivedSlackValues * new slack voltages provided by the superior grid - * @param sweepDataValues + * @param sweepData * instance of [[SweepValueStore]] from the previous sweep * @param transformers2w * two winding transformer models in the grid under investigation @@ -185,45 +185,41 @@ trait PowerFlowSupport { */ protected def composeOperatingPointWithUpdatedSlackVoltages( receivedSlackValues: ReceivedSlackValues, - sweepDataValues: Vector[SweepValueStore.SweepValueStoreData], + sweepData: Map[UUID, StateData], transformers2w: Set[TransformerModel], transformers3w: Set[Transformer3wModel], gridMainRefSystem: RefSystem ): Array[PresetData] = { - sweepDataValues - .map(sweepValueStoreData => { - - val nodeStateData = sweepValueStoreData.stateData - val targetVoltage = if (nodeStateData.nodeType == NodeType.SL) { - val receivedSlackVoltage = receivedSlackValues.values - .map { case (_, slackVoltageMsg) => slackVoltageMsg } - .find(_.nodeUuid == sweepValueStoreData.nodeUuid) - .getOrElse( - throw new RuntimeException( - s"Unable to find node with uuid " + - s"${sweepValueStoreData.nodeUuid} in received slack voltage values!" - ) + sweepData.map { case (nodeUuid, stateData) => + val targetVoltage = if (stateData.nodeType == NodeType.SL) { + val receivedSlackVoltage = receivedSlackValues.values + .map { case (_, slackVoltageMsg) => slackVoltageMsg } + .find(_.nodeUuid == nodeUuid) + .getOrElse( + throw new RuntimeException( + s"Unable to find node with uuid " + + s"$nodeUuid in received slack voltage values!" ) - - transformVoltage( - receivedSlackVoltage, - sweepValueStoreData.nodeUuid, - transformers2w, - transformers3w, - gridMainRefSystem ) - } else - new Complex(1, 0) - // note: target voltage will be ignored for slack node if provided - PresetData( - nodeStateData.index, - nodeStateData.nodeType, - nodeStateData.power, - targetVoltage.abs + transformVoltage( + receivedSlackVoltage, + nodeUuid, + transformers2w, + transformers3w, + gridMainRefSystem ) - }) - .toArray + } else + new Complex(1, 0) + + // note: target voltage will be ignored for slack node if provided + PresetData( + stateData.index, + stateData.nodeType, + stateData.power, + targetVoltage.abs + ) + }.toArray } diff --git a/src/main/scala/edu/ie3/simona/agent/grid/SweepValueStore.scala b/src/main/scala/edu/ie3/simona/agent/grid/SweepValueStore.scala index 4616c70d3f..62735190fa 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/SweepValueStore.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/SweepValueStore.scala @@ -10,7 +10,6 @@ import java.util.UUID import edu.ie3.powerflow.model.NodeData.StateData import edu.ie3.powerflow.model.PowerFlowResult.SuccessFullPowerFlowResult.ValidNewtonRaphsonPFResult -import edu.ie3.simona.agent.grid.SweepValueStore.SweepValueStoreData import edu.ie3.simona.exceptions.agent.DBFSAlgorithmException import edu.ie3.simona.model.grid.NodeModel @@ -18,26 +17,13 @@ import edu.ie3.simona.model.grid.NodeModel * [[DBFSAlgorithm]] to be used by the next sweep. * * @param sweepData - * a collection of [[SweepValueStoreData]] + * a map from node UUID to [[StateData]] */ final case class SweepValueStore private ( - sweepData: Vector[SweepValueStoreData] + sweepData: Map[UUID, StateData] ) -case object SweepValueStore { - - /** Data object that contains node specific data of one sweep of the - * [[DBFSAlgorithm]] - * - * @param stateData - * power flow state data - * @param nodeUuid - * node uuid of the sweep data - */ - final case class SweepValueStoreData private ( - nodeUuid: UUID, - stateData: StateData - ) +object SweepValueStore { /** Creates an empty [[SweepValueStore]] from on a valid power flow result * @@ -49,33 +35,30 @@ case object SweepValueStore { * mapping of node uuids of the grid to their index in the admittance * matrix * @return - * instance of [[SweepValueStore]] with all data to be saved as - * [[SweepValueStoreData]] + * instance of [[SweepValueStore]] with all state data */ def apply( validResult: ValidNewtonRaphsonPFResult, nodes: Set[NodeModel], nodeUuidToIndexMap: Map[UUID, Int] ): SweepValueStore = { - val sweepDataValues = nodes.foldLeft(Vector.empty[SweepValueStoreData])( - (valueStoreDataElements, node) => { - val uuid = node.uuid - val id = node.id - val nodeIdxOpt = nodeUuidToIndexMap.get(uuid) - val stateData = validResult.nodeData - .find(stateData => - nodeIdxOpt - .contains(stateData.index) - ) - .getOrElse( - throw new DBFSAlgorithmException( - s"Cannot find power flow result data for node $id [$uuid]!" - ) + val sweepDataValues = nodes.map { node => + val uuid = node.uuid + val id = node.id + val nodeIdxOpt = nodeUuidToIndexMap.get(uuid) + val stateData = validResult.nodeData + .find(stateData => + nodeIdxOpt + .contains(stateData.index) + ) + .getOrElse( + throw new DBFSAlgorithmException( + s"Cannot find power flow result data for node $id [$uuid]!" ) + ) - valueStoreDataElements :+ SweepValueStoreData(uuid, stateData) - } - ) + uuid -> stateData + }.toMap new SweepValueStore(sweepDataValues) } 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 65b36b9352..dbd2bec469 100644 --- a/src/test/scala/edu/ie3/simona/agent/grid/GridResultsSupportSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/grid/GridResultsSupportSpec.scala @@ -68,18 +68,15 @@ class GridResultsSupportSpec "calculating node results" should { "calculate node results correctly" in { val nodeUuid = UUID.randomUUID - val sweepValueStoreData = SweepValueStore.SweepValueStoreData( - nodeUuid, - new StateData( - 0, - NodeType.PQ, - Complex(0.9583756183209947, -0.04673985022513541), - Complex(0.006466666857417822, 2.7286658176028933e-15) - ) + val nodeStateData = new StateData( + 0, + NodeType.PQ, + Complex(0.9583756183209947, -0.04673985022513541), + Complex(0.006466666857417822, 2.7286658176028933e-15) ) val nodeResult = - calcNodeResult(sweepValueStoreData, defaultSimulationStart) + calcNodeResult(nodeUuid, nodeStateData, defaultSimulationStart) val expectedNodeResult = new NodeResult( defaultSimulationStart, nodeUuid, From 06be2d81a67417774fef338b608a153eb97ae201 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Fri, 8 Jul 2022 22:52:36 +0200 Subject: [PATCH 23/36] Revert "Refactoring SweepValueStore to use a map instead of a vector" This reverts commit a42da7c35589b710572775f43bc98e50af4d1aa8. --- .../ie3/simona/agent/grid/DBFSAlgorithm.scala | 57 ++++++++---------- .../agent/grid/GridResultsSupport.scala | 46 +++++++------- .../simona/agent/grid/PowerFlowSupport.scala | 60 ++++++++++--------- .../simona/agent/grid/SweepValueStore.scala | 55 +++++++++++------ .../agent/grid/GridResultsSupportSpec.scala | 15 +++-- 5 files changed, 127 insertions(+), 106 deletions(-) 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 a0c015ff8f..82bf9b8a10 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/DBFSAlgorithm.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/DBFSAlgorithm.scala @@ -187,13 +187,15 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { .get(currentSweepNo - 1) .map((_, currentSweepNo - 1)) }).map { case (result, sweepNo) => - result.sweepData.get(nodeUuid) match { - case Some(stateData) => + // get nodeUUID + result.sweepData.find(_.nodeUuid == nodeUuid) match { + case Some(sweepValueStoreData) => + val slackVoltageInPu = sweepValueStoreData.stateData.voltage val mainRefSystem = gridAgentBaseData.gridEnv.gridModel.mainRefSystem ( - mainRefSystem.vInSi(stateData.voltage.real), - mainRefSystem.vInSi(stateData.voltage.imag) + mainRefSystem.vInSi(slackVoltageInPu.real), + mainRefSystem.vInSi(slackVoltageInPu.imag) ) case None => throw new DBFSAlgorithmException( @@ -808,39 +810,31 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { } else { log.debug("Sweep value store is not empty. Check for deviation ...") - val previousSweepData = updatedGridAgentBaseData.sweepValueStores - .getOrElse( - currentSweepNo - 1, - throw new DBFSAlgorithmException( - s"No data for previous sweep with no ${currentSweepNo - 1} available!" - ) - ) - .sweepData - val currentSweepData = updatedGridAgentBaseData.sweepValueStores - .getOrElse( - currentSweepNo, - throw new DBFSAlgorithmException( - s"No data for current sweep with no $currentSweepNo available!" - ) - ) - .sweepData - - // define a set order of nodes, so that the order of previous and current state data matches - val nodeUuids = previousSweepData.keys.toSeq - // calculate deviation vector for all nodes val previousSweepNodePower: DenseVector[Complex] = DenseVector( - nodeUuids - .flatMap(previousSweepData.get) - .map(_.power) + updatedGridAgentBaseData.sweepValueStores + .getOrElse( + currentSweepNo - 1, + throw new DBFSAlgorithmException( + s"No data for previous sweep with no ${currentSweepNo - 1} available!" + ) + ) + .sweepData + .map(_.stateData.power) .toArray ) val currentSweepNodePower: DenseVector[Complex] = DenseVector( - nodeUuids - .flatMap(currentSweepData.get) - .map(_.power) + updatedGridAgentBaseData.sweepValueStores + .getOrElse( + currentSweepNo, + throw new DBFSAlgorithmException( + s"No data for current sweep with no $currentSweepNo available!" + ) + ) + .sweepData + .map(_.stateData.power) .toArray ) @@ -1078,12 +1072,13 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { case Some(sweepValueStore) => val (eInSi, fInSi) = refSystem.vInSi( sweepValueStore.sweepData + .find(_.nodeUuid == nodeUuid) .getOrElse( - nodeUuid, throw new DBFSAlgorithmException( s"Provided Sweep value store contains no data for node with id $nodeUuid" ) ) + .stateData .voltage ) ( 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 9879327175..7a0440bfea 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/GridResultsSupport.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/GridResultsSupport.scala @@ -19,6 +19,7 @@ import edu.ie3.datamodel.models.result.connector.{ } import edu.ie3.powerflow.model.NodeData.StateData import edu.ie3.simona.agent.grid.GridResultsSupport.PartialTransformer3wResult +import edu.ie3.simona.agent.grid.SweepValueStore.SweepValueStoreData import edu.ie3.simona.event.ResultEvent.PowerFlowResultEvent import edu.ie3.simona.model.grid.Transformer3wModel.yij import edu.ie3.simona.model.grid.Transformer3wPowerFlowCase.{ @@ -65,8 +66,12 @@ private[grid] trait GridResultsSupport { sweepValueStore: SweepValueStore )(implicit timestamp: ZonedDateTime): PowerFlowResultEvent = { // no sanity check for duplicated uuid result data as we expect valid data at this point - implicit val sweepValueStoreData: Map[UUID, StateData] = + implicit val sweepValueStoreData: Map[UUID, SweepValueStoreData] = sweepValueStore.sweepData + .map(sweepValueStoreData => + sweepValueStoreData.nodeUuid -> sweepValueStoreData + ) + .toMap implicit val iNominal: ComparableQuantity[ElectricCurrent] = grid.mainRefSystem.nominalCurrent @@ -83,9 +88,8 @@ private[grid] trait GridResultsSupport { .filterNot { case (uuid, _) => transformerNodesToIgnore.contains(uuid) } - .map { case (nodeUuid, stateData) => - calcNodeResult(nodeUuid, stateData, timestamp) - }, + .values + .map(calcNodeResult(_, timestamp)), grid.gridComponents.switches.map(calcSwitchResult(_, timestamp)), buildLineResults(grid.gridComponents.lines), buildTransformer2wResults(grid.gridComponents.transformers), @@ -108,7 +112,7 @@ private[grid] trait GridResultsSupport { * a set of [[LineResult]] s */ private def buildLineResults(lines: Set[LineModel])(implicit - sweepValueStoreData: Map[UUID, StateData], + sweepValueStoreData: Map[UUID, SweepValueStoreData], iNominal: ComparableQuantity[ElectricCurrent], timestamp: ZonedDateTime ): Set[LineResult] = { @@ -120,8 +124,8 @@ private[grid] trait GridResultsSupport { Some( calcLineResult( lineModel, - nodeAStateData, - nodeBStateData, + nodeAStateData.stateData, + nodeBStateData.stateData, iNominal, timestamp ) @@ -155,7 +159,7 @@ private[grid] trait GridResultsSupport { */ private def buildTransformer2wResults(transformers: Set[TransformerModel])( implicit - sweepValueStoreData: Map[UUID, StateData], + sweepValueStoreData: Map[UUID, SweepValueStoreData], iNominal: ComparableQuantity[ElectricCurrent], timestamp: ZonedDateTime ): Set[Transformer2WResult] = { @@ -167,8 +171,8 @@ private[grid] trait GridResultsSupport { Some( calcTransformer2wResult( trafo2w, - hvNodeStateData, - lvNodeStateData, + hvNodeStateData.stateData, + lvNodeStateData.stateData, iNominal, timestamp ) @@ -202,7 +206,7 @@ private[grid] trait GridResultsSupport { */ def buildTransformer3wResults(transformers3w: Set[Transformer3wModel])( implicit - sweepValueStoreData: Map[UUID, StateData], + sweepValueStoreData: Map[UUID, SweepValueStoreData], iNominal: ComparableQuantity[ElectricCurrent], timestamp: ZonedDateTime ): Set[PartialTransformer3wResult] = transformers3w.flatMap { trafo3w => @@ -225,8 +229,8 @@ private[grid] trait GridResultsSupport { Some( calcTransformer3wResult( trafo3w, - upperNodeStateData, - internalNodeStateData, + upperNodeStateData.stateData, + internalNodeStateData.stateData, iNominal, timestamp ) @@ -244,30 +248,28 @@ private[grid] trait GridResultsSupport { } } - /** Creates an instance of [[NodeResult]] based on the provided node UUID and - * node [[StateData]] + /** Creates an instance of [[NodeResult]] based on the provided + * [[SweepValueStoreData]] * - * @param nodeUuid - * the node uuid - * @param nodeStateData - * the node state data + * @param sweepValueStoreData + * the sweep value store with the node results * @param timestamp * the timestamp of the result * @return * instance of [[NodeResult]] based on the provided data */ protected def calcNodeResult( - nodeUuid: UUID, - nodeStateData: StateData, + sweepValueStoreData: SweepValueStoreData, timestamp: ZonedDateTime ): NodeResult = { + val nodeStateData = sweepValueStoreData.stateData val vMag = nodeStateData.voltage.abs val vAng = asin(nodeStateData.voltage.imag / vMag).toDegrees new NodeResult( timestamp, - nodeUuid, + sweepValueStoreData.nodeUuid, Quantities.getQuantity(vMag, PowerSystemUnits.PU), Quantities.getQuantity(vAng, PowerSystemUnits.DEGREE_GEOM) ) diff --git a/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala b/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala index 06e3d1ff4d..649a9c13e6 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala @@ -171,7 +171,7 @@ trait PowerFlowSupport { * * @param receivedSlackValues * new slack voltages provided by the superior grid - * @param sweepData + * @param sweepDataValues * instance of [[SweepValueStore]] from the previous sweep * @param transformers2w * two winding transformer models in the grid under investigation @@ -185,41 +185,45 @@ trait PowerFlowSupport { */ protected def composeOperatingPointWithUpdatedSlackVoltages( receivedSlackValues: ReceivedSlackValues, - sweepData: Map[UUID, StateData], + sweepDataValues: Vector[SweepValueStore.SweepValueStoreData], transformers2w: Set[TransformerModel], transformers3w: Set[Transformer3wModel], gridMainRefSystem: RefSystem ): Array[PresetData] = { - sweepData.map { case (nodeUuid, stateData) => - val targetVoltage = if (stateData.nodeType == NodeType.SL) { - val receivedSlackVoltage = receivedSlackValues.values - .map { case (_, slackVoltageMsg) => slackVoltageMsg } - .find(_.nodeUuid == nodeUuid) - .getOrElse( - throw new RuntimeException( - s"Unable to find node with uuid " + - s"$nodeUuid in received slack voltage values!" + sweepDataValues + .map(sweepValueStoreData => { + + val nodeStateData = sweepValueStoreData.stateData + val targetVoltage = if (nodeStateData.nodeType == NodeType.SL) { + val receivedSlackVoltage = receivedSlackValues.values + .map { case (_, slackVoltageMsg) => slackVoltageMsg } + .find(_.nodeUuid == sweepValueStoreData.nodeUuid) + .getOrElse( + throw new RuntimeException( + s"Unable to find node with uuid " + + s"${sweepValueStoreData.nodeUuid} in received slack voltage values!" + ) ) + + transformVoltage( + receivedSlackVoltage, + sweepValueStoreData.nodeUuid, + transformers2w, + transformers3w, + gridMainRefSystem ) + } else + new Complex(1, 0) - transformVoltage( - receivedSlackVoltage, - nodeUuid, - transformers2w, - transformers3w, - gridMainRefSystem + // note: target voltage will be ignored for slack node if provided + PresetData( + nodeStateData.index, + nodeStateData.nodeType, + nodeStateData.power, + targetVoltage.abs ) - } else - new Complex(1, 0) - - // note: target voltage will be ignored for slack node if provided - PresetData( - stateData.index, - stateData.nodeType, - stateData.power, - targetVoltage.abs - ) - }.toArray + }) + .toArray } diff --git a/src/main/scala/edu/ie3/simona/agent/grid/SweepValueStore.scala b/src/main/scala/edu/ie3/simona/agent/grid/SweepValueStore.scala index 62735190fa..4616c70d3f 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/SweepValueStore.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/SweepValueStore.scala @@ -10,6 +10,7 @@ import java.util.UUID import edu.ie3.powerflow.model.NodeData.StateData import edu.ie3.powerflow.model.PowerFlowResult.SuccessFullPowerFlowResult.ValidNewtonRaphsonPFResult +import edu.ie3.simona.agent.grid.SweepValueStore.SweepValueStoreData import edu.ie3.simona.exceptions.agent.DBFSAlgorithmException import edu.ie3.simona.model.grid.NodeModel @@ -17,13 +18,26 @@ import edu.ie3.simona.model.grid.NodeModel * [[DBFSAlgorithm]] to be used by the next sweep. * * @param sweepData - * a map from node UUID to [[StateData]] + * a collection of [[SweepValueStoreData]] */ final case class SweepValueStore private ( - sweepData: Map[UUID, StateData] + sweepData: Vector[SweepValueStoreData] ) -object SweepValueStore { +case object SweepValueStore { + + /** Data object that contains node specific data of one sweep of the + * [[DBFSAlgorithm]] + * + * @param stateData + * power flow state data + * @param nodeUuid + * node uuid of the sweep data + */ + final case class SweepValueStoreData private ( + nodeUuid: UUID, + stateData: StateData + ) /** Creates an empty [[SweepValueStore]] from on a valid power flow result * @@ -35,30 +49,33 @@ object SweepValueStore { * mapping of node uuids of the grid to their index in the admittance * matrix * @return - * instance of [[SweepValueStore]] with all state data + * instance of [[SweepValueStore]] with all data to be saved as + * [[SweepValueStoreData]] */ def apply( validResult: ValidNewtonRaphsonPFResult, nodes: Set[NodeModel], nodeUuidToIndexMap: Map[UUID, Int] ): SweepValueStore = { - val sweepDataValues = nodes.map { node => - val uuid = node.uuid - val id = node.id - val nodeIdxOpt = nodeUuidToIndexMap.get(uuid) - val stateData = validResult.nodeData - .find(stateData => - nodeIdxOpt - .contains(stateData.index) - ) - .getOrElse( - throw new DBFSAlgorithmException( - s"Cannot find power flow result data for node $id [$uuid]!" + val sweepDataValues = nodes.foldLeft(Vector.empty[SweepValueStoreData])( + (valueStoreDataElements, node) => { + val uuid = node.uuid + val id = node.id + val nodeIdxOpt = nodeUuidToIndexMap.get(uuid) + val stateData = validResult.nodeData + .find(stateData => + nodeIdxOpt + .contains(stateData.index) + ) + .getOrElse( + throw new DBFSAlgorithmException( + s"Cannot find power flow result data for node $id [$uuid]!" + ) ) - ) - uuid -> stateData - }.toMap + valueStoreDataElements :+ SweepValueStoreData(uuid, stateData) + } + ) new SweepValueStore(sweepDataValues) } 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 dbd2bec469..65b36b9352 100644 --- a/src/test/scala/edu/ie3/simona/agent/grid/GridResultsSupportSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/grid/GridResultsSupportSpec.scala @@ -68,15 +68,18 @@ class GridResultsSupportSpec "calculating node results" should { "calculate node results correctly" in { val nodeUuid = UUID.randomUUID - val nodeStateData = new StateData( - 0, - NodeType.PQ, - Complex(0.9583756183209947, -0.04673985022513541), - Complex(0.006466666857417822, 2.7286658176028933e-15) + val sweepValueStoreData = SweepValueStore.SweepValueStoreData( + nodeUuid, + new StateData( + 0, + NodeType.PQ, + Complex(0.9583756183209947, -0.04673985022513541), + Complex(0.006466666857417822, 2.7286658176028933e-15) + ) ) val nodeResult = - calcNodeResult(nodeUuid, nodeStateData, defaultSimulationStart) + calcNodeResult(sweepValueStoreData, defaultSimulationStart) val expectedNodeResult = new NodeResult( defaultSimulationStart, nodeUuid, From edbc579cfcc9efd31ce9e280dc3860036ccabd37 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Sun, 10 Jul 2022 01:10:50 +0200 Subject: [PATCH 24/36] Adapting slack voltage communication to only send one message per subgrid Enhancing DBFS cen grid test to also test a double connection to the superior subgrid --- .../ie3/simona/agent/grid/DBFSAlgorithm.scala | 69 +-- .../ie3/simona/agent/grid/GridAgentData.scala | 22 +- .../simona/agent/grid/PowerFlowSupport.scala | 5 +- .../agent/grid/ReceivedValuesStore.scala | 4 +- .../edu/ie3/simona/model/grid/GridModel.scala | 42 +- .../edu/ie3/simona/model/grid/NodeModel.scala | 4 +- .../ontology/messages/VoltageMessage.scala | 42 +- .../agent/grid/DBFSAlgorithmCenGridSpec.scala | 441 ++++++++++-------- .../test/common/model/grid/DbfsTestGrid.scala | 37 +- 9 files changed, 390 insertions(+), 276 deletions(-) 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 82bf9b8a10..6726932fa1 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/DBFSAlgorithm.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/DBFSAlgorithm.scala @@ -39,6 +39,7 @@ import edu.ie3.simona.ontology.messages.SchedulerMessage.{ ScheduleTriggerMessage, TriggerWithIdMessage } +import edu.ie3.simona.ontology.messages.VoltageMessage.ProvideSlackVoltageMessage.ExchangeVoltage import edu.ie3.simona.ontology.messages.VoltageMessage.{ ProvideSlackVoltageMessage, RequestSlackVoltageMessage @@ -158,19 +159,19 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { // if we receive a request for slack voltages from our inferior grids we want to answer it case Event( - RequestSlackVoltageMessage(currentSweepNo, nodeUuid), + RequestSlackVoltageMessage(currentSweepNo, nodeUuids), gridAgentBaseData: GridAgentBaseData ) => log.debug( - s"Received Slack Voltages request from {} for node {} and sweepNo: {}", + s"Received Slack Voltages request from {} for nodes {} and sweepNo: {}", sender(), - nodeUuid, + nodeUuids, gridAgentBaseData.currentSweepNo ) - // we either have voltages ready calculated (not the first sweep) or we don't have them here - // -> return calculated value or target voltage as physical value - val (slackE, slackF) = + nodeUuids.map { nodeUuid => + // we either have voltages ready calculated (not the first sweep) or we don't have them here + // -> return calculated value or target voltage as physical value (gridAgentBaseData.sweepValueStores.get(currentSweepNo) match { case Some(result) => Some(result, currentSweepNo) @@ -179,8 +180,8 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { // the next sweep, as it triggers calculations for the next sweep or b) at all other // (non last downstream grid agents) in sweep 0 log.debug( - "Unable to find slack voltage for node '{}' in sweep '{}'. Try to get voltage of previous sweep.", - nodeUuid, + "Unable to find slack voltage for nodes '{}' in sweep '{}'. Try to get voltage of previous sweep.", + nodeUuids, currentSweepNo ) gridAgentBaseData.sweepValueStores @@ -234,22 +235,25 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { vSlack, Quantities.getQuantity(0, refSystemVUnit) ) - } - - log.debug( - s"Provide {} to {} for node {} and sweepNo: {}", - s"$slackE, $slackF", - sender(), - nodeUuid, - gridAgentBaseData.currentSweepNo - ) + } match { + case (slackE, slackF) => + log.debug( + s"Provide {} to {} for node {} and sweepNo: {}", + s"$slackE, $slackF", + sender(), + nodeUuid, + gridAgentBaseData.currentSweepNo + ) - stay() replying ProvideSlackVoltageMessage( - currentSweepNo, - nodeUuid, - slackE, - slackF - ) + ExchangeVoltage(nodeUuid, slackE, slackF) + } + } match { + case exchangeVoltages => + stay() replying ProvideSlackVoltageMessage( + currentSweepNo, + exchangeVoltages + ) + } // receive grid power values message request from superior grids // / before power flow calc for this sweep we either have to stash() the message to answer it later (in current sweep) @@ -1196,18 +1200,21 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { s"asking superior grids for slack voltage values: {}", superiorGridGates ) + Option.when(superiorGridGates.nonEmpty) { Future .sequence( - superiorGridGates.map(superiorGridGate => { - val superiorGridAgent = subGridGateToActorRef(superiorGridGate) - (superiorGridAgent ? RequestSlackVoltageMessage( - currentSweepNo, - superiorGridGate.getSuperiorNode.getUuid - )).map { case providedSlackValues: ProvideSlackVoltageMessage => - (superiorGridAgent, providedSlackValues) + superiorGridGates + .groupBy(subGridGateToActorRef(_)) + .map { case (superiorGridAgent, gridGates) => + (superiorGridAgent ? RequestSlackVoltageMessage( + currentSweepNo, + gridGates.map(_.getSuperiorNode.getUuid) + )).map { case providedSlackValues: ProvideSlackVoltageMessage => + (superiorGridAgent, providedSlackValues) + } } - }) + .toVector ) .map(ReceivedSlackValues) .pipeTo(self) 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 5af13166c7..58bebac08b 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/GridAgentData.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/GridAgentData.scala @@ -383,30 +383,24 @@ object GridAgentData { receivedSlackValues: ReceivedSlackValues ): GridAgentBaseData = { val updatedNodeToReceivedSlackVoltageValuesMap = - receivedSlackValues.values.foldLeft( - receivedValueStore.nodeToReceivedSlackVoltage - ) { - case ( - nodeToSlackVoltageUpdated, - (senderRef, slackValues) - ) => - val nodeUuid: UUID = slackValues.nodeUuid - + receivedSlackValues.values.flatMap { case (senderRef, slackValues) => + slackValues.nodalSlackVoltages.map { exchangeVoltage => receivedValueStore.nodeToReceivedSlackVoltage - .get(nodeUuid) match { + .get(exchangeVoltage.nodeUuid) match { case Some(None) => /* Slack voltage is expected and not yet received */ - nodeToSlackVoltageUpdated + (nodeUuid -> Some(slackValues)) + exchangeVoltage.nodeUuid -> Some(exchangeVoltage) case Some(Some(_)) => throw new RuntimeException( - s"Already received slack value for node $nodeUuid!" + s"Already received slack value for node ${exchangeVoltage.nodeUuid}!" ) case None => throw new RuntimeException( - s"Received slack value for node $nodeUuid from $senderRef which is not in my slack values nodes list!" + s"Received slack value for node ${exchangeVoltage.nodeUuid} from $senderRef which is not in my slack values nodes list!" ) } - } + } + }.toMap this.copy( receivedValueStore = receivedValueStore.copy( nodeToReceivedSlackVoltage = diff --git a/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala b/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala index 649a9c13e6..fb7d9b6a14 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala @@ -24,7 +24,7 @@ import edu.ie3.simona.model.grid.{ TransformerModel } import edu.ie3.simona.ontology.messages.PowerMessage.ProvidePowerMessage -import edu.ie3.simona.ontology.messages.VoltageMessage.ProvideSlackVoltageMessage +import edu.ie3.simona.ontology.messages.VoltageMessage.ProvideSlackVoltageMessage.ExchangeVoltage import edu.ie3.util.quantities.PowerSystemUnits import tech.units.indriya.ComparableQuantity @@ -197,6 +197,7 @@ trait PowerFlowSupport { val targetVoltage = if (nodeStateData.nodeType == NodeType.SL) { val receivedSlackVoltage = receivedSlackValues.values .map { case (_, slackVoltageMsg) => slackVoltageMsg } + .flatMap(_.nodalSlackVoltages) .find(_.nodeUuid == sweepValueStoreData.nodeUuid) .getOrElse( throw new RuntimeException( @@ -288,7 +289,7 @@ trait PowerFlowSupport { * Complex nodal voltage to use */ private def transformVoltage( - receivedSlackVoltage: ProvideSlackVoltageMessage, + receivedSlackVoltage: ExchangeVoltage, nodeUuid: UUID, transformers2w: Set[TransformerModel], transformers3w: Set[Transformer3wModel], diff --git a/src/main/scala/edu/ie3/simona/agent/grid/ReceivedValuesStore.scala b/src/main/scala/edu/ie3/simona/agent/grid/ReceivedValuesStore.scala index e27589053d..c8de3f3848 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/ReceivedValuesStore.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/ReceivedValuesStore.scala @@ -16,7 +16,7 @@ import edu.ie3.simona.ontology.messages.PowerMessage.{ PowerResponseMessage, ProvidePowerMessage } -import edu.ie3.simona.ontology.messages.VoltageMessage.ProvideSlackVoltageMessage +import edu.ie3.simona.ontology.messages.VoltageMessage.ProvideSlackVoltageMessage.ExchangeVoltage import java.util.UUID @@ -49,7 +49,7 @@ object ReceivedValuesStore { type NodeToReceivedPower = Map[UUID, Map[ActorRef, Option[PowerResponseMessage]]] type NodeToReceivedSlackVoltage = - Map[UUID, Option[ProvideSlackVoltageMessage]] + Map[UUID, Option[ExchangeVoltage]] /** Get an empty, ready to be used instance of [[ReceivedValuesStore]] * containing an `empty` mapping of [[NodeToReceivedPower]] and 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 865f123c55..8c4ca14de1 100644 --- a/src/main/scala/edu/ie3/simona/model/grid/GridModel.scala +++ b/src/main/scala/edu/ie3/simona/model/grid/GridModel.scala @@ -497,25 +497,21 @@ case object GridModel { // build // / nodes val nodes: Set[NodeModel] = - subGridContainer.getRawGrid.getNodes.asScala - .collect { case nodeInput: NodeInput => - NodeModel(nodeInput, startDate, endDate) - } - .to(collection.immutable.Set) + subGridContainer.getRawGrid.getNodes.asScala.map { nodeInput => + NodeModel(nodeInput, startDate, endDate) + }.toSet // / lines val lines: Set[LineModel] = - subGridContainer.getRawGrid.getLines.asScala - .collect { case lineInput: LineInput => - getConnectedNodes(lineInput, nodes) - LineModel(lineInput, refSystem, startDate, endDate) - } - .to(collection.immutable.Set) + subGridContainer.getRawGrid.getLines.asScala.map { lineInput => + getConnectedNodes(lineInput, nodes) + LineModel(lineInput, refSystem, startDate, endDate) + }.toSet // / transformers val transformers: Set[TransformerModel] = - subGridContainer.getRawGrid.getTransformer2Ws.asScala - .collect { case transformer2wInput: Transformer2WInput => + subGridContainer.getRawGrid.getTransformer2Ws.asScala.map { + transformer2wInput => val (nodeA, _) = getConnectedNodes(transformer2wInput, nodes) if (nodeA.isSlack) { TransformerModel( @@ -529,13 +525,12 @@ case object GridModel { s"NodeA: ${transformer2wInput.getNodeA.getUuid} for transformer ${transformer2wInput.getUuid} is not set as slack. This has to be corrected first!" ) } - } - .to(collection.immutable.Set) + }.toSet // / transformers3w val transformer3ws: Set[Transformer3wModel] = - subGridContainer.getRawGrid.getTransformer3Ws.asScala - .collect { case transformer3wInput: Transformer3WInput => + subGridContainer.getRawGrid.getTransformer3Ws.asScala.map { + transformer3wInput => getConnectedNodes(transformer3wInput, nodes) Transformer3wModel( transformer3wInput, @@ -544,8 +539,7 @@ case object GridModel { startDate, endDate ) - } - .to(collection.immutable.Set) + }.toSet /* Transformers are shipped as full models, therefore also containing two nodes, that do not belong in here. * Odd those nodes out. */ @@ -564,12 +558,10 @@ case object GridModel { // / switches val switches: Set[SwitchModel] = - subGridContainer.getRawGrid.getSwitches.asScala - .collect { case switchInput: SwitchInput => - getConnectedNodes(switchInput, nodes) - SwitchModel(switchInput, startDate, endDate) - } - .to(collection.immutable.Set) + subGridContainer.getRawGrid.getSwitches.asScala.map { switchInput => + getConnectedNodes(switchInput, nodes) + SwitchModel(switchInput, startDate, endDate) + }.toSet // build val gridComponents = 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 ff0d24f7a0..377ddf0424 100644 --- a/src/main/scala/edu/ie3/simona/model/grid/NodeModel.scala +++ b/src/main/scala/edu/ie3/simona/model/grid/NodeModel.scala @@ -14,8 +14,8 @@ import edu.ie3.datamodel.models.voltagelevels.VoltageLevel import edu.ie3.simona.model.SystemComponent import edu.ie3.simona.util.SimonaConstants import edu.ie3.util.scala.OperationInterval +import tech.units.indriya.ComparableQuantity -import javax.measure.Quantity import javax.measure.quantity.Dimensionless /** This model represents an electric node @@ -38,7 +38,7 @@ final case class NodeModel( id: String, operationInterval: OperationInterval, isSlack: Boolean, - vTarget: Quantity[Dimensionless], + vTarget: ComparableQuantity[Dimensionless], voltLvl: VoltageLevel ) extends SystemComponent( uuid, diff --git a/src/main/scala/edu/ie3/simona/ontology/messages/VoltageMessage.scala b/src/main/scala/edu/ie3/simona/ontology/messages/VoltageMessage.scala index b97c383d3b..c807090a4d 100644 --- a/src/main/scala/edu/ie3/simona/ontology/messages/VoltageMessage.scala +++ b/src/main/scala/edu/ie3/simona/ontology/messages/VoltageMessage.scala @@ -6,9 +6,10 @@ package edu.ie3.simona.ontology.messages -import java.util.UUID +import edu.ie3.simona.ontology.messages.VoltageMessage.ProvideSlackVoltageMessage.ExchangeVoltage +import tech.units.indriya.ComparableQuantity -import javax.measure.Quantity +import java.util.UUID import javax.measure.quantity.ElectricPotential sealed trait VoltageMessage @@ -18,16 +19,45 @@ sealed trait VoltageMessage */ object VoltageMessage { + /** Request complex voltage at the nodes that the superior sub grid shares + * with the sender's sub grid + * @param currentSweepNo + * The current sweep + * @param nodeUuids + * The UUIDs of the nodes that are bordering the sender's grid + */ final case class RequestSlackVoltageMessage( currentSweepNo: Int, - nodeUuid: UUID + nodeUuids: Seq[UUID] ) extends VoltageMessage + /** Provide complex voltage at the nodes that the sender's sub grid shares + * with the inferior sub grid, as a reply to a + * [[RequestSlackVoltageMessage]]. + * @param nodalSlackVoltages + * The complex voltages of the shared nodes + */ final case class ProvideSlackVoltageMessage( currentSweepNo: Int, - nodeUuid: UUID, - e: Quantity[ElectricPotential], - f: Quantity[ElectricPotential] + nodalSlackVoltages: Seq[ExchangeVoltage] ) extends VoltageMessage + object ProvideSlackVoltageMessage { + + /** Defining the exchanged voltage at one interconnection point + * + * @param nodeUuid + * Unique identifier of the node for which complex voltage is shared + * @param e + * Real part of the slack voltage + * @param f + * Imaginary part of the slack voltage + */ + final case class ExchangeVoltage( + nodeUuid: UUID, + e: ComparableQuantity[ElectricPotential], + f: ComparableQuantity[ElectricPotential] + ) + } + } 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 385cfedf0e..6d61ef4ad0 100644 --- a/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmCenGridSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmCenGridSpec.scala @@ -12,7 +12,10 @@ import akka.testkit.{ImplicitSender, TestProbe} import akka.util.Timeout import com.typesafe.config.ConfigFactory import edu.ie3.simona.agent.EnvironmentRefs -import edu.ie3.simona.agent.grid.DBFSAlgorithmCenGridSpec.GAActorAndModel +import edu.ie3.simona.agent.grid.DBFSAlgorithmCenGridSpec.{ + InferiorGA, + SuperiorGA +} import edu.ie3.simona.agent.grid.GridAgentData.GridAgentInitData import edu.ie3.simona.agent.state.GridAgentState.SimulateGrid import edu.ie3.simona.model.grid.RefSystem @@ -26,6 +29,7 @@ import edu.ie3.simona.ontology.messages.SchedulerMessage.{ ScheduleTriggerMessage, TriggerWithIdMessage } +import edu.ie3.simona.ontology.messages.VoltageMessage.ProvideSlackVoltageMessage.ExchangeVoltage import edu.ie3.simona.ontology.messages.VoltageMessage.{ ProvideSlackVoltageMessage, RequestSlackVoltageMessage @@ -72,21 +76,22 @@ class DBFSAlgorithmCenGridSpec with ImplicitSender with DbfsTestGrid { - private val floatPrecision: Double = 0.00000000001 - private val scheduler = TestProbe("scheduler") private val primaryService = TestProbe("primaryService") private val weatherService = TestProbe("weatherService") - private val superiorGridAgent = TestProbe("superiorGridAgent_1000") + private val superiorGridAgent = SuperiorGA( + TestProbe("superiorGridAgent_1000"), + Seq(supNodeA.getUuid, supNodeB.getUuid) + ) private val inferiorGrid11 = - GAActorAndModel(TestProbe("inferiorGridAgent_11"), Seq(node1.getUuid)) + InferiorGA(TestProbe("inferiorGridAgent_11"), Seq(node1.getUuid)) private val inferiorGrid12 = - GAActorAndModel(TestProbe("inferiorGridAgent_12"), Seq(node2.getUuid)) + InferiorGA(TestProbe("inferiorGridAgent_12"), Seq(node2.getUuid)) - private val inferiorGrid13 = GAActorAndModel( + private val inferiorGrid13 = InferiorGA( TestProbe("inferiorGridAgent_13"), Seq(node3a.getUuid, node3b.getUuid) ) @@ -134,7 +139,7 @@ class DBFSAlgorithmCenGridSpec // send init data to agent and expect a CompletionMessage implicit val timeout: Timeout = Timeout(1, TimeUnit.SECONDS) - val expectedCompletionMessage = + val actualInitReply = Await.result( centerGridAgent ? TriggerWithIdMessage( InitializeGridAgentTrigger(gridAgentInitData), @@ -144,7 +149,7 @@ class DBFSAlgorithmCenGridSpec timeout.duration ) - expectedCompletionMessage shouldBe CompletionMessage( + actualInitReply shouldBe CompletionMessage( 0, Some( Vector( @@ -189,8 +194,6 @@ class DBFSAlgorithmCenGridSpec val startGridSimulationTriggerId = 2 val firstSweepNo = 0 - val slackNodeUuid = - UUID.fromString("9fe5fa33-6d3b-4153-a829-a16f4347bc4e") // send the start grid simulation trigger scheduler.send( @@ -204,24 +207,16 @@ class DBFSAlgorithmCenGridSpec /* We expect one grid power request message per inferior grid */ - val firstPowerRequestSender11 = inferiorGrid11.expectGridPowerMessageAsk() + val firstPowerRequestSender11 = inferiorGrid11.expectGridPowerRequest() - val firstPowerRequestSender12 = inferiorGrid12.expectGridPowerMessageAsk() + val firstPowerRequestSender12 = inferiorGrid12.expectGridPowerRequest() - val firstPowerRequestSender13 = inferiorGrid13.expectGridPowerMessageAsk() + val firstPowerRequestSender13 = inferiorGrid13.expectGridPowerRequest() - // we expect 1 request for slack voltage values - // (slack values are requested by our agent under test from the superior grid) - val firstSlackVoltageRequest = superiorGridAgent.expectMsgPF() { - case request @ RequestSlackVoltageMessage(sweepNo, nodeId) => - sweepNo shouldBe firstSweepNo - nodeId shouldBe slackNodeUuid - (request, superiorGridAgent.lastSender) - case x => - fail( - s"Invalid message received when expecting slack voltage request message. Message was $x" - ) - } + // 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 @@ -235,37 +230,46 @@ class DBFSAlgorithmCenGridSpec // 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.gaProbe.expectMsg( - ProvideSlackVoltageMessage( - firstSweepNo, - node1.getUuid, - Quantities.getQuantity(110, KILOVOLT), - Quantities.getQuantity(0, KILOVOLT) + inferiorGrid11.expectSlackVoltageProvision( + firstSweepNo, + Seq( + ExchangeVoltage( + node1.getUuid, + Quantities.getQuantity(110, KILOVOLT), + Quantities.getQuantity(0, KILOVOLT) + ) ) ) - inferiorGrid12.gaProbe.expectMsg( - ProvideSlackVoltageMessage( - firstSweepNo, - node2.getUuid, - Quantities.getQuantity(110, KILOVOLT), - Quantities.getQuantity(0, KILOVOLT) + inferiorGrid12.expectSlackVoltageProvision( + firstSweepNo, + Seq( + ExchangeVoltage( + node2.getUuid, + Quantities.getQuantity(110, KILOVOLT), + Quantities.getQuantity(0, KILOVOLT) + ) ) ) - inferiorGrid13.nodeUuids.foreach { nodeUuid => - inferiorGrid13.gaProbe.expectMsg( - ProvideSlackVoltageMessage( - firstSweepNo, - nodeUuid, + inferiorGrid13.expectSlackVoltageProvision( + firstSweepNo, + Seq( + ExchangeVoltage( + node3a.getUuid, + Quantities.getQuantity(110, KILOVOLT), + Quantities.getQuantity(0, KILOVOLT) + ), + ExchangeVoltage( + node3b.getUuid, Quantities.getQuantity(110, KILOVOLT), Quantities.getQuantity(0, KILOVOLT) ) ) - } + ) // we now answer the request of our centerGridAgent - // with 3 fake grid power messages and 1 fake slack voltage message + // with three fake grid power messages and one fake slack voltage message inferiorGrid11.gaProbe.send( firstPowerRequestSender11, @@ -306,104 +310,87 @@ class DBFSAlgorithmCenGridSpec ) ) - val slackRequestNodeUuid = firstSlackVoltageRequest match { - case (voltageRequest, slackAskSender) => - val slackRequestNodeUuid = voltageRequest.nodeUuid - val slackRequestSweepNo = voltageRequest.currentSweepNo - superiorGridAgent.send( - slackAskSender, - ProvideSlackVoltageMessage( - slackRequestSweepNo, - slackRequestNodeUuid, + superiorGridAgent.gaProbe.send( + firstSlackVoltageRequestSender, + ProvideSlackVoltageMessage( + firstSweepNo, + Seq( + ExchangeVoltage( + supNodeA.getUuid, + Quantities.getQuantity(380, KILOVOLT), + Quantities.getQuantity(0, KILOVOLT) + ), + ExchangeVoltage( + supNodeB.getUuid, Quantities.getQuantity(380, KILOVOLT), Quantities.getQuantity(0, KILOVOLT) ) ) - slackRequestNodeUuid - } - - // our test agent should now be ready to provide the grid power values, hence we ask for them and expect a - // corresponding response - - superiorGridAgent.send( - centerGridAgent, - RequestGridPowerMessage( - firstSweepNo, - Vector(slackNodeUuid) ) ) - superiorGridAgent.expectMsgPF() { - case ProvideGridPowerMessage(exchangedPower) => - exchangedPower.size shouldBe 1 - exchangedPower.headOption match { - case Some(ExchangePower(nodeUuid, p, q)) => - nodeUuid shouldBe slackNodeUuid - p should equalWithTolerance( - Quantities.getQuantity(0.080423711881452700000, MEGAVOLTAMPERE), - floatPrecision - ) - q should equalWithTolerance( - Quantities.getQuantity(-1.45357503915666260000, MEGAVOLTAMPERE), - floatPrecision - ) - case None => - fail("Did not expect to get nothing.") - } - case x => - fail( - s"Invalid message received when expecting grid power values message. Message was $x" + // 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, + Quantities.getQuantity(0, MEGAWATT), + Quantities.getQuantity(0, MEGAVAR) + ), + ExchangePower( + supNodeB.getUuid, + Quantities.getQuantity(0.160905770717798, MEGAVOLTAMPERE), + Quantities.getQuantity(-1.4535602349123878, MEGAVOLTAMPERE) ) - } + ) + ) // we start a second sweep by asking for next sweep values which should trigger the whole procedure again val secondSweepNo = firstSweepNo + 1 - superiorGridAgent.send( - centerGridAgent, - RequestGridPowerMessage( - secondSweepNo, - Vector(slackNodeUuid) - ) - ) + + superiorGridAgent.requestGridPower(centerGridAgent, secondSweepNo) // the agent now should ask for updated slack voltages from the superior grid - val secondSlackVoltageRequest = superiorGridAgent.expectMsgPF() { - case request @ RequestSlackVoltageMessage(sweepNo, nodeId) => - sweepNo shouldBe secondSweepNo - nodeId shouldBe slackNodeUuid - (request, superiorGridAgent.lastSender) - case x => - fail( - s"Invalid message received when expecting slack voltage request message. Message was $x" - ) - } + val secondSlackAskSender = + superiorGridAgent.expectSlackVoltageRequest(secondSweepNo) // the superior grid would answer with updated slack voltage values - val secondSlackAskSender = secondSlackVoltageRequest._2 - superiorGridAgent.send( + superiorGridAgent.gaProbe.send( secondSlackAskSender, ProvideSlackVoltageMessage( secondSweepNo, - slackRequestNodeUuid, - Quantities.getQuantity(380, KILOVOLT), - Quantities.getQuantity(0, KILOVOLT) + Seq( + ExchangeVoltage( + supNodeB.getUuid, + Quantities.getQuantity(380, KILOVOLT), + Quantities.getQuantity(0, KILOVOLT) + ), + ExchangeVoltage( + supNodeA.getUuid, + Quantities.getQuantity(380, KILOVOLT), + Quantities.getQuantity(0, KILOVOLT) + ) + ) ) ) - // after the intermediate power flow calculation - // We expect one grid power request message, as all four sub grids are mapped onto one actor reference + // After the intermediate power flow calculation, we expect one grid power + // request message per inferior subgrid val secondPowerRequestSender11 = - inferiorGrid11.expectGridPowerMessageAsk() + inferiorGrid11.expectGridPowerRequest() val secondPowerRequestSender12 = - inferiorGrid12.expectGridPowerMessageAsk() + inferiorGrid12.expectGridPowerRequest() val secondPowerRequestSender13 = - inferiorGrid13.expectGridPowerMessageAsk() + 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 4 inferior grid agents + // we simulate this behaviour now by doing the same for our three inferior grid agents inferiorGrid11.requestSlackVoltage(centerGridAgent, firstSweepNo) @@ -415,55 +402,46 @@ class DBFSAlgorithmCenGridSpec // to 1 p.u. (in physical values, here: 110kV) from the superior grid agent perspective // (here: centerGridAgent perspective) - inside(inferiorGrid11.gaProbe.expectMsgType[ProvideSlackVoltageMessage]) { - case ProvideSlackVoltageMessage(currentSweepNo, nodeUuid, e, f) => - currentSweepNo shouldBe firstSweepNo - nodeUuid shouldBe node1.getUuid - e should equalWithTolerance( - Quantities.getQuantity(110.1196117051188620, KILOVOLT) + inferiorGrid11.expectSlackVoltageProvision( + firstSweepNo, + Seq( + ExchangeVoltage( + node1.getUuid, + Quantities.getQuantity(110.156504579861168, KILOVOLT), + Quantities.getQuantity(-0.02700804012678551, KILOVOLT) ) - f should equalWithTolerance( - Quantities.getQuantity(-0.009318349620959118, KILOVOLT) - ) - } + ) + ) - inside(inferiorGrid12.gaProbe.expectMsgType[ProvideSlackVoltageMessage]) { - case ProvideSlackVoltageMessage(currentSweepNo, nodeUuid, e, f) => - currentSweepNo shouldBe firstSweepNo - nodeUuid shouldBe node2.getUuid - e should equalWithTolerance( - Quantities.getQuantity(110.1422124824355620, KILOVOLT) - ) - f should equalWithTolerance( - Quantities.getQuantity(-0.014094294956794604, KILOVOLT) + inferiorGrid12.expectSlackVoltageProvision( + firstSweepNo, + Seq( + ExchangeVoltage( + node2.getUuid, + Quantities.getQuantity(110.11927849702049, KILOVOLT), + Quantities.getQuantity(-0.01594978168595568, KILOVOLT) ) - } + ) + ) - inferiorGrid13.gaProbe.expectMsgType[ProvideSlackVoltageMessage] match { - case received if received.nodeUuid.equals(node3a.getUuid) => - received.currentSweepNo shouldBe firstSweepNo - received.nodeUuid shouldBe node3a.getUuid - received.e should equalWithTolerance( - Quantities.getQuantity(110.147346134387320, KILOVOLT) - ) - received.f should equalWithTolerance( - Quantities.getQuantity(-0.015819259689252657, KILOVOLT) - ) - case received if received.nodeUuid.equals(node3b.getUuid) => - received.currentSweepNo shouldBe firstSweepNo - received.nodeUuid shouldBe node3b.getUuid - received.e should equalWithTolerance( - Quantities.getQuantity(110.1277081582144170, KILOVOLT) - ) - received.f should equalWithTolerance( - Quantities.getQuantity(-0.011124597905979507, KILOVOLT) + inferiorGrid13.expectSlackVoltageProvision( + firstSweepNo, + Seq( + ExchangeVoltage( + node3a.getUuid, + Quantities.getQuantity(110.13956933728223, KILOVOLT), + Quantities.getQuantity(-0.02145845898589436, KILOVOLT) + ), + ExchangeVoltage( + node3b.getUuid, + Quantities.getQuantity(110.151555766674, KILOVOLT), + Quantities.getQuantity(-0.02544502304127, KILOVOLT) ) - case received => - fail(s"Msg with unknown node UUID ${received.nodeUuid} was received") - } + ) + ) - // we now answer the request of our centerGridAgent - // with 1 fake grid power message + // we now answer the requests of our centerGridAgent + // with three fake grid power message inferiorGrid11.gaProbe.send( secondPowerRequestSender11, ProvideGridPowerMessage( @@ -504,39 +482,40 @@ class DBFSAlgorithmCenGridSpec ) // we expect that the GridAgent unstashes the messages and return a value for our power request - superiorGridAgent.expectMsgPF() { - case ProvideGridPowerMessage(exchangedPower) => - exchangedPower.size shouldBe 1 - exchangedPower.headOption match { - case Some(ExchangePower(nodeUuid, p, q)) => - nodeUuid shouldBe slackNodeUuid - p should equalWithTolerance( - Quantities.getQuantity(0.080423711881702500000, MEGAVOLTAMPERE), - floatPrecision - ) - q should equalWithTolerance( - Quantities.getQuantity(-1.45357503915621860000, MEGAVOLTAMPERE), - floatPrecision - ) - case None => - fail("I did not expect to get nothing.") - } - - case x => - fail( - s"Invalid message received when expecting grid power message. Message was $x" + superiorGridAgent.expectGridPowerProvision( + Seq( + ExchangePower( + supNodeA.getUuid, + Quantities.getQuantity(0, MEGAWATT), + Quantities.getQuantity(0, MEGAVAR) + ), + ExchangePower( + supNodeB.getUuid, + Quantities.getQuantity(0.160905770717798, MEGAVOLTAMPERE), + Quantities.getQuantity(-1.4535602349123878, MEGAVOLTAMPERE) ) - } + ) + ) } } } object DBFSAlgorithmCenGridSpec extends UnitSpec { - final case class GAActorAndModel(gaProbe: TestProbe, nodeUuids: Seq[UUID]) { + private val floatPrecision: Double = 0.00000000001 + + sealed trait GAActorAndModel { + val gaProbe: TestProbe + val nodeUuids: Seq[UUID] def ref: ActorRef = gaProbe.ref + } + + final case class InferiorGA( + override val gaProbe: TestProbe, + override val nodeUuids: Seq[UUID] + ) extends GAActorAndModel { - def expectGridPowerMessageAsk(): ActorRef = { + def expectGridPowerRequest(): ActorRef = { gaProbe .expectMsgType[RequestGridPowerMessage] .nodeUuids should contain allElementsOf nodeUuids @@ -544,13 +523,101 @@ object DBFSAlgorithmCenGridSpec extends UnitSpec { gaProbe.lastSender } - def requestSlackVoltage(receiver: ActorRef, sweepNo: Int): Unit = { - nodeUuids.foreach { node => - gaProbe.send( - receiver, - RequestSlackVoltageMessage(sweepNo, node) - ) + def expectSlackVoltageProvision( + expectedSweepNo: Int, + expectedExchangedVoltages: Seq[ExchangeVoltage] + ): Unit = { + inside(gaProbe.expectMsgType[ProvideSlackVoltageMessage]) { + case ProvideSlackVoltageMessage(sweepNo, exchangedVoltages) => + sweepNo shouldBe expectedSweepNo + + exchangedVoltages.size shouldBe expectedExchangedVoltages.size + expectedExchangedVoltages.foreach { expectedVoltage => + exchangedVoltages.find( + _.nodeUuid == expectedVoltage.nodeUuid + ) match { + case Some(ExchangeVoltage(_, actualE, actualF)) => + actualE should equalWithTolerance( + expectedVoltage.e, + floatPrecision + ) + actualF should equalWithTolerance( + expectedVoltage.f, + floatPrecision + ) + case None => + fail( + s"Expected ExchangeVoltage with node UUID ${expectedVoltage.nodeUuid} " + + s"was not included in ProvideSlackVoltageMessage." + ) + } + } + } + } + + def requestSlackVoltage(receiver: ActorRef, sweepNo: Int): Unit = + gaProbe.send( + receiver, + RequestSlackVoltageMessage(sweepNo, nodeUuids) + ) + } + + final case class SuperiorGA( + override val gaProbe: TestProbe, + override val nodeUuids: Seq[UUID] + ) extends GAActorAndModel { + + def expectSlackVoltageRequest(expectedSweepNo: Int): ActorRef = { + inside( + gaProbe + .expectMsgType[RequestSlackVoltageMessage] + ) { + case RequestSlackVoltageMessage(msgSweepNo: Int, msgUuids: Seq[UUID]) => + msgSweepNo shouldBe expectedSweepNo + msgUuids should have size nodeUuids.size + msgUuids should contain allElementsOf nodeUuids } + + gaProbe.lastSender + } + + def expectGridPowerProvision( + expectedExchangedPowers: Seq[ExchangePower] + ): Unit = { + inside(gaProbe.expectMsgType[ProvideGridPowerMessage]) { + case ProvideGridPowerMessage(exchangedPower) => + exchangedPower should have size expectedExchangedPowers.size + + expectedExchangedPowers.foreach { expectedPower => + exchangedPower.find(_.nodeUuid == expectedPower.nodeUuid) match { + case Some(ExchangePower(_, actualP, actualQ)) => + actualP should equalWithTolerance( + expectedPower.p, + floatPrecision + ) + actualQ should equalWithTolerance( + expectedPower.q, + floatPrecision + ) + case None => + fail( + s"Expected ExchangePower with node UUID ${expectedPower.nodeUuid} " + + s"was not included in ProvideGridPowerMessage." + ) + } + } + + } + } + + def requestGridPower(receiver: ActorRef, sweepNo: Int): Unit = { + gaProbe.send( + receiver, + RequestGridPowerMessage( + sweepNo, + nodeUuids + ) + ) } } } 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 4452103a90..097f0bb548 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 @@ -7,7 +7,6 @@ package edu.ie3.simona.test.common.model.grid import java.util.UUID - import edu.ie3.datamodel.graph.SubGridGate import edu.ie3.datamodel.models.OperationTime import edu.ie3.datamodel.models.input.connector._ @@ -35,6 +34,7 @@ import edu.ie3.datamodel.models.input.{ import edu.ie3.datamodel.models.voltagelevels.GermanVoltageLevelUtils import edu.ie3.datamodel.utils.GridAndGeoUtils import edu.ie3.util.quantities.PowerSystemUnits._ + import javax.measure.MetricPrefix import tech.units.indriya.quantity.Quantities import tech.units.indriya.unit.Units._ @@ -90,7 +90,7 @@ trait DbfsTestGrid extends SubGridGateMokka { GermanVoltageLevelUtils.HV, 1 ) - private val slackNode = new NodeInput( + protected val supNodeA = new NodeInput( UUID.fromString("9fe5fa33-6d3b-4153-a829-a16f4347bc4e"), "HS_NET1_Station_1_380", OperatorInput.NO_OPERATOR_ASSIGNED, @@ -101,6 +101,17 @@ trait DbfsTestGrid extends SubGridGateMokka { GermanVoltageLevelUtils.EHV_380KV, 1000 ) + protected val supNodeB = new NodeInput( + UUID.fromString("fb4272fa-5a31-4218-9a46-0a37ac5b34a4"), + "HS_NET1_Station_2_380", + OperatorInput.NO_OPERATOR_ASSIGNED, + OperationTime.notLimited(), + Quantities.getQuantity(1.0, PU), + true, + NodeInput.DEFAULT_GEO_POSITION, + GermanVoltageLevelUtils.EHV_380KV, + 1000 + ) /* Mocking table of nodes of underlying grids * @@ -246,21 +257,33 @@ trait DbfsTestGrid extends SubGridGateMokka { private val transformer1 = new Transformer2WInput( UUID.fromString("6e9d912b-b652-471b-84d2-6ed571e53a7b"), - "HöS-Trafo_S1", + "HöS-Trafo_S2", OperatorInput.NO_OPERATOR_ASSIGNED, OperationTime.notLimited(), - slackNode, + supNodeA, node1, 1, trafoType, 0, false ) + private val transformer2 = new Transformer2WInput( + UUID.fromString("ceccd8cb-29dc-45d6-8a13-4b0033c5f1ef"), + "HöS-Trafo_S1", + OperatorInput.NO_OPERATOR_ASSIGNED, + OperationTime.notLimited(), + supNodeB, + node2, + 1, + trafoType, + 0, + false + ) protected val (hvGridContainer, hvSubGridGates) = { - val nodes = Set(node1, node2, node3a, node3b, slackNode) + val nodes = Set(node1, node2, node3a, node3b, supNodeA, supNodeB) val lines = Set(line1, line2, line3, line4, line5) - val transformers = Set(transformer1) + val transformers = Set(transformer1, transformer2) val rawGridElements = new RawGridElements( nodes.asJava, lines.asJava, @@ -335,7 +358,7 @@ trait DbfsTestGrid extends SubGridGateMokka { } protected val (ehvGridContainer, ehvSubGridGates) = { - val nodes = Set(slackNode) + val nodes = Set(supNodeA) val rawGridElements = new RawGridElements( nodes.asJava, Set.empty[LineInput].asJava, From b35c13ab421719ae9c60308ecfb100feb9e84240 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Sun, 10 Jul 2022 15:17:33 +0200 Subject: [PATCH 25/36] Always taking the slack node with smallest index --- .../simona/agent/grid/PowerFlowSupport.scala | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala b/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala index fb7d9b6a14..c0eb23ea7b 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala @@ -429,35 +429,35 @@ trait PowerFlowSupport { ) // / add WithForcedVoltageVector for slackNode // / as we know that there is at least one slack in every grid available here, we can do a direct call on element zero - // // NOTE: currently only the first slack node is taken, needs to be adapted when several slacks are present - val forcedSlackNodeVoltage = - operatingPoint.find(_.nodeType == NodeType.SL) match { - case Some(slackNodeData) => - WithForcedStartVoltages(Array(StateData(slackNodeData))) - case None => - throw new DBFSAlgorithmException( - s"Unable to find a slack node in grid ${gridModel.subnetNo}." - ) - } + + // // NOTE: currently only the first slack node is taken, needs to be adapted when several slacks are present. + val slackNodeData = operatingPoint + .filter(_.nodeType == NodeType.SL) + .minByOption(_.index) + .getOrElse( + throw new DBFSAlgorithmException( + s"Unable to find a slack node in grid ${gridModel.subnetNo}." + ) + ) + val forcedSlackNodeVoltage = WithForcedStartVoltages( + Array(StateData(slackNodeData)) + ) + // / execute val powerFlow = NewtonRaphsonPF(epsilon, maxIterations, admittanceMatrix) /* Currently, only one slack node per sub grid is allowed. In case a model has more than one, set all others to * PQ nodes. ATTENTION: This does not cover the power flow situation correctly! */ - val adaptedOperatingPoint = operatingPoint - .foldLeft((Array.empty[PresetData], true)) { - case ((adaptedOperatingPoint, firstSlack), nodePreset) - if nodePreset.nodeType == NodeType.SL => - /* If this is the first slack node see, leave it as a slack node. If it is not the first one. Make it a - * PQ node. */ - val adaptedNodePreset = - if (firstSlack) nodePreset - else nodePreset.copy(nodeType = NodeType.PQ) - (adaptedOperatingPoint.appended(adaptedNodePreset), false) - case ((adaptedOperatingPoint, firstSlack), nodePreset) => - (adaptedOperatingPoint.appended(nodePreset), firstSlack) - } match { case (operatingPoint, _) => operatingPoint } + val adaptedOperatingPoint = operatingPoint.map { nodePreset => + if (nodePreset.nodeType == NodeType.SL) { + /* If this is the first slack node see, leave it as a slack node. If it is not the first one. Make it a + * PQ node. */ + if (nodePreset == slackNodeData) nodePreset + else nodePreset.copy(nodeType = NodeType.PQ) + } else + nodePreset + } Try { powerFlow.calculate( From abfd7b8b657419e9bfd6d82f1ecf517ac365ec4c Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Mon, 11 Jul 2022 16:02:45 +0200 Subject: [PATCH 26/36] Fixed horribly inefficient map operation --- .../scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala b/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala index c0eb23ea7b..2266ef42aa 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala @@ -132,8 +132,9 @@ trait PowerFlowSupport { /* If the preset voltage is meant to be determined by means of received data and the node is a slack node * (only then there is received data), look it up and transform it */ val receivedSlackVoltage = - receivedValuesStore.nodeToReceivedSlackVoltage.values.flatten - .find(_.nodeUuid == nodeModel.uuid) + receivedValuesStore.nodeToReceivedSlackVoltage + .get(nodeModel.uuid) + .flatten .getOrElse( throw new RuntimeException( s"No slack voltage received for node ${nodeModel.id} [${nodeModel.uuid}]!" From 44e4a288b3575899eef2514f62ffdabba6dc099d Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Mon, 11 Jul 2022 19:29:17 +0200 Subject: [PATCH 27/36] Fixating the order of nodes --- .../ie3/simona/agent/grid/PowerFlowSupport.scala | 6 +++--- .../ie3/simona/agent/grid/SweepValueStore.scala | 2 +- .../edu/ie3/simona/model/grid/GridModel.scala | 13 ++++++------- .../agent/grid/DBFSAlgorithmCenGridSpec.scala | 5 +++-- .../scala/edu/ie3/simona/model/grid/GridSpec.scala | 14 +++++++------- .../simona/test/common/model/grid/BasicGrid.scala | 4 ++-- .../common/model/grid/BasicGridWithSwitches.scala | 4 ++-- .../test/common/model/grid/DbfsTestGrid.scala | 6 +++++- .../common/model/grid/FiveLinesWithNodes.scala | 8 ++++---- 9 files changed, 33 insertions(+), 29 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala b/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala index 2266ef42aa..1bfd23141e 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala @@ -70,7 +70,7 @@ trait PowerFlowSupport { * [[edu.ie3.powerflow.NewtonRaphsonPF.calculate()]] */ protected def composeOperatingPoint( - nodes: Set[NodeModel], + nodes: Seq[NodeModel], transformers2w: Set[TransformerModel], transformers3w: Set[Transformer3wModel], nodeUuidToIndexMap: Map[UUID, Int], @@ -82,7 +82,7 @@ trait PowerFlowSupport { val mainRefSystemPowerUnit = gridMainRefSystem.nominalPower.getUnit - nodes.toArray.map { nodeModel => + nodes.map { nodeModel => // note: currently we only support pq nodes as we not distinguish between pq/pv nodes - // when slack emulators or pv-node assets are added this needs to be considered here val nodeType = if (nodeModel.isSlack) NodeType.SL else NodeType.PQ @@ -160,7 +160,7 @@ trait PowerFlowSupport { } PresetData(nodeIdx, nodeType, apparentPower, targetVoltageInPu.abs) - } + }.toArray } /** Composes the current operation point needed by diff --git a/src/main/scala/edu/ie3/simona/agent/grid/SweepValueStore.scala b/src/main/scala/edu/ie3/simona/agent/grid/SweepValueStore.scala index 4616c70d3f..dfe459fc72 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/SweepValueStore.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/SweepValueStore.scala @@ -54,7 +54,7 @@ case object SweepValueStore { */ def apply( validResult: ValidNewtonRaphsonPFResult, - nodes: Set[NodeModel], + nodes: Seq[NodeModel], nodeUuidToIndexMap: Map[UUID, Int] ): SweepValueStore = { val sweepDataValues = nodes.foldLeft(Vector.empty[SweepValueStoreData])( 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 8c4ca14de1..f58bb10d9c 100644 --- a/src/main/scala/edu/ie3/simona/model/grid/GridModel.scala +++ b/src/main/scala/edu/ie3/simona/model/grid/GridModel.scala @@ -75,7 +75,7 @@ case object GridModel { * model */ final case class GridComponents( - nodes: Set[NodeModel], + nodes: Seq[NodeModel], lines: Set[LineModel], transformers: Set[TransformerModel], transformers3w: Set[Transformer3wModel], @@ -95,7 +95,7 @@ case object GridModel { */ private def getConnectedNodes( connector: ConnectorInput, - nodes: Set[NodeModel] + nodes: Seq[NodeModel] ): (NodeModel, NodeModel) = { val nodeAOpt: Option[NodeModel] = nodes.find(_.uuid.equals(connector.getNodeA.getUuid)) @@ -133,7 +133,7 @@ case object GridModel { */ private def getConnectedNodes( transformerInput: Transformer3WInput, - nodes: Set[NodeModel] + nodes: Seq[NodeModel] ): (NodeModel, NodeModel, NodeModel) = { val (nodeA, nodeB) = getConnectedNodes(transformerInput.asInstanceOf[ConnectorInput], nodes) @@ -496,10 +496,9 @@ case object GridModel { // build // / nodes - val nodes: Set[NodeModel] = - subGridContainer.getRawGrid.getNodes.asScala.map { nodeInput => - NodeModel(nodeInput, startDate, endDate) - }.toSet + val nodes = subGridContainer.getRawGrid.getNodes.asScala.toSeq.map { + nodeInput => NodeModel(nodeInput, startDate, endDate) + } // / lines val lines: Set[LineModel] = 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 6d61ef4ad0..bf84b94bb1 100644 --- a/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmCenGridSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmCenGridSpec.scala @@ -49,8 +49,9 @@ import edu.ie3.util.quantities.PowerSystemUnits._ import tech.units.indriya.quantity.Quantities import java.util.UUID -import java.util.concurrent.TimeUnit import scala.concurrent.Await +import scala.concurrent.duration.DurationInt +import scala.language.postfixOps /** Test to ensure the functions that a [[GridAgent]] in center position should * be able to do if the DBFSAlgorithm is used. The scheduler, the weather @@ -138,7 +139,7 @@ class DBFSAlgorithmCenGridSpec ) // send init data to agent and expect a CompletionMessage - implicit val timeout: Timeout = Timeout(1, TimeUnit.SECONDS) + implicit val timeout: Timeout = 3 seconds val actualInitReply = Await.result( centerGridAgent ? TriggerWithIdMessage( 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 5ede9135a8..d775ca2096 100644 --- a/src/test/scala/edu/ie3/simona/model/grid/GridSpec.scala +++ b/src/test/scala/edu/ie3/simona/model/grid/GridSpec.scala @@ -95,7 +95,7 @@ class GridSpec extends UnitSpec with LineInputTestData with DefaultTestData { "validate the connectivity of a connected grid correctly" in new BasicGridWithSwitches { // enable nodes - override val nodes: Set[NodeModel] = super.nodes + override val nodes: Seq[NodeModel] = super.nodes nodes.foreach(_.enable()) // enable lines @@ -129,7 +129,7 @@ class GridSpec extends UnitSpec with LineInputTestData with DefaultTestData { "throw an InvalidGridException if a grid is not connected" in new BasicGridWithSwitches { // enable nodes - override val nodes: Set[NodeModel] = super.nodes + override val nodes: Seq[NodeModel] = super.nodes nodes.foreach(_.enable()) // remove a line from the grid @@ -163,7 +163,7 @@ class GridSpec extends UnitSpec with LineInputTestData with DefaultTestData { "throw an InvalidGridException if two switches are connected @ the same node" in new BasicGridWithSwitches { // enable nodes - override val nodes: Set[NodeModel] = super.nodes + override val nodes: Seq[NodeModel] = super.nodes nodes.foreach(_.enable()) // add a second switch @ node13 (between node1 and node13) @@ -211,7 +211,7 @@ class GridSpec extends UnitSpec with LineInputTestData with DefaultTestData { "contains 3 open switches" in new BasicGridWithSwitches { // enable nodes - override val nodes: Set[NodeModel] = super.nodes + override val nodes: Seq[NodeModel] = super.nodes nodes.foreach(_.enable()) // enable switches override val switches: Set[SwitchModel] = super.switches @@ -255,7 +255,7 @@ class GridSpec extends UnitSpec with LineInputTestData with DefaultTestData { "contains 3 closed switches" in new BasicGridWithSwitches { // enable nodes - override val nodes: Set[NodeModel] = super.nodes + override val nodes: Seq[NodeModel] = super.nodes nodes.foreach(_.enable()) // enable switches override val switches: Set[SwitchModel] = super.switches @@ -339,7 +339,7 @@ class GridSpec extends UnitSpec with LineInputTestData with DefaultTestData { "contains 1 open and 2 closed switches" in new BasicGridWithSwitches { // enable nodes - override val nodes: Set[NodeModel] = super.nodes + override val nodes: Seq[NodeModel] = super.nodes nodes.foreach(_.enable()) // enable switches override val switches: Set[SwitchModel] = super.switches @@ -412,7 +412,7 @@ class GridSpec extends UnitSpec with LineInputTestData with DefaultTestData { "contains no switches" in new BasicGrid { // enable nodes - override val nodes: Set[NodeModel] = super.nodes + override val nodes: Seq[NodeModel] = super.nodes nodes.foreach(_.enable()) // get the grid from the raw data 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 5489c7fc98..99d62b0e29 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 @@ -73,8 +73,8 @@ trait BasicGrid extends FiveLinesWithNodes with DefaultTestData { transformerHvVoltLvl ) - override protected def nodes: Set[NodeModel] = - super.nodes + node6 + override protected def nodes: Seq[NodeModel] = + super.nodes :+ node6 // update nodeToIndexMap // nodeToIndexMap 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 5e61f1f69e..61be41a281 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 @@ -68,8 +68,8 @@ trait BasicGridWithSwitches extends BasicGrid { ) // add nodes to nodes list - override protected def nodes: Set[NodeModel] = - super.nodes ++ List(node13, node14, node15, node16, node17, node18) + override protected def nodes: Seq[NodeModel] = + super.nodes ++ Seq(node13, node14, node15, node16, node17, node18) // add nodes to nodeUuidToIndexMap override protected def nodeUuidToIndexMap: Map[UUID, Int] = 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 097f0bb548..e2ee5c059f 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 @@ -39,6 +39,7 @@ import javax.measure.MetricPrefix import tech.units.indriya.quantity.Quantities import tech.units.indriya.unit.Units._ +import scala.collection.mutable import scala.jdk.CollectionConverters._ /** Provides the high voltage level of SIMONA's test grid. Only consists of @@ -281,7 +282,10 @@ trait DbfsTestGrid extends SubGridGateMokka { ) protected val (hvGridContainer, hvSubGridGates) = { - val nodes = Set(node1, node2, node3a, node3b, supNodeA, supNodeB) + // LinkedHashSet in order to preserve the given order. + // This is important as long as only one slack node between two sub grids can exist + val nodes = + mutable.LinkedHashSet(node1, node2, node3a, node3b, supNodeB, supNodeA) val lines = Set(line1, line2, line3, line4, line5) val transformers = Set(transformer1, transformer2) val rawGridElements = new RawGridElements( 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 9ab6b13b6e..ed97b59e7a 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 @@ -20,8 +20,8 @@ import tech.units.indriya.ComparableQuantity import tech.units.indriya.quantity.Quantities import tech.units.indriya.unit.Units._ -/** A simple grid consisting of 6 nodes and 5 lines. Besides the [[NodeModel]] s - * and [[LineModels]] s it also contains the corresponding admittance matrix. +/** A simple grid consisting of 6 nodes and 5 lines. Besides the [[NodeModel]]s + * and [[LineModel]]s it also contains the corresponding admittance matrix. * * (5) * | @@ -131,8 +131,8 @@ trait FiveLinesWithNodes { linesRatedVoltage ) - protected def nodes: Set[NodeModel] = - Set(node0, node1, node2, node3, node4, node5) + protected def nodes: Seq[NodeModel] = + Seq(node0, node1, node2, node3, node4, node5) val line01: LineModel = _lineCreator( "line01", From fcee14cdbd9056a56daa5196d49a0945144805e9 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Tue, 19 Jul 2022 22:07:51 +0200 Subject: [PATCH 28/36] Have DBFS return power in correct unit (Watt/Var) --- .../edu/ie3/simona/model/grid/RefSystem.scala | 14 ++++-- .../util/scala/quantities/QuantityUtil.scala | 15 +++++++ .../agent/grid/DBFSAlgorithmCenGridSpec.scala | 8 ++-- .../util/quantities/QuantityUtilSpec.scala | 45 ++++++++++++++----- 4 files changed, 65 insertions(+), 17 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/model/grid/RefSystem.scala b/src/main/scala/edu/ie3/simona/model/grid/RefSystem.scala index 20178fdbe1..d058ddab33 100644 --- a/src/main/scala/edu/ie3/simona/model/grid/RefSystem.scala +++ b/src/main/scala/edu/ie3/simona/model/grid/RefSystem.scala @@ -9,6 +9,8 @@ package edu.ie3.simona.model.grid import breeze.math.Complex import edu.ie3.util.quantities.PowerSystemUnits._ import edu.ie3.util.quantities.{PowerSystemUnits, QuantityUtil} +import edu.ie3.util.scala.quantities.QuantityUtil.RichUnit + import javax.measure.Quantity import javax.measure.quantity._ import tech.units.indriya.ComparableQuantity @@ -97,7 +99,10 @@ final case class RefSystem private ( * unreferenced active power value in Watt */ def pInSi(pInPu: Quantity[Dimensionless]): ComparableQuantity[Power] = - nominalPower.multiply(pInPu).asType(classOf[Power]).to(nominalPower.getUnit) + nominalPower + .multiply(pInPu) + .asType(classOf[Power]) + .to(nominalPower.getUnit.toEquivalentIn(WATT)) def pInSi(pInPu: Double): ComparableQuantity[Power] = pInSi(Quantities.getQuantity(pInPu, PU)) @@ -121,10 +126,13 @@ final case class RefSystem private ( * @param qInPu * referenced active power value in p.u. * @return - * unreferenced active power value in Var + * unreferenced active power value in VAr */ def qInSi(qInPu: Quantity[Dimensionless]): ComparableQuantity[Power] = - nominalPower.multiply(qInPu).asType(classOf[Power]).to(nominalPower.getUnit) + nominalPower + .multiply(qInPu) + .asType(classOf[Power]) + .to(nominalPower.getUnit.toEquivalentIn(VAR)) def qInSi(qInPu: Double): ComparableQuantity[Power] = qInSi(Quantities.getQuantity(qInPu, PU)) diff --git a/src/main/scala/edu/ie3/util/scala/quantities/QuantityUtil.scala b/src/main/scala/edu/ie3/util/scala/quantities/QuantityUtil.scala index 1452dc0d0b..143c5ac73f 100644 --- a/src/main/scala/edu/ie3/util/scala/quantities/QuantityUtil.scala +++ b/src/main/scala/edu/ie3/util/scala/quantities/QuantityUtil.scala @@ -9,6 +9,7 @@ package edu.ie3.util.scala.quantities import edu.ie3.simona.exceptions.QuantityException import edu.ie3.util.quantities.{QuantityUtil => PSQuantityUtil} +import javax.measure import javax.measure.Quantity import tech.units.indriya.ComparableQuantity import tech.units.indriya.function.Calculus @@ -291,4 +292,18 @@ object QuantityUtil { if (q.isGreaterThan(other)) q else other } } + + implicit class RichUnit[Q <: Quantity[Q]]( + private val unit: measure.Unit[Q] + ) extends AnyVal { + + /** Transform some power unit to given unit with the same prefix + * @param targetUnit + * the target system unit + * @return + * this unit converted to given + */ + def toEquivalentIn(targetUnit: measure.Unit[Q]): measure.Unit[Q] = + targetUnit.transform(unit.getConverterTo(unit.getSystemUnit)) + } } 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 bf84b94bb1..850bfac8ad 100644 --- a/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmCenGridSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmCenGridSpec.scala @@ -343,8 +343,8 @@ class DBFSAlgorithmCenGridSpec ), ExchangePower( supNodeB.getUuid, - Quantities.getQuantity(0.160905770717798, MEGAVOLTAMPERE), - Quantities.getQuantity(-1.4535602349123878, MEGAVOLTAMPERE) + Quantities.getQuantity(0.160905770717798, MEGAWATT), + Quantities.getQuantity(-1.4535602349123878, MEGAVAR) ) ) ) @@ -492,8 +492,8 @@ class DBFSAlgorithmCenGridSpec ), ExchangePower( supNodeB.getUuid, - Quantities.getQuantity(0.160905770717798, MEGAVOLTAMPERE), - Quantities.getQuantity(-1.4535602349123878, MEGAVOLTAMPERE) + Quantities.getQuantity(0.160905770717798, MEGAWATT), + Quantities.getQuantity(-1.4535602349123878, MEGAVAR) ) ) ) diff --git a/src/test/scala/edu/ie3/util/quantities/QuantityUtilSpec.scala b/src/test/scala/edu/ie3/util/quantities/QuantityUtilSpec.scala index a8b71e6a84..94ed7cb42a 100644 --- a/src/test/scala/edu/ie3/util/quantities/QuantityUtilSpec.scala +++ b/src/test/scala/edu/ie3/util/quantities/QuantityUtilSpec.scala @@ -8,24 +8,25 @@ package edu.ie3.util.quantities import edu.ie3.simona.exceptions.QuantityException import edu.ie3.simona.test.common.UnitSpec -import tech.units.indriya.quantity.Quantities - -import javax.measure.Quantity -import javax.measure.quantity.{Energy, Power} +import edu.ie3.util.quantities.PowerSystemUnits._ import edu.ie3.util.scala.quantities.QuantityUtil +import edu.ie3.util.scala.quantities.QuantityUtil.RichUnit import org.scalatest.prop.TableDrivenPropertyChecks +import tech.units.indriya.quantity.Quantities +import tech.units.indriya.unit.Units.WATT import tech.units.indriya.unit.{ProductUnit, Units} -import javax.measure +import javax.measure.Quantity +import javax.measure.quantity.{Energy, Power} import scala.util.{Failure, Success} class QuantityUtilSpec extends UnitSpec with TableDrivenPropertyChecks { - val unit: measure.Unit[Power] = PowerSystemUnits.KILOWATT - val integrationUnit = + private val unit = PowerSystemUnits.KILOWATT + private val integrationUnit = new ProductUnit[Energy](PowerSystemUnits.KILOWATT.multiply(Units.SECOND)) - val integrationClass: Class[Energy] = classOf[Energy] - val averagingClass: Class[Power] = classOf[Power] - val values = Map( + private val integrationClass = classOf[Energy] + private val averagingClass = classOf[Power] + private val values = Map( 2L -> Quantities.getQuantity(5d, unit), 4L -> Quantities.getQuantity(15d, unit), 6L -> Quantities.getQuantity(-5d, unit), @@ -186,4 +187,28 @@ class QuantityUtilSpec extends UnitSpec with TableDrivenPropertyChecks { } } } + + "Converting units to alternative units" should { + "succeed if units are compatible" in { + val cases = Table( + ("sourceUnit", "targetUnit", "expectedUnit"), + (VOLTAMPERE, WATT, WATT), + (KILOVOLTAMPERE, WATT, KILOWATT), + (MEGAVOLTAMPERE, WATT, MEGAWATT), + (VAR, WATT, WATT), + (KILOVAR, WATT, KILOWATT), + (MEGAVAR, WATT, MEGAWATT), + (VOLTAMPERE, VAR, VAR), + (KILOVOLTAMPERE, VAR, KILOVAR), + (MEGAVOLTAMPERE, VAR, MEGAVAR), + (WATT, VAR, VAR), + (KILOWATT, VAR, KILOVAR), + (MEGAWATT, VAR, MEGAVAR) + ) + + forAll(cases) { (sourceUnit, targetUnit, expectedUnit) => + sourceUnit.toEquivalentIn(targetUnit) shouldBe expectedUnit + } + } + } } From 2eb4be0e237a4a03ffa93fbf45edd1fafad628f4 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Wed, 20 Jul 2022 16:14:00 +0200 Subject: [PATCH 29/36] Enhancing QuantityUtilSpec.scala Co-authored-by: Daniel Feismann <98817556+danielfeismann@users.noreply.github.com> --- .../scala/edu/ie3/util/quantities/QuantityUtilSpec.scala | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/test/scala/edu/ie3/util/quantities/QuantityUtilSpec.scala b/src/test/scala/edu/ie3/util/quantities/QuantityUtilSpec.scala index 94ed7cb42a..c2b3f0faec 100644 --- a/src/test/scala/edu/ie3/util/quantities/QuantityUtilSpec.scala +++ b/src/test/scala/edu/ie3/util/quantities/QuantityUtilSpec.scala @@ -203,7 +203,13 @@ class QuantityUtilSpec extends UnitSpec with TableDrivenPropertyChecks { (MEGAVOLTAMPERE, VAR, MEGAVAR), (WATT, VAR, VAR), (KILOWATT, VAR, KILOVAR), - (MEGAWATT, VAR, MEGAVAR) + (MEGAWATT, VAR, MEGAVAR), + (VAR, VOLTAMPERE, VOLTAMPERE), + (KILOVAR, VOLTAMPERE, KILOVOLTAMPERE), + (MEGAVAR, VOLTAMPERE, MEGAVOLTAMPERE), + (WATT, VOLTAMPERE, VOLTAMPERE), + (KILOWATT, VOLTAMPERE, KILOVOLTAMPERE), + (MEGAWATT, VOLTAMPERE, MEGAVOLTAMPERE) ) forAll(cases) { (sourceUnit, targetUnit, expectedUnit) => From c2372311a6c624e4a5b2d1f15f84dc1869bdaf57 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Thu, 21 Jul 2022 19:24:19 +0200 Subject: [PATCH 30/36] Reverting Var unit naming in ScalaDoc --- src/main/scala/edu/ie3/simona/model/grid/RefSystem.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/edu/ie3/simona/model/grid/RefSystem.scala b/src/main/scala/edu/ie3/simona/model/grid/RefSystem.scala index d058ddab33..50df2c4e49 100644 --- a/src/main/scala/edu/ie3/simona/model/grid/RefSystem.scala +++ b/src/main/scala/edu/ie3/simona/model/grid/RefSystem.scala @@ -126,7 +126,7 @@ final case class RefSystem private ( * @param qInPu * referenced active power value in p.u. * @return - * unreferenced active power value in VAr + * unreferenced active power value in Var */ def qInSi(qInPu: Quantity[Dimensionless]): ComparableQuantity[Power] = nominalPower From dedb104bc1ec9967d50e39b3c2d8742378798c16 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Sun, 31 Jul 2022 18:46:04 +0200 Subject: [PATCH 31/36] Merged fix & dev --- CHANGELOG.md | 3 + build.gradle | 4 +- gradle.properties | 13 +- .../ie3/simona/agent/grid/DBFSAlgorithm.scala | 49 ++-- .../simona/agent/grid/PowerFlowSupport.scala | 210 ++++++++++-------- .../service/primary/PrimaryServiceProxy.scala | 31 ++- .../weather/WeatherSourceWrapper.scala | 2 +- .../model/assets/control/QControlSpec.scala | 2 +- .../service/ev/ExtEvDataServiceSpec.scala | 2 +- .../common/input/TimeSeriesTestData.scala | 27 +-- 10 files changed, 188 insertions(+), 155 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70d13b910a..0aa94873ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Adapt to new simonaAPI snapshot [#95](https://github.com/ie3-institute/simona/issues/95) - Update Sphinx to 4.5.0 as well as extensions [#214](https://github.com/ie3-institute/simona/issues/214) - Improved code quality in and around DBFS algorithm [#265](https://github.com/ie3-institute/simona/issues/265) +- Adapt test to new PowerSystemUtils snapshot [#294](https://github.com/ie3-institute/simona/issues/294) ### Fixed - Location of `vn_simona` test grid (was partially in Berlin and Dortmund) [#72](https://github.com/ie3-institute/simona/issues/72) @@ -49,6 +50,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed some unreachable code [#167](https://github.com/ie3-institute/simona/issues/167) - Fix treatment of non-InitializeTrigger triggers in initialization within SimScheduler [#237](https://github.com/ie3-institute/simona/issues/237) - Fix breaking SIMONA caused by introducing temperature dependant load profiles in PSDM [#255](https://github.com/ie3-institute/simona/issues/255) +- Respect for voltage angle in DBFS slack voltage exchange protocol [#69](https://github.com/ie3-institute/simona/issues/69) +- Adapted to changed time series interfaces in PSDM [#296](https://github.com/ie3-institute/simona/issues/296) - Fix handling of multiple connections between subgrids [#22](https://github.com/ie3-institute/simona/issues/22) - Consolidate request replies for different sub grid gates in one message - Await and send responses for distinct pairs of sender reference and target node diff --git a/build.gradle b/build.gradle index 29a5634279..1a17bca7f6 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ plugins { id 'signing' id 'maven-publish' // publish to a maven repo (local or mvn central, has to be defined) id 'pmd' // code check, working on source code - id 'com.diffplug.spotless' version '6.8.0'// code format + id 'com.diffplug.spotless' version '6.9.0'// code format id 'com.github.onslip.gradle-one-jar' version '1.0.6' // pack a self contained jar id "com.github.ben-manes.versions" version '0.42.0' id "de.undercouch.download" version "5.1.0" // downloads plugin @@ -146,7 +146,7 @@ dependencies { /* Kafka */ implementation group: 'org.apache.kafka', name: 'kafka-clients', version: '3.2.0' - implementation 'io.confluent:kafka-streams-avro-serde:7.2.0' + implementation 'io.confluent:kafka-streams-avro-serde:7.2.1' implementation "com.sksamuel.avro4s:avro4s-core_${scalaVersion}:4.1.0" implementation 'org.apache.commons:commons-math3:3.6.1' // apache commons math3 diff --git a/gradle.properties b/gradle.properties index b761da57f7..7206f5237b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -9,20 +9,11 @@ # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. - -# Workaround to make spotless work with java 17 - -# see https://github.com/diffplug/spotless/tree/main/plugin-gradle#google-java-format and -# https://github.com/ie3-institute/simona/issues/59 for details -org.gradle.jvmargs=-Xmx4096m \ - --add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \ - --add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \ - --add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED \ - --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \ - --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED +org.gradle.jvmargs=-Xmx4096m # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true -com.github.maiflai.gradle-scalatest.mode = append \ No newline at end of file +com.github.maiflai.gradle-scalatest.mode = append 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 6726932fa1..7a6d9ad8bf 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/DBFSAlgorithm.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/DBFSAlgorithm.scala @@ -482,21 +482,23 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { ) val gridModel = gridAgentBaseData.gridEnv.gridModel - val powerFlowResult = newtonRaphsonPF( + + val (operatingPoint, slackNodeVoltages) = composeOperatingPoint( + gridModel.gridComponents.nodes, + gridModel.gridComponents.transformers, + gridModel.gridComponents.transformers3w, + gridModel.nodeUuidToIndexMap, + gridAgentBaseData.receivedValueStore, + gridModel.mainRefSystem + ) + + newtonRaphsonPF( gridModel, gridAgentBaseData.powerFlowParams.maxIterations, - composeOperatingPoint( - gridModel.gridComponents.nodes, - gridModel.gridComponents.transformers, - gridModel.gridComponents.transformers3w, - gridModel.nodeUuidToIndexMap, - gridAgentBaseData.receivedValueStore, - gridModel.mainRefSystem - ) - )(gridAgentBaseData.powerFlowParams.epsilon) - - // if res is valid, ask our assets (if any) for updated power values based on the newly determined nodal voltages - powerFlowResult match { + operatingPoint, + slackNodeVoltages + )(gridAgentBaseData.powerFlowParams.epsilon) match { + // if res is valid, ask our assets (if any) for updated power values based on the newly determined nodal voltages case validPowerFlowResult: ValidNewtonRaphsonPFResult => log.debug( "{}", @@ -628,9 +630,8 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { s"$actorName Unable to get results from previous sweep ${gridAgentBaseData.currentSweepNo - 1}!" ) ) - newtonRaphsonPF( - gridModel, - gridAgentBaseData.powerFlowParams.maxIterations, + + val (operatingPoint, slackNodeVoltages) = composeOperatingPointWithUpdatedSlackVoltages( receivedSlackValues, previousSweepData.sweepData, @@ -638,6 +639,12 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { gridModel.gridComponents.transformers3w, gridModel.mainRefSystem ) + + newtonRaphsonPF( + gridModel, + gridAgentBaseData.powerFlowParams.maxIterations, + operatingPoint, + slackNodeVoltages )(gridAgentBaseData.powerFlowParams.epsilon) match { case validPowerFlowResult: ValidNewtonRaphsonPFResult => log.debug( @@ -707,7 +714,10 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { case failedNewtonRaphsonPFResult: FailedNewtonRaphsonPFResult => val powerFlowDoneData = - PowerFlowDoneData(gridAgentBaseData, failedNewtonRaphsonPFResult) + PowerFlowDoneData( + gridAgentBaseData, + failedNewtonRaphsonPFResult + ) log.warning( "Power flow with updated slack voltage did finally not converge!" ) @@ -754,7 +764,7 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { /* This is the highest grid agent, therefore no data is received for the slack node. Suppress, that it is looked * up in the empty store. */ - val operationPoint = composeOperatingPoint( + val (operationPoint, slackNodeVoltages) = composeOperatingPoint( gridModel.gridComponents.nodes, gridModel.gridComponents.transformers, gridModel.gridComponents.transformers3w, @@ -778,7 +788,8 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { newtonRaphsonPF( gridModel, gridAgentBaseData.powerFlowParams.maxIterations, - operationPoint + operationPoint, + slackNodeVoltages )(gridAgentBaseData.powerFlowParams.epsilon) match { case validPowerFlowResult: ValidNewtonRaphsonPFResult => log.debug( diff --git a/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala b/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala index 1bfd23141e..814f6f8de4 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala @@ -6,7 +6,6 @@ package edu.ie3.simona.agent.grid -import java.util.UUID import breeze.math.Complex import edu.ie3.powerflow.NewtonRaphsonPF import edu.ie3.powerflow.model.NodeData.{PresetData, StateData} @@ -16,22 +15,16 @@ import edu.ie3.powerflow.model.StartData.WithForcedStartVoltages import edu.ie3.powerflow.model.enums.NodeType import edu.ie3.simona.agent.grid.ReceivedValues.ReceivedSlackValues import edu.ie3.simona.exceptions.agent.DBFSAlgorithmException -import edu.ie3.simona.model.grid.{ - GridModel, - NodeModel, - RefSystem, - Transformer3wModel, - TransformerModel -} +import edu.ie3.simona.model.grid._ import edu.ie3.simona.ontology.messages.PowerMessage.ProvidePowerMessage import edu.ie3.simona.ontology.messages.VoltageMessage.ProvideSlackVoltageMessage.ExchangeVoltage import edu.ie3.util.quantities.PowerSystemUnits import tech.units.indriya.ComparableQuantity +import tech.units.indriya.quantity.Quantities +import java.util.UUID import javax.measure.Quantity import javax.measure.quantity.{Dimensionless, ElectricPotential} -import tech.units.indriya.quantity.Quantities - import scala.collection.mutable import scala.util.{Failure, Success, Try} @@ -67,7 +60,8 @@ trait PowerFlowSupport { * receivedValuesStore) * @return * current operating point of the grid to be used with - * [[edu.ie3.powerflow.NewtonRaphsonPF.calculate()]] + * [[edu.ie3.powerflow.NewtonRaphsonPF.calculate()]] as well as the complex + * slack node target voltages */ protected def composeOperatingPoint( nodes: Seq[NodeModel], @@ -78,14 +72,13 @@ trait PowerFlowSupport { gridMainRefSystem: RefSystem, targetVoltageFromReceivedData: Boolean = true, ignoreTargetVoltage: Boolean = false - ): Array[PresetData] = { - - val mainRefSystemPowerUnit = gridMainRefSystem.nominalPower.getUnit - - nodes.map { nodeModel => + ): (Array[PresetData], WithForcedStartVoltages) = { + val (operatingPoints, stateData) = nodes.map { nodeModel => // note: currently we only support pq nodes as we not distinguish between pq/pv nodes - // when slack emulators or pv-node assets are added this needs to be considered here val nodeType = if (nodeModel.isSlack) NodeType.SL else NodeType.PQ + + /* Determine the operating point for this given node */ val nodeIdx = nodeUuidToIndexMap.getOrElse( nodeModel.uuid, throw new RuntimeException( @@ -97,6 +90,7 @@ trait PowerFlowSupport { receivedValuesStore.nodeToReceivedPower .get(nodeModel.uuid) match { case Some(actorRefsWithPower) => + val powerUnit = gridMainRefSystem.nominalPower.getUnit val (p, q) = actorRefsWithPower .map { case (_, powerMsg) => powerMsg } .collect { @@ -113,8 +107,8 @@ trait PowerFlowSupport { } .foldLeft( ( - Quantities.getQuantity(0, mainRefSystemPowerUnit), - Quantities.getQuantity(0, mainRefSystemPowerUnit) + Quantities.getQuantity(0, powerUnit), + Quantities.getQuantity(0, powerUnit) ) ) { case ((pSum, qSum), powerMessage) => (pSum.add(powerMessage.p), qSum.add(powerMessage.q)) @@ -127,8 +121,8 @@ trait PowerFlowSupport { case None => new Complex(0, 0) } - val targetVoltageInPu = - if (targetVoltageFromReceivedData && nodeType == NodeType.SL) { + val targetVoltage = + if (targetVoltageFromReceivedData && nodeModel.isSlack) { /* If the preset voltage is meant to be determined by means of received data and the node is a slack node * (only then there is received data), look it up and transform it */ val receivedSlackVoltage = @@ -149,18 +143,59 @@ trait PowerFlowSupport { gridMainRefSystem ) } else { - /* Either, the received data shall not be considered or the node is not a slack node: Depending on if the - * node's is meant to be neglected or not, return a dummy value. */ - Complex.one * (if (!ignoreTargetVoltage) - nodeModel.vTarget - .to(PowerSystemUnits.PU) - .getValue - .doubleValue() - else 1.0) + // Either the received data shall not be considered or the node is not a slack node + Complex.one * + (if (!ignoreTargetVoltage) + nodeModel.vTarget + .to(PowerSystemUnits.PU) + .getValue + .doubleValue() + else 1.0) } - PresetData(nodeIdx, nodeType, apparentPower, targetVoltageInPu.abs) - }.toArray + val optStateData = Option.when(nodeModel.isSlack)( + StateData( + nodeIdx, + NodeType.SL, + targetVoltage, + apparentPower + ) + ) + + ( + PresetData( + nodeIdx, + nodeType, + apparentPower, + targetVoltage.abs + ), + optStateData + ) + }.unzip + + // NOTE: Currently, only one slack node per sub grid is allowed. + val slackNodeData = stateData + .flatten + .minByOption(_.index) + .getOrElse( + throw new DBFSAlgorithmException( + s"Unable to find a slack node." + ) + ) + + /* In case a model has more than one, set all others to PQ nodes. + ATTENTION: This does not cover the power flow situation correctly! */ + val adaptedOperatingPoint = operatingPoints.map { nodePreset => + if (nodePreset.nodeType == NodeType.SL) { + // If this is the slack node we picked, leave it as a slack node. + if (nodePreset.index == slackNodeData.index) nodePreset + // If it is not the one, make it a PQ node. + else nodePreset.copy(nodeType = NodeType.PQ) + } else + nodePreset + } + + (operatingPoints.toArray, WithForcedStartVoltages(Array(slackNodeData))) } /** Composes the current operation point needed by @@ -182,7 +217,8 @@ trait PowerFlowSupport { * instance of [[RefSystem]] of the grid under investigation * @return * current operating point of the grid to be used with - * [[edu.ie3.powerflow.NewtonRaphsonPF.calculate()]] + * [[edu.ie3.powerflow.NewtonRaphsonPF.calculate()]] as well as the complex + * slack node target voltages */ protected def composeOperatingPointWithUpdatedSlackVoltages( receivedSlackValues: ReceivedSlackValues, @@ -190,44 +226,55 @@ trait PowerFlowSupport { transformers2w: Set[TransformerModel], transformers3w: Set[Transformer3wModel], gridMainRefSystem: RefSystem - ): Array[PresetData] = { - sweepDataValues - .map(sweepValueStoreData => { - - val nodeStateData = sweepValueStoreData.stateData - val targetVoltage = if (nodeStateData.nodeType == NodeType.SL) { - val receivedSlackVoltage = receivedSlackValues.values - .map { case (_, slackVoltageMsg) => slackVoltageMsg } - .flatMap(_.nodalSlackVoltages) - .find(_.nodeUuid == sweepValueStoreData.nodeUuid) - .getOrElse( - throw new RuntimeException( - s"Unable to find node with uuid " + - s"${sweepValueStoreData.nodeUuid} in received slack voltage values!" - ) + ): (Array[PresetData], WithForcedStartVoltages) = + sweepDataValues.map { sweepValueStoreData => + val nodeStateData = sweepValueStoreData.stateData + val targetVoltage = if (nodeStateData.nodeType == NodeType.SL) { + val receivedSlackVoltage = receivedSlackValues.values + .map { case (_, slackVoltageMsg) => slackVoltageMsg } + .flatMap(_.nodalSlackVoltages) + .find(_.nodeUuid == sweepValueStoreData.nodeUuid) + .getOrElse( + throw new RuntimeException( + s"Unable to find node with uuid " + + s"${sweepValueStoreData.nodeUuid} in received slack voltage values!" ) - - transformVoltage( - receivedSlackVoltage, - sweepValueStoreData.nodeUuid, - transformers2w, - transformers3w, - gridMainRefSystem ) - } else - new Complex(1, 0) - // note: target voltage will be ignored for slack node if provided + transformVoltage( + receivedSlackVoltage, + sweepValueStoreData.nodeUuid, + transformers2w, + transformers3w, + gridMainRefSystem + ) + } else + Complex.one + + // note: target voltage will be ignored for slack node if provided + ( PresetData( nodeStateData.index, nodeStateData.nodeType, nodeStateData.power, targetVoltage.abs + ), + Option.when(nodeStateData.nodeType == NodeType.SL)( + StateData( + nodeStateData.index, + nodeStateData.nodeType, + targetVoltage, + nodeStateData.power + ) ) - }) - .toArray - - } + ) + }.unzip match { + case (operatingPoint, stateData) => + ( + operatingPoint.toArray, + WithForcedStartVoltages(stateData.flatten.toArray) + ) + } /** A debug method that composes a string with voltage information (in p.u.) * from a [[ValidNewtonRaphsonPFResult]] @@ -411,6 +458,8 @@ trait PowerFlowSupport { * Maximum permissible iterations * @param operatingPoint * Current operation point of the grid + * @param slackVoltages + * Complex target voltages of the slack nodes * @param epsilons * Ascending ordered list of convergence thresholds for relaxation * @return @@ -419,7 +468,8 @@ trait PowerFlowSupport { protected final def newtonRaphsonPF( gridModel: GridModel, maxIterations: Int, - operatingPoint: Array[PresetData] + operatingPoint: Array[PresetData], + slackVoltages: WithForcedStartVoltages )(epsilons: Vector[Double]): PowerFlowResult = { epsilons.headOption match { case Some(epsilon) => @@ -428,42 +478,15 @@ trait PowerFlowSupport { gridModel.nodeUuidToIndexMap, gridModel.gridComponents ) - // / add WithForcedVoltageVector for slackNode - // / as we know that there is at least one slack in every grid available here, we can do a direct call on element zero - - // // NOTE: currently only the first slack node is taken, needs to be adapted when several slacks are present. - val slackNodeData = operatingPoint - .filter(_.nodeType == NodeType.SL) - .minByOption(_.index) - .getOrElse( - throw new DBFSAlgorithmException( - s"Unable to find a slack node in grid ${gridModel.subnetNo}." - ) - ) - val forcedSlackNodeVoltage = WithForcedStartVoltages( - Array(StateData(slackNodeData)) - ) // / execute val powerFlow = NewtonRaphsonPF(epsilon, maxIterations, admittanceMatrix) - /* Currently, only one slack node per sub grid is allowed. In case a model has more than one, set all others to - * PQ nodes. ATTENTION: This does not cover the power flow situation correctly! */ - val adaptedOperatingPoint = operatingPoint.map { nodePreset => - if (nodePreset.nodeType == NodeType.SL) { - /* If this is the first slack node see, leave it as a slack node. If it is not the first one. Make it a - * PQ node. */ - if (nodePreset == slackNodeData) nodePreset - else nodePreset.copy(nodeType = NodeType.PQ) - } else - nodePreset - } - Try { powerFlow.calculate( - adaptedOperatingPoint, - Some(forcedSlackNodeVoltage) + operatingPoint, + Some(slackVoltages) ) }.map { case _: PowerFlowResult.FailedPowerFlowResult if epsilons.size > 1 => @@ -474,7 +497,12 @@ trait PowerFlowSupport { epsilon, epsilonsLeft.headOption.getOrElse("") ) - newtonRaphsonPF(gridModel, maxIterations, operatingPoint)( + newtonRaphsonPF( + gridModel, + maxIterations, + operatingPoint, + slackVoltages + )( epsilonsLeft ) case result => diff --git a/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceProxy.scala b/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceProxy.scala index e0444514b5..003e5ab512 100644 --- a/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceProxy.scala +++ b/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceProxy.scala @@ -8,24 +8,24 @@ package edu.ie3.simona.service.primary import akka.actor.{Actor, ActorRef, PoisonPill, Props} import edu.ie3.datamodel.io.connectors.SqlConnector +import edu.ie3.datamodel.io.csv.CsvIndividualTimeSeriesMetaInformation +import edu.ie3.datamodel.io.naming.timeseries.IndividualTimeSeriesMetaInformation import edu.ie3.datamodel.io.naming.{ DatabaseNamingStrategy, EntityPersistenceNamingStrategy, FileNamingStrategy } -import edu.ie3.datamodel.io.csv.CsvIndividualTimeSeriesMetaInformation -import edu.ie3.datamodel.io.naming.timeseries.IndividualTimeSeriesMetaInformation -import edu.ie3.datamodel.io.source.{ - TimeSeriesMappingSource, - TimeSeriesTypeSource -} import edu.ie3.datamodel.io.source.csv.{ CsvTimeSeriesMappingSource, - CsvTimeSeriesTypeSource + CsvTimeSeriesMetaInformationSource } import edu.ie3.datamodel.io.source.sql.{ SqlTimeSeriesMappingSource, - SqlTimeSeriesTypeSource + SqlTimeSeriesMetaInformationSource +} +import edu.ie3.datamodel.io.source.{ + TimeSeriesMappingSource, + TimeSeriesMetaInformationSource } import edu.ie3.datamodel.models.value.Value import edu.ie3.simona.config.SimonaConfig @@ -67,6 +67,7 @@ import java.text.SimpleDateFormat import java.time.ZonedDateTime import java.util.UUID import scala.Option.when +import scala.compat.java8.OptionConverters.RichOptionalGeneric import scala.jdk.CollectionConverters._ import scala.util.{Failure, Success, Try} @@ -148,15 +149,13 @@ case class PrimaryServiceProxy( createSources(primaryConfig).map { case (mappingSource, metaInformationSource) => val modelToTimeSeries = mappingSource.getMapping.asScala.toMap - val timeSeriesMetaInformation = - metaInformationSource.getTimeSeriesMetaInformation.asScala.toMap - val timeSeriesToSourceRef = modelToTimeSeries.values .to(LazyList) .distinct .flatMap { timeSeriesUuid => - timeSeriesMetaInformation - .get(timeSeriesUuid) match { + metaInformationSource + .getTimeSeriesMetaInformation(timeSeriesUuid) + .asScala match { case Some(metaInformation) => /* Only register those entries, that meet the supported column schemes */ when( @@ -186,7 +185,7 @@ case class PrimaryServiceProxy( private def createSources( primaryConfig: PrimaryConfig - ): Try[(TimeSeriesMappingSource, TimeSeriesTypeSource)] = { + ): Try[(TimeSeriesMappingSource, TimeSeriesMetaInformationSource)] = { Seq( primaryConfig.sqlParams, primaryConfig.influxDb1xParams, @@ -201,7 +200,7 @@ case class PrimaryServiceProxy( directoryPath, fileNamingStrategy ), - new CsvTimeSeriesTypeSource( + new CsvTimeSeriesMetaInformationSource( csvSep, directoryPath, fileNamingStrategy @@ -219,7 +218,7 @@ case class PrimaryServiceProxy( sqlParams.schemaName, new EntityPersistenceNamingStrategy() ), - new SqlTimeSeriesTypeSource( + new SqlTimeSeriesMetaInformationSource( sqlConnector, sqlParams.schemaName, new DatabaseNamingStrategy() diff --git a/src/main/scala/edu/ie3/simona/service/weather/WeatherSourceWrapper.scala b/src/main/scala/edu/ie3/simona/service/weather/WeatherSourceWrapper.scala index cd625a216a..9d7cbb0723 100644 --- a/src/main/scala/edu/ie3/simona/service/weather/WeatherSourceWrapper.scala +++ b/src/main/scala/edu/ie3/simona/service/weather/WeatherSourceWrapper.scala @@ -45,7 +45,7 @@ import edu.ie3.simona.util.TickUtil import edu.ie3.simona.util.TickUtil.TickLong import edu.ie3.util.exceptions.EmptyQuantityException import edu.ie3.util.interval.ClosedInterval -import edu.ie3.util.scala.DoubleUtils.ImplicitDouble +import edu.ie3.util.DoubleUtils.ImplicitDouble import tech.units.indriya.quantity.Quantities import tech.units.indriya.unit.Units diff --git a/src/test/scala/edu/ie3/simona/model/assets/control/QControlSpec.scala b/src/test/scala/edu/ie3/simona/model/assets/control/QControlSpec.scala index fcd76be9b9..63a6eee22d 100644 --- a/src/test/scala/edu/ie3/simona/model/assets/control/QControlSpec.scala +++ b/src/test/scala/edu/ie3/simona/model/assets/control/QControlSpec.scala @@ -71,7 +71,7 @@ class QControlSpec extends UnitSpec with TableDrivenPropertyChecks { ) intercept[QControlException]( QControl(invalidInput) - ).getMessage shouldBe "Got an invalid definition of fixed power factor: cosPhiFixed{points=[CharacteristicCoordinate{x=1 PU, y=2 PU}, CharacteristicCoordinate{x=3 PU, y=4 PU}]}. It may only contain one coordinate" + ).getMessage shouldBe "Got an invalid definition of fixed power factor: cosPhiFixed{points=[CharacteristicCoordinate{x=1 p.u., y=2 p.u.}, CharacteristicCoordinate{x=3 p.u., y=4 p.u.}]}. It may only contain one coordinate" } "parse a valid CosPhiFixed correctly" in { diff --git a/src/test/scala/edu/ie3/simona/service/ev/ExtEvDataServiceSpec.scala b/src/test/scala/edu/ie3/simona/service/ev/ExtEvDataServiceSpec.scala index 5d844053f2..63a1ec44fd 100644 --- a/src/test/scala/edu/ie3/simona/service/ev/ExtEvDataServiceSpec.scala +++ b/src/test/scala/edu/ie3/simona/service/ev/ExtEvDataServiceSpec.scala @@ -741,7 +741,7 @@ class ExtEvDataServiceSpec extData.receiveTriggerQueue.size() shouldBe 1 // only evcs 1 should be included, the other one is full extData.receiveTriggerQueue.take() shouldBe new ProvideEvcsFreeLots( - Map(evcs1UUID -> new Integer(2)).asJava + Map(evcs1UUID -> Integer.valueOf(2)).asJava ) } diff --git a/src/test/scala/edu/ie3/simona/test/common/input/TimeSeriesTestData.scala b/src/test/scala/edu/ie3/simona/test/common/input/TimeSeriesTestData.scala index 64d674e3ad..492a3124d9 100644 --- a/src/test/scala/edu/ie3/simona/test/common/input/TimeSeriesTestData.scala +++ b/src/test/scala/edu/ie3/simona/test/common/input/TimeSeriesTestData.scala @@ -6,10 +6,8 @@ package edu.ie3.simona.test.common.input -import edu.ie3.datamodel.io.naming.timeseries.{ - ColumnScheme, - IndividualTimeSeriesMetaInformation -} +import edu.ie3.datamodel.io.csv.CsvIndividualTimeSeriesMetaInformation +import edu.ie3.datamodel.io.naming.timeseries.ColumnScheme import java.util.UUID @@ -21,19 +19,22 @@ trait TimeSeriesTestData { protected val uuidPqh: UUID = UUID.fromString("46be1e57-e4ed-4ef7-95f1-b2b321cb2047") - protected val metaP: IndividualTimeSeriesMetaInformation = - new IndividualTimeSeriesMetaInformation( + protected val metaP: CsvIndividualTimeSeriesMetaInformation = + new CsvIndividualTimeSeriesMetaInformation( uuidP, - ColumnScheme.ACTIVE_POWER + ColumnScheme.ACTIVE_POWER, + s"its_p_$uuidP" ) - protected val metaPq: IndividualTimeSeriesMetaInformation = - new IndividualTimeSeriesMetaInformation( + protected val metaPq: CsvIndividualTimeSeriesMetaInformation = + new CsvIndividualTimeSeriesMetaInformation( uuidPq, - ColumnScheme.APPARENT_POWER + ColumnScheme.APPARENT_POWER, + s"its_pq_$uuidPq" ) - protected val metaPqh: IndividualTimeSeriesMetaInformation = - new IndividualTimeSeriesMetaInformation( + protected val metaPqh: CsvIndividualTimeSeriesMetaInformation = + new CsvIndividualTimeSeriesMetaInformation( uuidPqh, - ColumnScheme.APPARENT_POWER_AND_HEAT_DEMAND + ColumnScheme.APPARENT_POWER_AND_HEAT_DEMAND, + s"its_pqh_$uuidPqh" ) } From 1daeab04bebd3e4a1dd604668d7f505417188de2 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Sun, 31 Jul 2022 19:43:04 +0200 Subject: [PATCH 32/36] Fixing the merge --- .../edu/ie3/simona/agent/grid/PowerFlowSupport.scala | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala b/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala index 814f6f8de4..076dbc42b1 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala @@ -174,8 +174,7 @@ trait PowerFlowSupport { }.unzip // NOTE: Currently, only one slack node per sub grid is allowed. - val slackNodeData = stateData - .flatten + val slackNodeData = stateData.flatten .minByOption(_.index) .getOrElse( throw new DBFSAlgorithmException( @@ -195,7 +194,10 @@ trait PowerFlowSupport { nodePreset } - (operatingPoints.toArray, WithForcedStartVoltages(Array(slackNodeData))) + ( + adaptedOperatingPoint.toArray, + WithForcedStartVoltages(Array(slackNodeData)) + ) } /** Composes the current operation point needed by From d99ce36e3d8fe9e8f7c223e48fc01e7abe754c17 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Sun, 31 Jul 2022 20:31:09 +0200 Subject: [PATCH 33/36] Applying test adaptations of #71 (slack voltage angle) --- .../agent/grid/DBFSAlgorithmCenGridSpec.scala | 41 ++++++++++--------- 1 file changed, 21 insertions(+), 20 deletions(-) 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 850bfac8ad..263bd07a6c 100644 --- a/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmCenGridSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmCenGridSpec.scala @@ -330,6 +330,7 @@ class DBFSAlgorithmCenGridSpec ) ) + // 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) @@ -366,10 +367,10 @@ class DBFSAlgorithmCenGridSpec Seq( ExchangeVoltage( supNodeB.getUuid, - Quantities.getQuantity(380, KILOVOLT), - Quantities.getQuantity(0, KILOVOLT) + Quantities.getQuantity(374.22694614463, KILOVOLT), // 380 kV @ 10° + Quantities.getQuantity(65.9863075134335, KILOVOLT) // 380 kV @ 10° ), - ExchangeVoltage( + ExchangeVoltage( // this one should currently be ignored anyways supNodeA.getUuid, Quantities.getQuantity(380, KILOVOLT), Quantities.getQuantity(0, KILOVOLT) @@ -393,50 +394,50 @@ class DBFSAlgorithmCenGridSpec // 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) + inferiorGrid11.requestSlackVoltage(centerGridAgent, secondSweepNo) - inferiorGrid12.requestSlackVoltage(centerGridAgent, firstSweepNo) + inferiorGrid12.requestSlackVoltage(centerGridAgent, secondSweepNo) - inferiorGrid13.requestSlackVoltage(centerGridAgent, firstSweepNo) + 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( - firstSweepNo, + secondSweepNo, Seq( ExchangeVoltage( node1.getUuid, - Quantities.getQuantity(110.156504579861168, KILOVOLT), - Quantities.getQuantity(-0.02700804012678551, KILOVOLT) + Quantities.getQuantity(108.487669651919932, KILOVOLT), + Quantities.getQuantity(19.101878551141232, KILOVOLT) ) ) ) inferiorGrid12.expectSlackVoltageProvision( - firstSweepNo, + secondSweepNo, Seq( ExchangeVoltage( node2.getUuid, - Quantities.getQuantity(110.11927849702049, KILOVOLT), - Quantities.getQuantity(-0.01594978168595568, KILOVOLT) + Quantities.getQuantity(108.449088870497683, KILOVOLT), + Quantities.getQuantity(19.10630456834157630, KILOVOLT) ) ) ) inferiorGrid13.expectSlackVoltageProvision( - firstSweepNo, + secondSweepNo, Seq( ExchangeVoltage( node3a.getUuid, - Quantities.getQuantity(110.13956933728223, KILOVOLT), - Quantities.getQuantity(-0.02145845898589436, KILOVOLT) + Quantities.getQuantity(108.470028019077087, KILOVOLT), + Quantities.getQuantity(19.104403047662570, KILOVOLT) ), ExchangeVoltage( node3b.getUuid, - Quantities.getQuantity(110.151555766674, KILOVOLT), - Quantities.getQuantity(-0.02544502304127, KILOVOLT) + Quantities.getQuantity(108.482524607256866, KILOVOLT), + Quantities.getQuantity(19.1025584700935336, KILOVOLT) ) ) ) @@ -492,8 +493,8 @@ class DBFSAlgorithmCenGridSpec ), ExchangePower( supNodeB.getUuid, - Quantities.getQuantity(0.160905770717798, MEGAWATT), - Quantities.getQuantity(-1.4535602349123878, MEGAVAR) + Quantities.getQuantity(0.16090577067051856, MEGAWATT), + Quantities.getQuantity(-1.4535602358772026, MEGAVAR) ) ) ) @@ -585,7 +586,7 @@ object DBFSAlgorithmCenGridSpec extends UnitSpec { def expectGridPowerProvision( expectedExchangedPowers: Seq[ExchangePower] ): Unit = { - inside(gaProbe.expectMsgType[ProvideGridPowerMessage]) { + inside(gaProbe.expectMsgType[ProvideGridPowerMessage](10.seconds)) { case ProvideGridPowerMessage(exchangedPower) => exchangedPower should have size expectedExchangedPowers.size From e0aa25123d878fea40e829006be74baa8a6b8bad Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Mon, 1 Aug 2022 14:33:51 +0200 Subject: [PATCH 34/36] Renaming ReceivedSlackValues to ReceivedSlackVoltageValues --- .../scala/edu/ie3/simona/agent/grid/DBFSAlgorithm.scala | 8 ++++---- .../scala/edu/ie3/simona/agent/grid/GridAgentData.scala | 8 ++++---- .../edu/ie3/simona/agent/grid/PowerFlowSupport.scala | 4 ++-- .../scala/edu/ie3/simona/agent/grid/ReceivedValues.scala | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) 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 7a6d9ad8bf..8bcc7bace4 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/DBFSAlgorithm.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/DBFSAlgorithm.scala @@ -121,7 +121,7 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { case receivedPowers: ReceivedPowerValues => /* Can be a message from an asset or a message from an inferior grid */ gridAgentBaseData.updateWithReceivedPowerValues(receivedPowers) - case receivedSlacks: ReceivedSlackValues => + case receivedSlacks: ReceivedSlackVoltageValues => gridAgentBaseData.updateWithReceivedSlackVoltages(receivedSlacks) case unknownReceivedValues => throw new DBFSAlgorithmException( @@ -615,7 +615,7 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { // this means we requested an update of the slack voltage values, but for now don't request (and hence don't expect) // updated power values for our power flow calculations case Event( - receivedSlackValues: ReceivedSlackValues, + receivedSlackValues: ReceivedSlackVoltageValues, gridAgentBaseData: GridAgentBaseData ) => log.debug( @@ -1205,7 +1205,7 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { subGridGateToActorRef: Map[SubGridGate, ActorRef], superiorGridGates: Vector[SubGridGate], askTimeout: Duration - ): Option[Future[ReceivedSlackValues]] = { + ): Option[Future[ReceivedSlackVoltageValues]] = { implicit val timeout: AkkaTimeout = AkkaTimeout.create(askTimeout) log.debug( s"asking superior grids for slack voltage values: {}", @@ -1227,7 +1227,7 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { } .toVector ) - .map(ReceivedSlackValues) + .map(ReceivedSlackVoltageValues) .pipeTo(self) } } 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 58bebac08b..140f5c4330 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/GridAgentData.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/GridAgentData.scala @@ -14,7 +14,7 @@ import edu.ie3.powerflow.model.PowerFlowResult import edu.ie3.powerflow.model.PowerFlowResult.SuccessFullPowerFlowResult.ValidNewtonRaphsonPFResult import edu.ie3.simona.agent.grid.ReceivedValues.{ ReceivedPowerValues, - ReceivedSlackValues + ReceivedSlackVoltageValues } import edu.ie3.simona.agent.grid.ReceivedValuesStore.NodeToReceivedPower import edu.ie3.simona.model.grid.{GridModel, RefSystem} @@ -370,8 +370,8 @@ object GridAgentData { } .map { case (uuid, _) => uuid } - /** Update this [[GridAgentBaseData]] with [[ReceivedSlackValues]] and - * return a copy of this [[GridAgentBaseData]] for further processing + /** Update this [[GridAgentBaseData]] with [[ReceivedSlackVoltageValues]] + * and return a copy of this [[GridAgentBaseData]] for further processing * * @param receivedSlackValues * the slack voltage values that should be used for the update @@ -380,7 +380,7 @@ object GridAgentData { * receivedSlackValues */ def updateWithReceivedSlackVoltages( - receivedSlackValues: ReceivedSlackValues + receivedSlackValues: ReceivedSlackVoltageValues ): GridAgentBaseData = { val updatedNodeToReceivedSlackVoltageValuesMap = receivedSlackValues.values.flatMap { case (senderRef, slackValues) => diff --git a/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala b/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala index 076dbc42b1..3a74fc314f 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala @@ -13,7 +13,7 @@ import edu.ie3.powerflow.model.PowerFlowResult import edu.ie3.powerflow.model.PowerFlowResult.SuccessFullPowerFlowResult.ValidNewtonRaphsonPFResult import edu.ie3.powerflow.model.StartData.WithForcedStartVoltages import edu.ie3.powerflow.model.enums.NodeType -import edu.ie3.simona.agent.grid.ReceivedValues.ReceivedSlackValues +import edu.ie3.simona.agent.grid.ReceivedValues.ReceivedSlackVoltageValues import edu.ie3.simona.exceptions.agent.DBFSAlgorithmException import edu.ie3.simona.model.grid._ import edu.ie3.simona.ontology.messages.PowerMessage.ProvidePowerMessage @@ -223,7 +223,7 @@ trait PowerFlowSupport { * slack node target voltages */ protected def composeOperatingPointWithUpdatedSlackVoltages( - receivedSlackValues: ReceivedSlackValues, + receivedSlackValues: ReceivedSlackVoltageValues, sweepDataValues: Vector[SweepValueStore.SweepValueStoreData], transformers2w: Set[TransformerModel], transformers3w: Set[Transformer3wModel], diff --git a/src/main/scala/edu/ie3/simona/agent/grid/ReceivedValues.scala b/src/main/scala/edu/ie3/simona/agent/grid/ReceivedValues.scala index 7c578c858a..a4818ce0c6 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/ReceivedValues.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/ReceivedValues.scala @@ -47,7 +47,7 @@ object ReceivedValues { * @param values * the slack voltage values and their senders */ - final case class ReceivedSlackValues( + final case class ReceivedSlackVoltageValues( values: Vector[ActorSlackVoltageRequestResponse] ) extends ReceivedValues From eb9097ec1ca12dce78378f170e02add9b09666d3 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Mon, 1 Aug 2022 14:36:43 +0200 Subject: [PATCH 35/36] Added comment to nodes sequence conversion --- src/main/scala/edu/ie3/simona/model/grid/GridModel.scala | 3 +++ 1 file changed, 3 insertions(+) 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 f58bb10d9c..64a7686215 100644 --- a/src/main/scala/edu/ie3/simona/model/grid/GridModel.scala +++ b/src/main/scala/edu/ie3/simona/model/grid/GridModel.scala @@ -496,6 +496,9 @@ case object GridModel { // build // / nodes + // // the set of nodes is converted to a sequence here, since the + // // order of nodes is important for data preparations related to + // // power flow calculation val nodes = subGridContainer.getRawGrid.getNodes.asScala.toSeq.map { nodeInput => NodeModel(nodeInput, startDate, endDate) } From bc66dc3b4decd12856db006a991af86e91d41f93 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Mon, 1 Aug 2022 21:01:08 +0200 Subject: [PATCH 36/36] Replacing RichQuantity and RichUnit with the new copies in PSU --- .../edu/ie3/simona/model/grid/RefSystem.scala | 2 +- .../simona/model/participant/EvcsModel.scala | 2 +- .../util/scala/quantities/QuantityUtil.scala | 44 +------------------ .../util/quantities/QuantityUtilSpec.scala | 33 -------------- 4 files changed, 3 insertions(+), 78 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/model/grid/RefSystem.scala b/src/main/scala/edu/ie3/simona/model/grid/RefSystem.scala index 50df2c4e49..4a4b550daf 100644 --- a/src/main/scala/edu/ie3/simona/model/grid/RefSystem.scala +++ b/src/main/scala/edu/ie3/simona/model/grid/RefSystem.scala @@ -8,8 +8,8 @@ package edu.ie3.simona.model.grid import breeze.math.Complex import edu.ie3.util.quantities.PowerSystemUnits._ +import edu.ie3.util.quantities.QuantityUtils.RichUnit import edu.ie3.util.quantities.{PowerSystemUnits, QuantityUtil} -import edu.ie3.util.scala.quantities.QuantityUtil.RichUnit import javax.measure.Quantity import javax.measure.quantity._ diff --git a/src/main/scala/edu/ie3/simona/model/participant/EvcsModel.scala b/src/main/scala/edu/ie3/simona/model/participant/EvcsModel.scala index 84e7bdb380..c3f314fb43 100644 --- a/src/main/scala/edu/ie3/simona/model/participant/EvcsModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant/EvcsModel.scala @@ -17,9 +17,9 @@ import edu.ie3.simona.model.participant.control.QControl import edu.ie3.simona.util.TickUtil.TickLong import edu.ie3.util.quantities.PowerSystemUnits import edu.ie3.util.quantities.PowerSystemUnits.{MEGAVAR, MEGAWATT} +import edu.ie3.util.quantities.QuantityUtils.RichQuantity import edu.ie3.util.scala.OperationInterval import edu.ie3.util.scala.quantities.QuantityUtil -import edu.ie3.util.scala.quantities.QuantityUtil.RichQuantity import tech.units.indriya.ComparableQuantity import tech.units.indriya.quantity.Quantities diff --git a/src/main/scala/edu/ie3/util/scala/quantities/QuantityUtil.scala b/src/main/scala/edu/ie3/util/scala/quantities/QuantityUtil.scala index 143c5ac73f..7253e2b70e 100644 --- a/src/main/scala/edu/ie3/util/scala/quantities/QuantityUtil.scala +++ b/src/main/scala/edu/ie3/util/scala/quantities/QuantityUtil.scala @@ -8,14 +8,12 @@ package edu.ie3.util.scala.quantities import edu.ie3.simona.exceptions.QuantityException import edu.ie3.util.quantities.{QuantityUtil => PSQuantityUtil} - -import javax.measure -import javax.measure.Quantity import tech.units.indriya.ComparableQuantity import tech.units.indriya.function.Calculus import tech.units.indriya.quantity.Quantities import tech.units.indriya.unit.Units +import javax.measure.Quantity import scala.collection.mutable import scala.util.{Failure, Try} @@ -266,44 +264,4 @@ object QuantityUtil { } } - implicit class RichQuantity[Q <: Quantity[Q]]( - private val q: ComparableQuantity[Q] - ) extends AnyVal { - - /** Returns the smaller of two Quantities - * - * @param other - * the other Quantity - * @return - * the smaller of both Quantities - */ - def min(other: ComparableQuantity[Q]): ComparableQuantity[Q] = { - if (q.isLessThanOrEqualTo(other)) q else other - } - - /** Returns the bigger of two Quantities - * - * @param other - * the other Quantity - * @return - * the bigger of both Quantities - */ - def max(other: ComparableQuantity[Q]): ComparableQuantity[Q] = { - if (q.isGreaterThan(other)) q else other - } - } - - implicit class RichUnit[Q <: Quantity[Q]]( - private val unit: measure.Unit[Q] - ) extends AnyVal { - - /** Transform some power unit to given unit with the same prefix - * @param targetUnit - * the target system unit - * @return - * this unit converted to given - */ - def toEquivalentIn(targetUnit: measure.Unit[Q]): measure.Unit[Q] = - targetUnit.transform(unit.getConverterTo(unit.getSystemUnit)) - } } diff --git a/src/test/scala/edu/ie3/util/quantities/QuantityUtilSpec.scala b/src/test/scala/edu/ie3/util/quantities/QuantityUtilSpec.scala index c2b3f0faec..44c9bd2902 100644 --- a/src/test/scala/edu/ie3/util/quantities/QuantityUtilSpec.scala +++ b/src/test/scala/edu/ie3/util/quantities/QuantityUtilSpec.scala @@ -8,12 +8,9 @@ package edu.ie3.util.quantities import edu.ie3.simona.exceptions.QuantityException import edu.ie3.simona.test.common.UnitSpec -import edu.ie3.util.quantities.PowerSystemUnits._ import edu.ie3.util.scala.quantities.QuantityUtil -import edu.ie3.util.scala.quantities.QuantityUtil.RichUnit import org.scalatest.prop.TableDrivenPropertyChecks import tech.units.indriya.quantity.Quantities -import tech.units.indriya.unit.Units.WATT import tech.units.indriya.unit.{ProductUnit, Units} import javax.measure.Quantity @@ -187,34 +184,4 @@ class QuantityUtilSpec extends UnitSpec with TableDrivenPropertyChecks { } } } - - "Converting units to alternative units" should { - "succeed if units are compatible" in { - val cases = Table( - ("sourceUnit", "targetUnit", "expectedUnit"), - (VOLTAMPERE, WATT, WATT), - (KILOVOLTAMPERE, WATT, KILOWATT), - (MEGAVOLTAMPERE, WATT, MEGAWATT), - (VAR, WATT, WATT), - (KILOVAR, WATT, KILOWATT), - (MEGAVAR, WATT, MEGAWATT), - (VOLTAMPERE, VAR, VAR), - (KILOVOLTAMPERE, VAR, KILOVAR), - (MEGAVOLTAMPERE, VAR, MEGAVAR), - (WATT, VAR, VAR), - (KILOWATT, VAR, KILOVAR), - (MEGAWATT, VAR, MEGAVAR), - (VAR, VOLTAMPERE, VOLTAMPERE), - (KILOVAR, VOLTAMPERE, KILOVOLTAMPERE), - (MEGAVAR, VOLTAMPERE, MEGAVOLTAMPERE), - (WATT, VOLTAMPERE, VOLTAMPERE), - (KILOWATT, VOLTAMPERE, KILOVOLTAMPERE), - (MEGAWATT, VOLTAMPERE, MEGAVOLTAMPERE) - ) - - forAll(cases) { (sourceUnit, targetUnit, expectedUnit) => - sourceUnit.toEquivalentIn(targetUnit) shouldBe expectedUnit - } - } - } }