From 3e44121b44775b4bbf092b76a18d7aa8eead6432 Mon Sep 17 00:00:00 2001 From: "Kittl, Chris" Date: Tue, 11 Jan 2022 09:31:01 +0100 Subject: [PATCH 1/6] Respect for slack voltage angle in distributed power flow calculation --- CHANGELOG.md | 5 +- .../ie3/simona/agent/grid/DBFSAlgorithm.scala | 196 ++++++----- .../simona/agent/grid/PowerFlowSupport.scala | 332 ++++++++++++------ .../agent/grid/DBFSAlgorithmCenGridSpec.scala | 34 +- .../EvcsAgentModelCalculationSpec.scala | 2 +- 5 files changed, 351 insertions(+), 218 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c47d5ac600..e56da64ce4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,4 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Improving code readability in EvcsAgent by moving FreeLotsRequest to separate methods -[Unreleased]: https://github.com/ie3-institute/simona +### Fixed +- Respect for voltage angle in DBFS slack voltage exchange protocol + +[Unreleased]: https://github.com/ie3-institute/simona/compare/a14a093239f58fca9b2b974712686b33e5e5f939...HEAD 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..50ed4fc77b 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/DBFSAlgorithm.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/DBFSAlgorithm.scala @@ -436,18 +436,23 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { ) val gridModel = gridAgentBaseData.gridEnv.gridModel - val powerFlowResult = newtonRaphsonPF( - gridModel, - gridAgentBaseData.powerFlowParams.maxIterations, - composeOperatingPoint( - gridModel.gridComponents.nodes, - gridModel.gridComponents.transformers, - gridModel.gridComponents.transformers3w, - gridModel.nodeUuidToIndexMap, - gridAgentBaseData.receivedValueStore, - gridModel.mainRefSystem - ) - )(gridAgentBaseData.powerFlowParams.epsilon) + + val powerFlowResult = composeOperatingPoint( + gridModel.gridComponents.nodes, + gridModel.gridComponents.transformers, + gridModel.gridComponents.transformers3w, + gridModel.nodeUuidToIndexMap, + gridAgentBaseData.receivedValueStore, + gridModel.mainRefSystem + ) match { + case (operatingPoint, slackNodeVoltages) => + newtonRaphsonPF( + gridModel, + gridAgentBaseData.powerFlowParams.maxIterations, + operatingPoint, + slackNodeVoltages + )(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 { @@ -586,92 +591,100 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { s"$actorName Unable to get results from previous sweep ${gridAgentBaseData.currentSweepNo - 1}!" ) ) - newtonRaphsonPF( - gridModel, - gridAgentBaseData.powerFlowParams.maxIterations, - composeOperatingPointWithUpdatedSlackVoltages( - receivedSlackValues, - previousSweepData.sweepData, - gridModel.gridComponents.transformers, - gridModel.gridComponents.transformers3w, - gridModel.mainRefSystem - ) - )(gridAgentBaseData.powerFlowParams.epsilon) match { - case validPowerFlowResult: ValidNewtonRaphsonPFResult => - log.debug( - "{}", - composeValidNewtonRaphsonPFResultVoltagesDebugString( - validPowerFlowResult, - gridModel - ) - ) - - // update the data - val sweepValueStore = SweepValueStore( - validPowerFlowResult, - gridModel.gridComponents.nodes, - gridModel.nodeUuidToIndexMap - ) - val updatedSweepValueStore = - gridAgentBaseData.sweepValueStores + (gridAgentBaseData.currentSweepNo -> sweepValueStore) - - // send request to child grids and assets for updated p/q values - // we start the grid simulation by requesting the p/q values of all the nodes we are responsible for - // as well as the slack voltage power from our superior grid - // 1. assets p/q values - val askForAssetPowersOpt = - askForAssetPowers( - currentTick, - Some(sweepValueStore), - gridAgentBaseData.gridEnv.nodeToAssetAgents, - gridModel.mainRefSystem, - gridAgentBaseData.powerFlowParams.sweepTimeout - ) - // 2. inferior grids p/q values - val askForInferiorGridPowersOpt = - askInferiorGridsForPowers( - gridAgentBaseData.currentSweepNo, - gridAgentBaseData.gridEnv.subnetGateToActorRef, - gridAgentBaseData.inferiorGridGates, - gridAgentBaseData.powerFlowParams.sweepTimeout - ) - - // when we don't have inferior grids and no assets both methods return None and we can skip doing another power - // flow calculation otherwise we go back to simulate grid and wait for the answers - (askForAssetPowersOpt, askForInferiorGridPowersOpt) match { - case (None, None) => + composeOperatingPointWithUpdatedSlackVoltages( + receivedSlackValues, + previousSweepData.sweepData, + gridModel.gridComponents.transformers, + gridModel.gridComponents.transformers3w, + gridModel.mainRefSystem + ) match { + case (operatingPoint, slackNodeVoltages) => + newtonRaphsonPF( + gridModel, + gridAgentBaseData.powerFlowParams.maxIterations, + operatingPoint, + slackNodeVoltages + )(gridAgentBaseData.powerFlowParams.epsilon) match { + case validPowerFlowResult: ValidNewtonRaphsonPFResult => log.debug( - "I don't have assets or child grids. " + - "Going back to SimulateGrid and provide the power flow result if there is any request left." + "{}", + composeValidNewtonRaphsonPFResultVoltagesDebugString( + validPowerFlowResult, + gridModel + ) ) - val powerFlowDoneData = - PowerFlowDoneData(gridAgentBaseData, validPowerFlowResult) + // update the data + val sweepValueStore = SweepValueStore( + validPowerFlowResult, + gridModel.gridComponents.nodes, + gridModel.nodeUuidToIndexMap + ) + val updatedSweepValueStore = + gridAgentBaseData.sweepValueStores + (gridAgentBaseData.currentSweepNo -> sweepValueStore) + + // send request to child grids and assets for updated p/q values + // we start the grid simulation by requesting the p/q values of all the nodes we are responsible for + // as well as the slack voltage power from our superior grid + // 1. assets p/q values + val askForAssetPowersOpt = + askForAssetPowers( + currentTick, + Some(sweepValueStore), + gridAgentBaseData.gridEnv.nodeToAssetAgents, + gridModel.mainRefSystem, + gridAgentBaseData.powerFlowParams.sweepTimeout + ) - unstashAll() // we can answer the stashed grid power requests now - goto(SimulateGrid) using powerFlowDoneData + // 2. inferior grids p/q values + val askForInferiorGridPowersOpt = + askInferiorGridsForPowers( + gridAgentBaseData.currentSweepNo, + gridAgentBaseData.gridEnv.subnetGateToActorRef, + gridAgentBaseData.inferiorGridGates, + gridAgentBaseData.powerFlowParams.sweepTimeout + ) - case _ => - log.debug( - "Going back to SimulateGrid and wait for my assets or inferior grids to return." - ) + // when we don't have inferior grids and no assets both methods return None and we can skip doing another power + // flow calculation otherwise we go back to simulate grid and wait for the answers + (askForAssetPowersOpt, askForInferiorGridPowersOpt) match { + case (None, None) => + log.debug( + "I don't have assets or child grids. " + + "Going back to SimulateGrid and provide the power flow result if there is any request left." + ) - // go back to simulate grid - goto(SimulateGrid) using gridAgentBaseData - .updateWithReceivedSlackVoltages(receivedSlackValues) - .copy(sweepValueStores = updatedSweepValueStore) - } + val powerFlowDoneData = + PowerFlowDoneData(gridAgentBaseData, validPowerFlowResult) - case failedNewtonRaphsonPFResult: FailedNewtonRaphsonPFResult => - val powerFlowDoneData = - PowerFlowDoneData(gridAgentBaseData, failedNewtonRaphsonPFResult) - log.warning( - "Power flow with updated slack voltage did finally not converge!" - ) - unstashAll() // we can answer the stashed grid power requests now and report a failed power flow back - goto(SimulateGrid) using powerFlowDoneData + unstashAll() // we can answer the stashed grid power requests now + goto(SimulateGrid) using powerFlowDoneData + + case _ => + log.debug( + "Going back to SimulateGrid and wait for my assets or inferior grids to return." + ) + + // go back to simulate grid + goto(SimulateGrid) using gridAgentBaseData + .updateWithReceivedSlackVoltages(receivedSlackValues) + .copy(sweepValueStores = updatedSweepValueStore) + } + case failedNewtonRaphsonPFResult: FailedNewtonRaphsonPFResult => + val powerFlowDoneData = + PowerFlowDoneData( + gridAgentBaseData, + failedNewtonRaphsonPFResult + ) + log.warning( + "Power flow with updated slack voltage did finally not converge!" + ) + unstashAll() // we can answer the stashed grid power requests now and report a failed power flow back + goto(SimulateGrid) using powerFlowDoneData + + } } // happens only when we received slack data and power values before we received a request to provide grid data @@ -712,7 +725,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, @@ -736,7 +749,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 ebf017b22d..03beff21f9 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala @@ -6,8 +6,11 @@ package edu.ie3.simona.agent.grid +import akka.actor.ActorRef + import java.util.UUID import breeze.math.Complex +import edu.ie3.datamodel.models.input.connector.Transformer2WInput import edu.ie3.powerflow.NewtonRaphsonPF import edu.ie3.powerflow.model.NodeData.{PresetData, StateData} import edu.ie3.powerflow.model.PowerFlowResult @@ -23,13 +26,14 @@ import edu.ie3.simona.model.grid.{ Transformer3wModel, TransformerModel } +import edu.ie3.simona.ontology.messages.PowerMessage import edu.ie3.simona.ontology.messages.PowerMessage.ProvidePowerMessage import edu.ie3.simona.ontology.messages.VoltageMessage.ProvideSlackVoltageMessage import edu.ie3.util.quantities.PowerSystemUnits import tech.units.indriya.ComparableQuantity import javax.measure.Quantity -import javax.measure.quantity.{Dimensionless, ElectricPotential} +import javax.measure.quantity.{Dimensionless, ElectricPotential, Power} import tech.units.indriya.quantity.Quantities import scala.annotation.tailrec @@ -66,7 +70,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: Set[NodeModel], @@ -77,93 +82,195 @@ trait PowerFlowSupport { gridMainRefSystem: RefSystem, targetVoltageFromReceivedData: Boolean = true, ignoreTargetVoltage: Boolean = false - ): Array[PresetData] = { - - val mainRefSystemPowerUnit = gridMainRefSystem.nominalPower.getUnit + ): (Array[PresetData], WithForcedStartVoltages) = + nodes.foldLeft((Array.empty[PresetData], Array.empty[StateData])) { + case ((operatingPoint, stateData), 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 - nodes.foldLeft(Array.empty[PresetData])((operatingPoint, 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 - val nodeIdx = nodeUuidToIndexMap.getOrElse( - nodeModel.uuid, - throw new RuntimeException( - s"Received data for node ${nodeModel.id} [${nodeModel.uuid}] which is not in my nodeUuidToIndexMap!" + /* Determine the operating point for this given node */ + val nodeIdx = nodeUuidToIndexMap.getOrElse( + nodeModel.uuid, + throw new RuntimeException( + s"Received data for node ${nodeModel.id} [${nodeModel.uuid}] which is not in my nodeUuidToIndexMap!" + ) ) - ) - val apparentPower: Complex = - receivedValuesStore.nodeToReceivedPower - .get(nodeModel.uuid) match { - case Some(actorRefsWithPower) => - val (p, q) = actorRefsWithPower - .map(_._2) - .collect { - case Some(providePowerMessage: ProvidePowerMessage) => - providePowerMessage - case Some(message) => - throw new RuntimeException( - s"Received message $message which cannot processed here!" - ) - case None => - throw new RuntimeException( - s"Did not receive all power values I expected @ node ${nodeModel.id}. This is a fatal and should never happen!" - ) - } - .foldLeft( - ( - Quantities.getQuantity(0, mainRefSystemPowerUnit), - Quantities.getQuantity(0, mainRefSystemPowerUnit) - ) - )((pqSum, powerMessage) => { - (pqSum._1.add(powerMessage.p), pqSum._2.add(powerMessage.q)) - }) + /* Determine the nodal residual apparent power */ + val apparentPower = determineNodalApparentPower( + receivedValuesStore.nodeToReceivedPower.get(nodeModel.uuid), + nodeModel.id, + gridMainRefSystem + ) - new Complex( - gridMainRefSystem.pInPu(p).getValue.doubleValue(), - gridMainRefSystem.qInPu(q).getValue.doubleValue() + /* Determine node type, target voltage and updated state data (dependent on the node type) */ + nodeTypeTargetVoltageStateData( + nodeModel, + nodeIdx, + receivedValuesStore, + transformers2w, + transformers3w, + apparentPower, + stateData, + gridMainRefSystem, + targetVoltageFromReceivedData, + ignoreTargetVoltage + ) match { + case (nodeType, targetVoltage, stateData) => + ( + operatingPoint :+ PresetData( + nodeIdx, + nodeType, + apparentPower, + targetVoltage.abs + ), + stateData ) - case None => new Complex(0, 0) } + } match { + case (operatingPoint, stateData) => + (operatingPoint, WithForcedStartVoltages(stateData)) + } - val targetVoltageInPu = - if (targetVoltageFromReceivedData && nodeType == NodeType.SL) { - /* 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) - .getOrElse( - throw new RuntimeException( - s"No slack voltage received for node ${nodeModel.id} [${nodeModel.uuid}]!" - ) - ) - - transformVoltage( - receivedSlackVoltage, - nodeModel.uuid, - transformers2w, - transformers3w, - 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) + /** Determine the residual apparent nodal power balance for a given node. + * + * @param receivedPower + * Optional vector of received power messages + * @param nodeId + * Id of the node (for debugging reasons) + * @param refSystem + * Main reference system of the grid + * @return + */ + private def determineNodalApparentPower( + receivedPower: Option[ + Vector[(ActorRef, Option[PowerMessage.PowerResponseMessage])] + ], + nodeId: String, + refSystem: RefSystem + ): Complex = receivedPower match { + case Some(actorRefsWithPower) => + val powerUnit = refSystem.nominalPower.getUnit + val (p, q) = actorRefsWithPower + .map(_._2) + .collect { + case Some(providePowerMessage: ProvidePowerMessage) => + providePowerMessage + case Some(message) => + throw new RuntimeException( + s"Received message $message which cannot processed here!" + ) + case None => + throw new RuntimeException( + s"Did not receive all power values I expected @ node $nodeId. This is a fatal and should never happen!" + ) } + .foldLeft( + ( + Quantities.getQuantity(0, powerUnit), + Quantities.getQuantity(0, powerUnit) + ) + )((pqSum, powerMessage) => { + (pqSum._1.add(powerMessage.p), pqSum._2.add(powerMessage.q)) + }) - val presetNodeData = - PresetData(nodeIdx, nodeType, apparentPower, targetVoltageInPu.abs) + new Complex( + refSystem.pInPu(p).getValue.doubleValue(), + refSystem.qInPu(q).getValue.doubleValue() + ) + case None => new Complex(0, 0) + } - operatingPoint :+ presetNodeData + /** Determine a target voltage if no received values are meant to be taken + * + * @param nodeModel + * The nodal model + * @param ignoreTargetVoltage + * Whether or not to ignore the nodal target voltage provided by input + * model + * @return + * A complex nodal target voltage + */ + private def nodalTargetVoltage( + nodeModel: NodeModel, + ignoreTargetVoltage: Boolean + ): Complex = Complex.one * (if (!ignoreTargetVoltage) + nodeModel.vTarget + .to(PowerSystemUnits.PU) + .getValue + .doubleValue() + else 1.0) - }) + /** Determine node type, target voltage and state data (dependent of the node + * type) + * + * @param nodeModel + * Complete node model + * @param nodeIndex + * Index of the node + * @param receivedValuesStore + * Store of received data + * @param transformers2w + * Set of two winding transformer models + * @param transformers3w + * Set of three winding transformer models + * @param residualPower + * Relative nodal residual power + * @param stateData + * Current collection of state data + * @param refSystem + * Reference system to use + * @param targetVoltageFromReceivedData + * Whether or not to take target voltage from received data + * @param ignoreTargetVoltage + * Whether or not to neglect the target voltage given in node input + * @return + */ + private def nodeTypeTargetVoltageStateData( + nodeModel: NodeModel, + nodeIndex: Int, + receivedValuesStore: ReceivedValuesStore, + transformers2w: Set[TransformerModel], + transformers3w: Set[Transformer3wModel], + residualPower: Complex, + stateData: Array[StateData], + refSystem: RefSystem, + targetVoltageFromReceivedData: Boolean, + ignoreTargetVoltage: Boolean + ): (NodeType, Complex, Array[StateData]) = if (nodeModel.isSlack) { + val targetVoltage = if (targetVoltageFromReceivedData) { + /* 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) + .getOrElse( + throw new RuntimeException( + s"No slack voltage received for node ${nodeModel.id} [${nodeModel.uuid}]!" + ) + ) + + transformVoltage( + receivedSlackVoltage, + nodeModel.uuid, + transformers2w, + transformers3w, + refSystem + ) + } else nodalTargetVoltage(nodeModel, ignoreTargetVoltage) + val updatedStateData = stateData :+ StateData( + nodeIndex, + NodeType.SL, + targetVoltage, + residualPower + ) + (NodeType.SL, targetVoltage, updatedStateData) + } else { + ( + NodeType.PQ, + nodalTargetVoltage(nodeModel, ignoreTargetVoltage), + stateData + ) } /** Composes the current operation point needed by @@ -185,7 +292,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, @@ -193,10 +301,9 @@ trait PowerFlowSupport { transformers2w: Set[TransformerModel], transformers3w: Set[Transformer3wModel], gridMainRefSystem: RefSystem - ): Array[PresetData] = { - sweepDataValues - .map(sweepValueStoreData => { - + ): (Array[PresetData], WithForcedStartVoltages) = + sweepDataValues.foldLeft(Array.empty[PresetData], Array.empty[StateData]) { + case ((operatingPoint, stateData), sweepValueStoreData) => val nodeStateData = sweepValueStoreData.stateData val targetVoltage = if (nodeStateData.nodeType == NodeType.SL) { val receivedSlackVoltage = receivedSlackValues.values @@ -217,19 +324,31 @@ trait PowerFlowSupport { gridMainRefSystem ) } else - new Complex(1, 0) + Complex.one + + val updatedStateData = if (nodeStateData.nodeType == NodeType.SL) { + stateData :+ StateData( + nodeStateData.index, + nodeStateData.nodeType, + targetVoltage, + nodeStateData.power + ) + } else stateData // note: target voltage will be ignored for slack node if provided - PresetData( - nodeStateData.index, - nodeStateData.nodeType, - nodeStateData.power, - targetVoltage.abs + ( + operatingPoint :+ PresetData( + nodeStateData.index, + nodeStateData.nodeType, + nodeStateData.power, + targetVoltage.abs + ), + updatedStateData ) - }) - .toArray - - } + } match { + case (operatingPoint, stateData) => + (operatingPoint, WithForcedStartVoltages(stateData)) + } /** A debug method that composes a string with voltage information (in p.u.) * from a [[ValidNewtonRaphsonPFResult]] @@ -412,6 +531,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 @@ -421,7 +542,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) => @@ -430,24 +552,13 @@ 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 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}." - ) - } + // / execute val powerFlow = NewtonRaphsonPF(epsilon, maxIterations, admittanceMatrix) powerFlow.calculate( operatingPoint, - Some(forcedSlackNodeVoltage) + Some(slackVoltages) ) match { case _: PowerFlowResult.FailedPowerFlowResult if epsilons.size > 1 => // if we can relax, we relax @@ -457,7 +568,12 @@ trait PowerFlowSupport { epsilon, epsilonsLeft.headOption.getOrElse("") ) - newtonRaphsonPF(gridModel, maxIterations, operatingPoint)( + newtonRaphsonPF( + gridModel, + maxIterations, + operatingPoint, + slackVoltages + )( epsilonsLeft ) case result => 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..3a9a2fc0fa 100644 --- a/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmCenGridSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmCenGridSpec.scala @@ -351,8 +351,8 @@ class DBFSAlgorithmCenGridSpec secondSlackAskSender ! ProvideSlackVoltageMessage( secondSweepNo, slackRequestNodeUuid, - Quantities.getQuantity(380, KILOVOLT), - Quantities.getQuantity(0, KILOVOLT) + Quantities.getQuantity(374.22694614463, KILOVOLT), // 380 kV @ 10° + Quantities.getQuantity(65.9863075134335, KILOVOLT) // 380 kV @ 10° ) // after the intermediate power flow calculation @@ -387,7 +387,7 @@ 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) + centerGridAgent ! RequestSlackVoltageMessage(secondSweepNo, nodeUuid) } // as we are in the second sweep, all provided slack voltages should be unequal @@ -405,28 +405,28 @@ class DBFSAlgorithmCenGridSpec secondProvidedSlackVoltages.size shouldBe 4 val secondExpectedResults = List( ProvideSlackVoltageMessage( - firstSweepNo, + secondSweepNo, UUID.fromString("d44ba8ed-81db-4a22-a40d-f7c0d0808a75"), - Quantities.getQuantity(110.1277081582144170, KILOVOLT), - Quantities.getQuantity(-0.011124597905979507, KILOVOLT) + Quantities.getQuantity(108.4565525820633120, KILOVOLT), + Quantities.getQuantity(19.11252024208434820, KILOVOLT) ), ProvideSlackVoltageMessage( - firstSweepNo, + secondSweepNo, UUID.fromString("e364ef00-e6ca-46b1-ba2b-bb73c0c6fee0"), - Quantities.getQuantity(110.1422124824355620, KILOVOLT), - Quantities.getQuantity(-0.014094294956794604, KILOVOLT) + Quantities.getQuantity(108.4713522355223970, KILOVOLT), + Quantities.getQuantity(19.11211431087968570, KILOVOLT) ), ProvideSlackVoltageMessage( - firstSweepNo, + secondSweepNo, UUID.fromString("78c5d473-e01b-44c4-afd2-e4ff3c4a5d7c"), - Quantities.getQuantity(110.1196117051188620, KILOVOLT), - Quantities.getQuantity(-0.009318349620959118, KILOVOLT) + Quantities.getQuantity(108.4482654805414590, KILOVOLT), + Quantities.getQuantity(19.11289311507043710, KILOVOLT) ), ProvideSlackVoltageMessage( - firstSweepNo, + secondSweepNo, UUID.fromString("47ef9983-8fcf-4713-be90-093fc27864ae"), - Quantities.getQuantity(110.147346134387320, KILOVOLT), - Quantities.getQuantity(-0.015819259689252657, KILOVOLT) + Quantities.getQuantity(108.4767074327598750, KILOVOLT), + Quantities.getQuantity(19.11130700154577660, KILOVOLT) ) ) @@ -491,11 +491,11 @@ class DBFSAlgorithmCenGridSpec case ProvideGridPowerMessage(nodeUuid, p, q) => nodeUuid shouldBe slackNodeUuid p should equalWithTolerance( - Quantities.getQuantity(0.080423711881452700000, MEGAVOLTAMPERE), + Quantities.getQuantity(0.08042371202897453, MEGAVOLTAMPERE), floatPrecision ) q should equalWithTolerance( - Quantities.getQuantity(-1.45357503915621860000, MEGAVOLTAMPERE), + Quantities.getQuantity(-1.4535750374419432, MEGAVOLTAMPERE), floatPrecision ) case x => diff --git a/src/test/scala/edu/ie3/simona/agent/participant/EvcsAgentModelCalculationSpec.scala b/src/test/scala/edu/ie3/simona/agent/participant/EvcsAgentModelCalculationSpec.scala index b2ae707b29..3ac8cd2021 100644 --- a/src/test/scala/edu/ie3/simona/agent/participant/EvcsAgentModelCalculationSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/participant/EvcsAgentModelCalculationSpec.scala @@ -1084,7 +1084,7 @@ class EvcsAgentModelCalculationSpec testingTolerance ) q should equalWithTolerance( - Quantities.getQuantity(-3.35669e-3, MEGAVAR), + Quantities.getQuantity(-0.01065446385, MEGAVAR), testingTolerance ) } From 1eba305c5553423068daa05d61fe60d462c70d0c Mon Sep 17 00:00:00 2001 From: "Kittl, Chris" Date: Tue, 11 Jan 2022 09:40:01 +0100 Subject: [PATCH 2/6] Fix reactive power in Evcs test --- .../agent/participant/EvcsAgentModelCalculationSpec.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/scala/edu/ie3/simona/agent/participant/EvcsAgentModelCalculationSpec.scala b/src/test/scala/edu/ie3/simona/agent/participant/EvcsAgentModelCalculationSpec.scala index 3ac8cd2021..b2ae707b29 100644 --- a/src/test/scala/edu/ie3/simona/agent/participant/EvcsAgentModelCalculationSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/participant/EvcsAgentModelCalculationSpec.scala @@ -1084,7 +1084,7 @@ class EvcsAgentModelCalculationSpec testingTolerance ) q should equalWithTolerance( - Quantities.getQuantity(-0.01065446385, MEGAVAR), + Quantities.getQuantity(-3.35669e-3, MEGAVAR), testingTolerance ) } From 1824d0a5810ff1036159b0d7c85767882bdc81eb Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Tue, 3 May 2022 12:09:07 +0200 Subject: [PATCH 3/6] Implemented my own suggestions --- .../simona/agent/grid/PowerFlowSupport.scala | 155 ++++++++---------- 1 file changed, 72 insertions(+), 83 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 03beff21f9..55d7b3668e 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala @@ -7,10 +7,7 @@ package edu.ie3.simona.agent.grid import akka.actor.ActorRef - -import java.util.UUID import breeze.math.Complex -import edu.ie3.datamodel.models.input.connector.Transformer2WInput import edu.ie3.powerflow.NewtonRaphsonPF import edu.ie3.powerflow.model.NodeData.{PresetData, StateData} import edu.ie3.powerflow.model.PowerFlowResult @@ -19,24 +16,19 @@ 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 import edu.ie3.simona.ontology.messages.PowerMessage.ProvidePowerMessage import edu.ie3.simona.ontology.messages.VoltageMessage.ProvideSlackVoltageMessage import edu.ie3.util.quantities.PowerSystemUnits import tech.units.indriya.ComparableQuantity - -import javax.measure.Quantity -import javax.measure.quantity.{Dimensionless, ElectricPotential, Power} import tech.units.indriya.quantity.Quantities +import java.util.UUID +import javax.measure.Quantity +import javax.measure.quantity.{Dimensionless, ElectricPotential} import scala.annotation.tailrec +import scala.collection.mutable /** Support and helper methods for power flow calculations provided by * [[edu.ie3.powerflow]] @@ -83,53 +75,51 @@ trait PowerFlowSupport { targetVoltageFromReceivedData: Boolean = true, ignoreTargetVoltage: Boolean = false ): (Array[PresetData], WithForcedStartVoltages) = - nodes.foldLeft((Array.empty[PresetData], Array.empty[StateData])) { - case ((operatingPoint, stateData), 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 + 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 - /* Determine the operating point for this given node */ - val nodeIdx = nodeUuidToIndexMap.getOrElse( - nodeModel.uuid, - throw new RuntimeException( - s"Received data for node ${nodeModel.id} [${nodeModel.uuid}] which is not in my nodeUuidToIndexMap!" - ) + /* Determine the operating point for this given node */ + val nodeIdx = nodeUuidToIndexMap.getOrElse( + nodeModel.uuid, + throw new RuntimeException( + s"Received data for node ${nodeModel.id} [${nodeModel.uuid}] which is not in my nodeUuidToIndexMap!" ) + ) - /* Determine the nodal residual apparent power */ - val apparentPower = determineNodalApparentPower( - receivedValuesStore.nodeToReceivedPower.get(nodeModel.uuid), - nodeModel.id, - gridMainRefSystem - ) + /* Determine the nodal residual apparent power */ + val apparentPower = determineNodalApparentPower( + receivedValuesStore.nodeToReceivedPower.get(nodeModel.uuid), + nodeModel.id, + gridMainRefSystem + ) - /* Determine node type, target voltage and updated state data (dependent on the node type) */ - nodeTypeTargetVoltageStateData( - nodeModel, - nodeIdx, - receivedValuesStore, - transformers2w, - transformers3w, - apparentPower, - stateData, - gridMainRefSystem, - targetVoltageFromReceivedData, - ignoreTargetVoltage - ) match { - case (nodeType, targetVoltage, stateData) => - ( - operatingPoint :+ PresetData( - nodeIdx, - nodeType, - apparentPower, - targetVoltage.abs - ), - stateData - ) - } - } match { + /* Determine node type, target voltage and updated state data (dependent on the node type) */ + nodeTypeTargetVoltageStateData( + nodeModel, + nodeIdx, + receivedValuesStore, + transformers2w, + transformers3w, + apparentPower, + gridMainRefSystem, + targetVoltageFromReceivedData, + ignoreTargetVoltage + ) match { + case (nodeType, targetVoltage, optStateData) => + ( + PresetData( + nodeIdx, + nodeType, + apparentPower, + targetVoltage.abs + ), + optStateData + ) + } + }.unzip match { case (operatingPoint, stateData) => - (operatingPoint, WithForcedStartVoltages(stateData)) + (operatingPoint, WithForcedStartVoltages(stateData.flatten)) } /** Determine the residual apparent nodal power balance for a given node. @@ -181,26 +171,6 @@ trait PowerFlowSupport { case None => new Complex(0, 0) } - /** Determine a target voltage if no received values are meant to be taken - * - * @param nodeModel - * The nodal model - * @param ignoreTargetVoltage - * Whether or not to ignore the nodal target voltage provided by input - * model - * @return - * A complex nodal target voltage - */ - private def nodalTargetVoltage( - nodeModel: NodeModel, - ignoreTargetVoltage: Boolean - ): Complex = Complex.one * (if (!ignoreTargetVoltage) - nodeModel.vTarget - .to(PowerSystemUnits.PU) - .getValue - .doubleValue() - else 1.0) - /** Determine node type, target voltage and state data (dependent of the node * type) * @@ -216,8 +186,6 @@ trait PowerFlowSupport { * Set of three winding transformer models * @param residualPower * Relative nodal residual power - * @param stateData - * Current collection of state data * @param refSystem * Reference system to use * @param targetVoltageFromReceivedData @@ -233,11 +201,10 @@ trait PowerFlowSupport { transformers2w: Set[TransformerModel], transformers3w: Set[Transformer3wModel], residualPower: Complex, - stateData: Array[StateData], refSystem: RefSystem, targetVoltageFromReceivedData: Boolean, ignoreTargetVoltage: Boolean - ): (NodeType, Complex, Array[StateData]) = if (nodeModel.isSlack) { + ): (NodeType, Complex, Option[StateData]) = if (nodeModel.isSlack) { val targetVoltage = if (targetVoltageFromReceivedData) { /* 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 */ @@ -258,21 +225,43 @@ trait PowerFlowSupport { refSystem ) } else nodalTargetVoltage(nodeModel, ignoreTargetVoltage) - val updatedStateData = stateData :+ StateData( + val updatedStateData = StateData( nodeIndex, NodeType.SL, targetVoltage, residualPower ) - (NodeType.SL, targetVoltage, updatedStateData) + (NodeType.SL, targetVoltage, Some(updatedStateData)) } else { ( NodeType.PQ, nodalTargetVoltage(nodeModel, ignoreTargetVoltage), - stateData + None ) } + /** Determine a target voltage if no received values are meant to be taken + * + * @param nodeModel + * The nodal model + * @param ignoreTargetVoltage + * Whether or not to ignore the nodal target voltage provided by input + * model + * @return + * A complex nodal target voltage + */ + private def nodalTargetVoltage( + nodeModel: NodeModel, + ignoreTargetVoltage: Boolean + ): Complex = + Complex.one * + (if (!ignoreTargetVoltage) + nodeModel.vTarget + .to(PowerSystemUnits.PU) + .getValue + .doubleValue() + else 1.0) + /** Composes the current operation point needed by * [[edu.ie3.powerflow.NewtonRaphsonPF.calculate()]] by reusing the provided * p/q values from the provided sweepDataValues and combines them with @@ -364,7 +353,7 @@ 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 From a2b62e7ec4ee07f35f252d1243dbd31d50d40085 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Mon, 20 Jun 2022 18:04:45 +0200 Subject: [PATCH 4/6] Improving composeOperatingPointWithUpdatedSlackVoltages --- .../simona/agent/grid/PowerFlowSupport.scala | 72 +++++++++---------- 1 file changed, 36 insertions(+), 36 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 55d7b3668e..81d4a0bdff 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/PowerFlowSupport.scala @@ -291,52 +291,52 @@ trait PowerFlowSupport { transformers3w: Set[Transformer3wModel], gridMainRefSystem: RefSystem ): (Array[PresetData], WithForcedStartVoltages) = - sweepDataValues.foldLeft(Array.empty[PresetData], Array.empty[StateData]) { - case ((operatingPoint, stateData), sweepValueStoreData) => - val nodeStateData = sweepValueStoreData.stateData - val targetVoltage = if (nodeStateData.nodeType == NodeType.SL) { - val receivedSlackVoltage = receivedSlackValues.values - .flatMap(_._2) - .find(_.nodeUuid == sweepValueStoreData.nodeUuid) - .getOrElse( - throw new RuntimeException( - s"Unable to find node with uuid " + - s"${sweepValueStoreData.nodeUuid} in received slack voltage values!" - ) + sweepDataValues.map { sweepValueStoreData => + val nodeStateData = sweepValueStoreData.stateData + val targetVoltage = if (nodeStateData.nodeType == NodeType.SL) { + val receivedSlackVoltage = receivedSlackValues.values + .flatMap { case (_, slackValueOpt) => slackValueOpt } + .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 - Complex.one - val updatedStateData = if (nodeStateData.nodeType == NodeType.SL) { - stateData :+ StateData( + 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 ) - } else stateData - - // note: target voltage will be ignored for slack node if provided - ( - operatingPoint :+ PresetData( - nodeStateData.index, - nodeStateData.nodeType, - nodeStateData.power, - targetVoltage.abs - ), - updatedStateData ) - } match { + ) + }.unzip match { case (operatingPoint, stateData) => - (operatingPoint, WithForcedStartVoltages(stateData)) + ( + operatingPoint.toArray, + WithForcedStartVoltages(stateData.flatten.toArray) + ) } /** A debug method that composes a string with voltage information (in p.u.) From 11e4d8d244a34c9767e2aa996f09e3c93e153aa1 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Sat, 30 Jul 2022 21:29:50 +0200 Subject: [PATCH 5/6] Simplifying DBFSAlgorithm changes a bit --- .../ie3/simona/agent/grid/DBFSAlgorithm.scala | 187 +++++++++--------- 1 file changed, 92 insertions(+), 95 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 97444f8c05..ea5a684f36 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/DBFSAlgorithm.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/DBFSAlgorithm.scala @@ -440,25 +440,22 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { val gridModel = gridAgentBaseData.gridEnv.gridModel - val powerFlowResult = composeOperatingPoint( + val (operatingPoint, slackNodeVoltages) = composeOperatingPoint( gridModel.gridComponents.nodes, gridModel.gridComponents.transformers, gridModel.gridComponents.transformers3w, gridModel.nodeUuidToIndexMap, gridAgentBaseData.receivedValueStore, gridModel.mainRefSystem - ) match { - case (operatingPoint, slackNodeVoltages) => - newtonRaphsonPF( - gridModel, - gridAgentBaseData.powerFlowParams.maxIterations, - operatingPoint, - slackNodeVoltages - )(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 { + newtonRaphsonPF( + gridModel, + gridAgentBaseData.powerFlowParams.maxIterations, + 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( "{}", @@ -595,99 +592,99 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { ) ) - composeOperatingPointWithUpdatedSlackVoltages( - receivedSlackValues, - previousSweepData.sweepData, - gridModel.gridComponents.transformers, - gridModel.gridComponents.transformers3w, - gridModel.mainRefSystem - ) match { - case (operatingPoint, slackNodeVoltages) => - newtonRaphsonPF( - gridModel, - gridAgentBaseData.powerFlowParams.maxIterations, - operatingPoint, - slackNodeVoltages - )(gridAgentBaseData.powerFlowParams.epsilon) match { - case validPowerFlowResult: ValidNewtonRaphsonPFResult => - log.debug( - "{}", - composeValidNewtonRaphsonPFResultVoltagesDebugString( - validPowerFlowResult, - gridModel - ) - ) - - // update the data - val sweepValueStore = SweepValueStore( - validPowerFlowResult, - gridModel.gridComponents.nodes, - gridModel.nodeUuidToIndexMap - ) - val updatedSweepValueStore = - gridAgentBaseData.sweepValueStores + (gridAgentBaseData.currentSweepNo -> sweepValueStore) - - // send request to child grids and assets for updated p/q values - // we start the grid simulation by requesting the p/q values of all the nodes we are responsible for - // as well as the slack voltage power from our superior grid - // 1. assets p/q values - val askForAssetPowersOpt = - askForAssetPowers( - currentTick, - Some(sweepValueStore), - gridAgentBaseData.gridEnv.nodeToAssetAgents, - gridModel.mainRefSystem, - gridAgentBaseData.powerFlowParams.sweepTimeout - ) + val (operatingPoint, slackNodeVoltages) = + composeOperatingPointWithUpdatedSlackVoltages( + receivedSlackValues, + previousSweepData.sweepData, + gridModel.gridComponents.transformers, + gridModel.gridComponents.transformers3w, + gridModel.mainRefSystem + ) - // 2. inferior grids p/q values - val askForInferiorGridPowersOpt = - askInferiorGridsForPowers( - gridAgentBaseData.currentSweepNo, - gridAgentBaseData.gridEnv.subnetGateToActorRef, - gridAgentBaseData.inferiorGridGates, - gridAgentBaseData.powerFlowParams.sweepTimeout - ) + newtonRaphsonPF( + gridModel, + gridAgentBaseData.powerFlowParams.maxIterations, + operatingPoint, + slackNodeVoltages + )(gridAgentBaseData.powerFlowParams.epsilon) match { + case validPowerFlowResult: ValidNewtonRaphsonPFResult => + log.debug( + "{}", + composeValidNewtonRaphsonPFResultVoltagesDebugString( + validPowerFlowResult, + gridModel + ) + ) - // when we don't have inferior grids and no assets both methods return None and we can skip doing another power - // flow calculation otherwise we go back to simulate grid and wait for the answers - (askForAssetPowersOpt, askForInferiorGridPowersOpt) match { - case (None, None) => - log.debug( - "I don't have assets or child grids. " + - "Going back to SimulateGrid and provide the power flow result if there is any request left." - ) + // update the data + val sweepValueStore = SweepValueStore( + validPowerFlowResult, + gridModel.gridComponents.nodes, + gridModel.nodeUuidToIndexMap + ) + val updatedSweepValueStore = + gridAgentBaseData.sweepValueStores + (gridAgentBaseData.currentSweepNo -> sweepValueStore) + + // send request to child grids and assets for updated p/q values + // we start the grid simulation by requesting the p/q values of all the nodes we are responsible for + // as well as the slack voltage power from our superior grid + // 1. assets p/q values + val askForAssetPowersOpt = + askForAssetPowers( + currentTick, + Some(sweepValueStore), + gridAgentBaseData.gridEnv.nodeToAssetAgents, + gridModel.mainRefSystem, + gridAgentBaseData.powerFlowParams.sweepTimeout + ) - val powerFlowDoneData = - PowerFlowDoneData(gridAgentBaseData, validPowerFlowResult) + // 2. inferior grids p/q values + val askForInferiorGridPowersOpt = + askInferiorGridsForPowers( + gridAgentBaseData.currentSweepNo, + gridAgentBaseData.gridEnv.subnetGateToActorRef, + gridAgentBaseData.inferiorGridGates, + gridAgentBaseData.powerFlowParams.sweepTimeout + ) - unstashAll() // we can answer the stashed grid power requests now - goto(SimulateGrid) using powerFlowDoneData + // when we don't have inferior grids and no assets both methods return None and we can skip doing another power + // flow calculation otherwise we go back to simulate grid and wait for the answers + (askForAssetPowersOpt, askForInferiorGridPowersOpt) match { + case (None, None) => + log.debug( + "I don't have assets or child grids. " + + "Going back to SimulateGrid and provide the power flow result if there is any request left." + ) - case _ => - log.debug( - "Going back to SimulateGrid and wait for my assets or inferior grids to return." - ) + val powerFlowDoneData = + PowerFlowDoneData(gridAgentBaseData, validPowerFlowResult) - // go back to simulate grid - goto(SimulateGrid) using gridAgentBaseData - .updateWithReceivedSlackVoltages(receivedSlackValues) - .copy(sweepValueStores = updatedSweepValueStore) - } + unstashAll() // we can answer the stashed grid power requests now + goto(SimulateGrid) using powerFlowDoneData - case failedNewtonRaphsonPFResult: FailedNewtonRaphsonPFResult => - val powerFlowDoneData = - PowerFlowDoneData( - gridAgentBaseData, - failedNewtonRaphsonPFResult - ) - log.warning( - "Power flow with updated slack voltage did finally not converge!" + case _ => + log.debug( + "Going back to SimulateGrid and wait for my assets or inferior grids to return." ) - unstashAll() // we can answer the stashed grid power requests now and report a failed power flow back - goto(SimulateGrid) using powerFlowDoneData + // go back to simulate grid + goto(SimulateGrid) using gridAgentBaseData + .updateWithReceivedSlackVoltages(receivedSlackValues) + .copy(sweepValueStores = updatedSweepValueStore) } + + case failedNewtonRaphsonPFResult: FailedNewtonRaphsonPFResult => + val powerFlowDoneData = + PowerFlowDoneData( + gridAgentBaseData, + failedNewtonRaphsonPFResult + ) + log.warning( + "Power flow with updated slack voltage did finally not converge!" + ) + unstashAll() // we can answer the stashed grid power requests now and report a failed power flow back + goto(SimulateGrid) using powerFlowDoneData + } // happens only when we received slack data and power values before we received a request to provide grid data From e99ce260a5a7085efd581875cead999670003e21 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Sat, 30 Jul 2022 21:57:49 +0200 Subject: [PATCH 6/6] Reverting the creation of extra methods --- .../simona/agent/grid/PowerFlowSupport.scala | 246 ++++++------------ 1 file changed, 79 insertions(+), 167 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 81d4a0bdff..0b83f9a2ee 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 akka.actor.ActorRef import breeze.math.Complex import edu.ie3.powerflow.NewtonRaphsonPF import edu.ie3.powerflow.model.NodeData.{PresetData, StateData} @@ -17,7 +16,6 @@ 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._ -import edu.ie3.simona.ontology.messages.PowerMessage import edu.ie3.simona.ontology.messages.PowerMessage.ProvidePowerMessage import edu.ie3.simona.ontology.messages.VoltageMessage.ProvideSlackVoltageMessage import edu.ie3.util.quantities.PowerSystemUnits @@ -78,6 +76,7 @@ trait PowerFlowSupport { 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 /* Determine the operating point for this given node */ val nodeIdx = nodeUuidToIndexMap.getOrElse( @@ -87,180 +86,93 @@ trait PowerFlowSupport { ) ) - /* Determine the nodal residual apparent power */ - val apparentPower = determineNodalApparentPower( - receivedValuesStore.nodeToReceivedPower.get(nodeModel.uuid), - nodeModel.id, - gridMainRefSystem - ) - - /* Determine node type, target voltage and updated state data (dependent on the node type) */ - nodeTypeTargetVoltageStateData( - nodeModel, - nodeIdx, - receivedValuesStore, - transformers2w, - transformers3w, - apparentPower, - gridMainRefSystem, - targetVoltageFromReceivedData, - ignoreTargetVoltage - ) match { - case (nodeType, targetVoltage, optStateData) => - ( - PresetData( - nodeIdx, - nodeType, - apparentPower, - targetVoltage.abs - ), - optStateData - ) - } - }.unzip match { - case (operatingPoint, stateData) => - (operatingPoint, WithForcedStartVoltages(stateData.flatten)) - } + val apparentPower: Complex = + receivedValuesStore.nodeToReceivedPower + .get(nodeModel.uuid) match { + case Some(actorRefsWithPower) => + val powerUnit = gridMainRefSystem.nominalPower.getUnit + val (p, q) = actorRefsWithPower + .map { case (_, powerMsg) => powerMsg } + .collect { + case Some(providePowerMessage: ProvidePowerMessage) => + providePowerMessage + case Some(message) => + throw new RuntimeException( + s"Received message $message which cannot processed here!" + ) + case None => + throw new RuntimeException( + s"Did not receive all power values I expected @ node ${nodeModel.id}. This is a fatal and should never happen!" + ) + } + .foldLeft( + ( + Quantities.getQuantity(0, powerUnit), + Quantities.getQuantity(0, powerUnit) + ) + ) { case ((pSum, qSum), powerMessage) => + (pSum.add(powerMessage.p), qSum.add(powerMessage.q)) + } - /** Determine the residual apparent nodal power balance for a given node. - * - * @param receivedPower - * Optional vector of received power messages - * @param nodeId - * Id of the node (for debugging reasons) - * @param refSystem - * Main reference system of the grid - * @return - */ - private def determineNodalApparentPower( - receivedPower: Option[ - Vector[(ActorRef, Option[PowerMessage.PowerResponseMessage])] - ], - nodeId: String, - refSystem: RefSystem - ): Complex = receivedPower match { - case Some(actorRefsWithPower) => - val powerUnit = refSystem.nominalPower.getUnit - val (p, q) = actorRefsWithPower - .map(_._2) - .collect { - case Some(providePowerMessage: ProvidePowerMessage) => - providePowerMessage - case Some(message) => - throw new RuntimeException( - s"Received message $message which cannot processed here!" - ) - case None => - throw new RuntimeException( - s"Did not receive all power values I expected @ node $nodeId. This is a fatal and should never happen!" + new Complex( + gridMainRefSystem.pInPu(p).getValue.doubleValue(), + gridMainRefSystem.qInPu(q).getValue.doubleValue() ) + case None => new Complex(0, 0) } - .foldLeft( - ( - Quantities.getQuantity(0, powerUnit), - Quantities.getQuantity(0, powerUnit) - ) - )((pqSum, powerMessage) => { - (pqSum._1.add(powerMessage.p), pqSum._2.add(powerMessage.q)) - }) - new Complex( - refSystem.pInPu(p).getValue.doubleValue(), - refSystem.qInPu(q).getValue.doubleValue() - ) - case None => new Complex(0, 0) - } + 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 = + receivedValuesStore.nodeToReceivedSlackVoltage.values.flatten + .find(_.nodeUuid == nodeModel.uuid) + .getOrElse( + throw new RuntimeException( + s"No slack voltage received for node ${nodeModel.id} [${nodeModel.uuid}]!" + ) + ) - /** Determine node type, target voltage and state data (dependent of the node - * type) - * - * @param nodeModel - * Complete node model - * @param nodeIndex - * Index of the node - * @param receivedValuesStore - * Store of received data - * @param transformers2w - * Set of two winding transformer models - * @param transformers3w - * Set of three winding transformer models - * @param residualPower - * Relative nodal residual power - * @param refSystem - * Reference system to use - * @param targetVoltageFromReceivedData - * Whether or not to take target voltage from received data - * @param ignoreTargetVoltage - * Whether or not to neglect the target voltage given in node input - * @return - */ - private def nodeTypeTargetVoltageStateData( - nodeModel: NodeModel, - nodeIndex: Int, - receivedValuesStore: ReceivedValuesStore, - transformers2w: Set[TransformerModel], - transformers3w: Set[Transformer3wModel], - residualPower: Complex, - refSystem: RefSystem, - targetVoltageFromReceivedData: Boolean, - ignoreTargetVoltage: Boolean - ): (NodeType, Complex, Option[StateData]) = if (nodeModel.isSlack) { - val targetVoltage = if (targetVoltageFromReceivedData) { - /* 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) - .getOrElse( - throw new RuntimeException( - s"No slack voltage received for node ${nodeModel.id} [${nodeModel.uuid}]!" - ) + transformVoltage( + receivedSlackVoltage, + nodeModel.uuid, + transformers2w, + transformers3w, + gridMainRefSystem ) + } else { + Complex.one * + (if (!ignoreTargetVoltage) + nodeModel.vTarget + .to(PowerSystemUnits.PU) + .getValue + .doubleValue() + else 1.0) + } - transformVoltage( - receivedSlackVoltage, - nodeModel.uuid, - transformers2w, - transformers3w, - refSystem + val optStateData = Option.when(nodeModel.isSlack)( + StateData( + nodeIdx, + NodeType.SL, + targetVoltage, + apparentPower + ) ) - } else nodalTargetVoltage(nodeModel, ignoreTargetVoltage) - val updatedStateData = StateData( - nodeIndex, - NodeType.SL, - targetVoltage, - residualPower - ) - (NodeType.SL, targetVoltage, Some(updatedStateData)) - } else { - ( - NodeType.PQ, - nodalTargetVoltage(nodeModel, ignoreTargetVoltage), - None - ) - } - /** Determine a target voltage if no received values are meant to be taken - * - * @param nodeModel - * The nodal model - * @param ignoreTargetVoltage - * Whether or not to ignore the nodal target voltage provided by input - * model - * @return - * A complex nodal target voltage - */ - private def nodalTargetVoltage( - nodeModel: NodeModel, - ignoreTargetVoltage: Boolean - ): Complex = - Complex.one * - (if (!ignoreTargetVoltage) - nodeModel.vTarget - .to(PowerSystemUnits.PU) - .getValue - .doubleValue() - else 1.0) + ( + PresetData( + nodeIdx, + nodeType, + apparentPower, + targetVoltage.abs + ), + optStateData + ) + }.unzip match { + case (operatingPoint, stateData) => + (operatingPoint, WithForcedStartVoltages(stateData.flatten)) + } /** Composes the current operation point needed by * [[edu.ie3.powerflow.NewtonRaphsonPF.calculate()]] by reusing the provided