diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f64e9f668..b54f552bfa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added smart charging logic [#31](https://github.com/ie3-institute/simona/issues/31) and flex calculation in `EvcsAgent` [#332](https://github.com/ie3-institute/simona/issues/332) - Enhance output quotes of `RunSimona` [#743](https://github.com/ie3-institute/simona/issues/743) - Printing logs of failed tests [#747](https://github.com/ie3-institute/simona/issues/747) +- Models for measurements within the grid structure [#89](https://github.com/ie3-institute/simona/issues/89) +- Config possibility for transformer control groups [#90](https://github.com/ie3-institute/simona/issues/90) ### Changed - Adapted to changed data source in PSDM [#435](https://github.com/ie3-institute/simona/issues/435) @@ -39,6 +41,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Unified consideration of scaling factor when simulating system participants [#81](https://github.com/ie3-institute/simona/issues/81) - Small improvements in `ResultEventListener` [#738](https://github.com/ie3-institute/simona/issues/738) - Converting `SimonaSim` to pekko typed/terminating SimonSim when initialization fails [#210](https://github.com/ie3-institute/simona/issues/210) +- Converting the `GridAgent` and the `DBFSAlgorithm` to `pekko typed` [#666](https://github.com/ie3-institute/simona/issues/666) ### Fixed - Removed a repeated line in the documentation of vn_simona config [#658](https://github.com/ie3-institute/simona/issues/658) diff --git a/build.gradle b/build.gradle index 933d7f6276..b367e10368 100644 --- a/build.gradle +++ b/build.gradle @@ -9,7 +9,7 @@ plugins { id 'pmd' // code check, working on source code id 'com.diffplug.spotless' version '6.25.0'// code format id "com.github.ben-manes.versions" version '0.51.0' - id "de.undercouch.download" version "5.5.0" // downloads plugin + id "de.undercouch.download" version "5.6.0" // downloads plugin id "kr.motd.sphinx" version "2.10.1" // documentation generation id "com.github.johnrengelman.shadow" version "8.1.1" // fat jar id "org.sonarqube" version "4.4.1.3373" // sonarqube @@ -24,12 +24,12 @@ ext { javaVersion = JavaVersion.VERSION_17 scalaVersion = '2.13' - scalaBinaryVersion = '2.13.12' - pekkoVersion = '1.0.1' + scalaBinaryVersion = '2.13.13' + pekkoVersion = '1.0.2' jtsVersion = '1.19.0' confluentKafkaVersion = '7.4.0' tscfgVersion = '1.0.0' - scapegoatVersion = '2.1.3' + scapegoatVersion = '2.1.5' testContainerVersion = '0.41.3' @@ -102,12 +102,12 @@ dependencies { /* logging */ implementation "com.typesafe.scala-logging:scala-logging_${scalaVersion}:3.9.5" // pekko scala logging - implementation "ch.qos.logback:logback-classic:1.5.0" + implementation "ch.qos.logback:logback-classic:1.5.3" /* testing */ testImplementation 'org.spockframework:spock-core:2.3-groovy-4.0' testImplementation 'org.scalatestplus:mockito-3-4_2.13:3.2.10.0' - testImplementation 'org.mockito:mockito-core:5.10.0' // mocking framework + testImplementation 'org.mockito:mockito-core:5.11.0' // mocking framework testImplementation "org.scalatest:scalatest_${scalaVersion}:3.2.18" testRuntimeOnly 'com.vladsch.flexmark:flexmark-all:0.64.8' //scalatest html output testImplementation group: 'org.pegdown', name: 'pegdown', version: '1.6.0' diff --git a/docs/readthedocs/config.md b/docs/readthedocs/config.md index 91a6d952a5..64a9dfc1b7 100644 --- a/docs/readthedocs/config.md +++ b/docs/readthedocs/config.md @@ -236,3 +236,42 @@ Secondary convergence criterion for the power flow calculation is the number of Resolution of the power flow calculation: `simona.powerflow.resolution = "3600s"` + +## Transformer Control Group configuration + +It's possible to add a voltage control function to a transformer or group of transformers. This requires measurements within the network to be under voltage control and at least one corresponding transformer. +The voltage control will attempt to adjust the voltage by changing the tap position of the corresponding transformer. If changing the tap position would cause a voltage limit to be exceeded, the initial voltage deviation cannot be reduced by the voltage control system. + +Transformer control groups must contain at least one transformer and one measurement. And can be configured as shown in this example for two transformer control groups: +``` +simona.control.transformer = [ +{ +transformers = ["31a2b9bf-e785-4475-aa44-1c34646e8c79"], +measurements = ["923f2d69-3093-4198-86e4-13d2d1c220f8"], +vMin = 0.98, +vMax = 1.02 +} +, { +transformers = ["1132dbf4-e8a1-44ae-8415-f42d4497aa1d"], +measurements = ["7686b818-a0ba-465c-8e4e-f7d3c4e171fc"], +vMin = 0.98, +vMax = 1.02 +} +] +``` + +UUID of transformer in control group: + +`transformers = ["31a2b9bf-e785-4475-aa44-1c34646e8c79"]` + +UUID of measurement in control group: + +`measurements = ["923f2d69-3093-4198-86e4-13d2d1c220f8"]` + +Minimum Voltage Limit in p.u.: + +`vMin = 0.98` + +Maximum Voltage Limit in p.u.: + +`vMax = 1.02` diff --git a/docs/readthedocs/models.md b/docs/readthedocs/models.md index f4709ebab9..e32ee67098 100644 --- a/docs/readthedocs/models.md +++ b/docs/readthedocs/models.md @@ -33,3 +33,11 @@ models/load_model models/pv_model models/wec_model ``` + +## Measurement and Control +```{toctree} +--- +maxdepth: 1 +--- +models/measurement_control +``` diff --git a/docs/readthedocs/models/cts_model.md b/docs/readthedocs/models/cts_model.md index eea6fcfe0c..0ca51a7395 100644 --- a/docs/readthedocs/models/cts_model.md +++ b/docs/readthedocs/models/cts_model.md @@ -7,7 +7,7 @@ This storage model operates on volumes, although the functions it provides for o ## Attributes, Units and Remarks -Please refer to {doc}`PowerSystemDataModel - CTS Model ` for Attributes and Units used in this Model. +Please refer to {doc}`PowerSystemDataModel - CTS Model ` for Attributes and Units used in this Model. ## Calculations ### Maximal storage capacity diff --git a/docs/readthedocs/models/measurement_control.md b/docs/readthedocs/models/measurement_control.md new file mode 100644 index 0000000000..e57238d6a6 --- /dev/null +++ b/docs/readthedocs/models/measurement_control.md @@ -0,0 +1,5 @@ +(measurement_control)= + +# Transformer Control Groups + +Transformer control group can be used to implement control functionalities like long-range control for active voltage stability. For this purpose, network areas and transformers can be logically linked to a control group via measuring points. If a deviation from the target voltage magnitude is detected at one of the measuring points, the transformer is switched to the appropriate tap position to solve the deviation, provided that no limit values are violated at other measuring points. This requires that only measuring points are included in control groups that can also be influenced by the associated transformer. diff --git a/docs/readthedocs/models/thermal_grid_model.md b/docs/readthedocs/models/thermal_grid_model.md index e7aa2c214e..fc16159552 100644 --- a/docs/readthedocs/models/thermal_grid_model.md +++ b/docs/readthedocs/models/thermal_grid_model.md @@ -5,4 +5,4 @@ The Thermal Grid Model introduces a coupling point to thermal system, equivalent ## Attributes, Units and Remarks -Please refer to {doc}`PowerSystemDataModel - Thermal Bus ` for Attributes and Units used in this Model. +Please refer to {doc}`PowerSystemDataModel - Thermal Bus ` for Attributes and Units used in this Model. diff --git a/gradle/scripts/scoverage.gradle b/gradle/scripts/scoverage.gradle index 0a2e0157e8..e8ac99cf1f 100644 --- a/gradle/scripts/scoverage.gradle +++ b/gradle/scripts/scoverage.gradle @@ -3,7 +3,7 @@ // https://github.com/scoverage/gradle-scoverage/issues/109 for details scoverage { - scoverageVersion = "2.0.11" + scoverageVersion = "2.1.0" scoverageScalaVersion = scalaBinaryVersion coverageOutputHTML = false coverageOutputXML = true diff --git a/input/samples/vn_simona/vn_simona.conf b/input/samples/vn_simona/vn_simona.conf index fe2e05d439..59185bb5fb 100644 --- a/input/samples/vn_simona/vn_simona.conf +++ b/input/samples/vn_simona/vn_simona.conf @@ -182,3 +182,17 @@ simona.powerflow.newtonraphson.epsilon = [1E-12] simona.powerflow.newtonraphson.iterations = 50 simona.powerflow.resolution = "3600s" simona.powerflow.stopOnFailure = true + +simona.control.transformer = [ + { + transformers = ["31a2b9bf-e785-4475-aa44-1c34646e8c79"], + measurements = ["923f2d69-3093-4198-86e4-13d2d1c220f8"], + vMin = 0.98, + vMax = 1.02 + }, { + transformers = ["1132dbf4-e8a1-44ae-8415-f42d4497aa1d"], + measurements = ["7686b818-a0ba-465c-8e4e-f7d3c4e171fc"], + vMin = 0.98, + vMax = 1.02 + } +] diff --git a/src/main/resources/config/config-template.conf b/src/main/resources/config/config-template.conf index 859d98e016..b006cafd6b 100644 --- a/src/main/resources/config/config-template.conf +++ b/src/main/resources/config/config-template.conf @@ -126,6 +126,14 @@ GridOutputConfig { transformers3w: boolean | false } +#@define +TransformerControlGroup { + measurements: [string] + transformers: [string] + vMax: Double + vMin: Double +} + ################################################################## # Agentsim ################################################################## @@ -333,3 +341,11 @@ simona.event.listener = [ eventsToProcess = [string] } ] + +################################################################## +# Configuration of Control Schemes +################################################################## +#@optional +simona.control = { + transformer = [TransformerControlGroup] +} diff --git a/src/main/scala/edu/ie3/simona/actor/SimonaActorNaming.scala b/src/main/scala/edu/ie3/simona/actor/SimonaActorNaming.scala index bc5b548f2f..6aeeaf093c 100644 --- a/src/main/scala/edu/ie3/simona/actor/SimonaActorNaming.scala +++ b/src/main/scala/edu/ie3/simona/actor/SimonaActorNaming.scala @@ -6,7 +6,9 @@ package edu.ie3.simona.actor -import org.apache.pekko.actor.{ActorRef, ActorRefFactory, Props} +import org.apache.pekko.actor.typed.ActorRef +import org.apache.pekko.actor.typed.scaladsl.adapter.TypedActorRefOps +import org.apache.pekko.actor.{ActorRefFactory, Props, ActorRef => ClassicRef} import java.util.UUID @@ -15,10 +17,10 @@ object SimonaActorNaming { implicit class RichActorRefFactory(private val refFactory: ActorRefFactory) extends AnyVal { - def simonaActorOf(props: Props, actorId: String): ActorRef = + def simonaActorOf(props: Props, actorId: String): ClassicRef = refFactory.actorOf(props, actorName(props, actorId)) - def simonaActorOf(props: Props): ActorRef = + def simonaActorOf(props: Props): ClassicRef = refFactory.actorOf(props, actorName(props, simonaActorUuid)) } @@ -62,13 +64,13 @@ object SimonaActorNaming { def actorName(typeName: String, actorId: String): String = s"${typeName}_${cleanActorIdForPekko(actorId)}" - /** Extracts the actor name from given [[ActorRef]]. Cluster singletons are + /** Extracts the actor name from given [[ClassicRef]]. Cluster singletons are * taken care of separately. * * @return * the actor name extract from the ActorRef */ - def actorName(actorRef: ActorRef): String = + def actorName(actorRef: ClassicRef): String = actorRef.path.name match { case "singleton" => // singletons end in /${actorName}/singleton @@ -77,6 +79,14 @@ object SimonaActorNaming { other } + /** Extracts the actor name from given [[ActorRef]]. Cluster singletons are + * taken care of separately. + * + * @return + * the actor name extract from the ActorRef + */ + def actorName(actorRef: ActorRef[_]): String = actorName(actorRef.toClassic) + /** Constructs the type name from given props. * * @return diff --git a/src/main/scala/edu/ie3/simona/agent/EnvironmentRefs.scala b/src/main/scala/edu/ie3/simona/agent/EnvironmentRefs.scala index 2421cdd5c3..4b5c6c920d 100644 --- a/src/main/scala/edu/ie3/simona/agent/EnvironmentRefs.scala +++ b/src/main/scala/edu/ie3/simona/agent/EnvironmentRefs.scala @@ -6,6 +6,7 @@ package edu.ie3.simona.agent +import edu.ie3.simona.event.RuntimeEvent import edu.ie3.simona.ontology.messages.SchedulerMessage import org.apache.pekko.actor.typed.ActorRef import org.apache.pekko.actor.{ActorRef => ClassicRef} @@ -26,7 +27,7 @@ import org.apache.pekko.actor.{ActorRef => ClassicRef} */ final case class EnvironmentRefs( scheduler: ActorRef[SchedulerMessage], - runtimeEventListener: ClassicRef, + runtimeEventListener: ActorRef[RuntimeEvent], primaryServiceProxy: ClassicRef, weather: ClassicRef, evDataService: Option[ClassicRef], 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 1cab5c0589..f47f3ef8b8 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/DBFSAlgorithm.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/DBFSAlgorithm.scala @@ -6,10 +6,6 @@ package edu.ie3.simona.agent.grid -import org.apache.pekko.actor.typed.scaladsl.adapter.ClassicActorRefOps -import org.apache.pekko.actor.{ActorRef, FSM, PoisonPill} -import org.apache.pekko.pattern.{ask, pipe} -import org.apache.pekko.util.{Timeout => PekkoTimeout} import breeze.linalg.{DenseMatrix, DenseVector} import breeze.math.Complex import edu.ie3.datamodel.graph.SubGridGate @@ -19,18 +15,22 @@ import edu.ie3.powerflow.model.PowerFlowResult import edu.ie3.powerflow.model.PowerFlowResult.FailedPowerFlowResult.FailedNewtonRaphsonPFResult import edu.ie3.powerflow.model.PowerFlowResult.SuccessFullPowerFlowResult.ValidNewtonRaphsonPFResult import edu.ie3.powerflow.model.enums.NodeType -import edu.ie3.simona.agent.grid.GridAgent._ +import edu.ie3.simona.agent.grid.GridAgent.idle import edu.ie3.simona.agent.grid.GridAgentData.{ GridAgentBaseData, + GridAgentConstantData, PowerFlowDoneData, } +import edu.ie3.simona.agent.grid.GridAgentMessage._ import edu.ie3.simona.agent.grid.ReceivedValues._ -import edu.ie3.simona.agent.state.AgentState -import edu.ie3.simona.agent.state.AgentState.Idle -import edu.ie3.simona.agent.state.GridAgentState.{ - CheckPowerDifferences, - HandlePowerFlowCalculations, - SimulateGrid, +import edu.ie3.simona.agent.grid.VoltageMessage.ProvideSlackVoltageMessage.ExchangeVoltage +import edu.ie3.simona.agent.grid.VoltageMessage.{ + ProvideSlackVoltageMessage, + RequestSlackVoltageMessage, +} +import edu.ie3.simona.agent.participant.ParticipantAgent.{ + FinishParticipantSimulation, + ParticipantMessage, } import edu.ie3.simona.event.RuntimeEvent.PowerFlowFailed import edu.ie3.simona.exceptions.agent.DBFSAlgorithmException @@ -38,714 +38,810 @@ import edu.ie3.simona.model.grid.{NodeModel, RefSystem} import edu.ie3.simona.ontology.messages.Activation import edu.ie3.simona.ontology.messages.PowerMessage._ import edu.ie3.simona.ontology.messages.SchedulerMessage.Completion -import edu.ie3.simona.ontology.messages.VoltageMessage.ProvideSlackVoltageMessage.ExchangeVoltage -import edu.ie3.simona.ontology.messages.VoltageMessage.{ - ProvideSlackVoltageMessage, - RequestSlackVoltageMessage, -} import edu.ie3.simona.util.TickUtil.TickLong import edu.ie3.util.scala.quantities.Megavars import edu.ie3.util.scala.quantities.SquantsUtils.RichElectricPotential +import org.apache.pekko.actor.typed.scaladsl.AskPattern._ +import org.apache.pekko.actor.typed.scaladsl.adapter.TypedActorRefOps +import org.apache.pekko.actor.typed.scaladsl.{ + ActorContext, + Behaviors, + StashBuffer, +} +import org.apache.pekko.actor.typed.{ActorRef, Behavior, Scheduler} +import org.apache.pekko.pattern.ask +import org.apache.pekko.util.{Timeout => PekkoTimeout} +import org.slf4j.Logger import squants.Each import squants.energy.Megawatts import java.time.{Duration, ZonedDateTime} import java.util.UUID import scala.concurrent.{ExecutionContext, Future} +import scala.util.{Failure, Success} /** Trait that is normally mixed into every [[GridAgent]] to enable distributed * forward backward sweep (DBFS) algorithm execution. It is considered to be * the standard behaviour of a [[GridAgent]]. */ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { - this: GridAgent => - // implicit ExecutionContext should be in scope - // see https://pekko.apache.org/docs/pekko/current/futures.html - implicit val ec: ExecutionContext = context.dispatcher - - when(SimulateGrid) { - - // first part of the grid simulation, same for all gridAgents on all levels - // we start with a forward-sweep by requesting the data from our child assets and grids (if any) - case Event( - Activation(currentTick), - gridAgentBaseData: GridAgentBaseData, - ) => - log.debug("Start sweep number: {}", gridAgentBaseData.currentSweepNo) - // hold tick and trigger for the whole time the dbfs is executed or - // at least until the the first full sweep is done (for superior grid agent only) - holdTick(currentTick) - - // 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 - askForAssetPowers( - currentTick, - gridAgentBaseData.sweepValueStores - .get(gridAgentBaseData.currentSweepNo), - gridAgentBaseData.gridEnv.nodeToAssetAgents, - gridAgentBaseData.gridEnv.gridModel.mainRefSystem, - gridAgentBaseData.powerFlowParams.sweepTimeout, - ) - // 2. inferior grids p/q values - askInferiorGridsForPowers( - gridAgentBaseData.currentSweepNo, - gridAgentBaseData.gridEnv.subgridGateToActorRef, - gridAgentBaseData.inferiorGridGates, - gridAgentBaseData.powerFlowParams.sweepTimeout, - ) + /** Method that defines the [[Behavior]] for simulating the grid. + * @param gridAgentData + * state data of the actor + * @param currentTick + * current simulation tick + * @return + * a [[Behavior]] + */ + private[grid] def simulateGrid( + gridAgentData: GridAgentData, + currentTick: Long, + )(implicit + constantData: GridAgentConstantData, + buffer: StashBuffer[GridAgentMessage], + ): Behavior[GridAgentMessage] = Behaviors.receivePartial { + case (ctx, message) => + (message, gridAgentData) match { + // first part of the grid simulation, same for all gridAgents on all levels + // we start with a forward-sweep by requesting the data from our child assets and grids (if any) + case ( + WrappedActivation(activation: Activation), + gridAgentBaseData: GridAgentBaseData, + ) => + ctx.log.debug( + "Start sweep number: {}", + gridAgentBaseData.currentSweepNo, + ) - // 3. superior grids slack voltage - askSuperiorGridsForSlackVoltages( - gridAgentBaseData.currentSweepNo, - gridAgentBaseData.gridEnv.subgridGateToActorRef, - gridAgentBaseData.superiorGridGates, - gridAgentBaseData.powerFlowParams.sweepTimeout, - ) + // 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 + askForAssetPowers( + currentTick, + gridAgentBaseData.sweepValueStores + .get(gridAgentBaseData.currentSweepNo), + gridAgentBaseData.gridEnv.nodeToAssetAgents, + gridAgentBaseData.gridEnv.gridModel.mainRefSystem, + gridAgentBaseData.powerFlowParams.sweepTimeout, + )(ctx) + + // 2. inferior grids p/q values + askInferiorGridsForPowers( + gridAgentBaseData.currentSweepNo, + gridAgentBaseData.gridEnv.subgridGateToActorRef, + gridAgentBaseData.inferiorGridGates, + gridAgentBaseData.powerFlowParams.sweepTimeout, + )(ctx) + + // 3. superior grids slack voltage + askSuperiorGridsForSlackVoltages( + gridAgentBaseData.currentSweepNo, + gridAgentBaseData.gridEnv.subgridGateToActorRef, + gridAgentBaseData.superiorGridGates, + gridAgentBaseData.powerFlowParams.sweepTimeout, + )(ctx) + + simulateGrid(gridAgentBaseData, activation.tick) + + // if we receive power values as response on our request, we process them here + case ( + receivedValues: ReceivedValues, + gridAgentBaseData: GridAgentBaseData, + ) => + // 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: ReceivedSlackVoltageValues => + gridAgentBaseData.updateWithReceivedSlackVoltages( + receivedSlacks + ) + case unknownReceivedValues => + throw new DBFSAlgorithmException( + s"Received unknown values: $unknownReceivedValues" + ) + } + + // check if we have enough data for a power flow calculation or a + // power differences check (if the grid agent is a superior agent) + // if yes, check if we have failing power flow in at least one of the inferior grids + // if there are failing ones, escalate the failure to the superior grid (if any), + // if not go to power flow or power differences check + // if we haven't received everything yet, stay and wait + val allValuesReceived = + updatedGridAgentBaseData.allRequestedDataReceived - stay() - - // if we receive power values as response on our request, we process them here - case Event( - receivedValues: ReceivedValues, - gridAgentBaseData: GridAgentBaseData, - ) => - // 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: ReceivedSlackVoltageValues => - gridAgentBaseData.updateWithReceivedSlackVoltages(receivedSlacks) - case unknownReceivedValues => - throw new DBFSAlgorithmException( - s"Received unknown values: $unknownReceivedValues" + ctx.log.debug( + "{}", + if (allValuesReceived) + "Got answers for all my requests for Slack Voltages and Power Values." + else + "Still waiting for answers my requests for Slack Voltages and Power Values.", ) - } - // check if we have enough data for a power flow calculation or a - // power differences check (if the grid agent is a superior agent) - // if yes, check if we have failing power flow in at least one of the inferior grids - // if there are failing ones, escalate the failure to the superior grid (if any), - // if not go to power flow or power differences check - // if we haven't received everything yet, stay and wait - val allValuesReceived = updatedGridAgentBaseData.allRequestedDataReceived - - log.debug( - "{}", - if (allValuesReceived) - "Got answers for all my requests for Slack Voltages and Power Values." - else - "Still waiting for answers my requests for Slack Voltages and Power Values.", - ) + if (gridAgentBaseData.isSuperior) { + goToCheckPowerDifferencesOrStay( + allValuesReceived, + updatedGridAgentBaseData, + currentTick, + simulateGrid, + )(ctx, constantData, buffer) + } else { + goToPowerFlowCalculationOrStay( + allValuesReceived, + updatedGridAgentBaseData, + currentTick, + simulateGrid, + )(ctx, constantData, buffer) + } - if (gridAgentBaseData.isSuperior) { - goToCheckPowerDifferencesOrStay( - allValuesReceived, - updatedGridAgentBaseData, - ) - } else { - goToPowerFlowCalculationOrStay( - allValuesReceived, - updatedGridAgentBaseData, - ) - } + // if we receive a request for slack voltages from our inferior grids we want to answer it + case ( + RequestSlackVoltageMessage( + currentSweepNo, + nodeUuids, + sender, + ), + gridAgentBaseData: GridAgentBaseData, + ) => + ctx.log.debug( + s"Received Slack Voltages request from {} for nodes {} and sweepNo: {}", + sender, + nodeUuids, + gridAgentBaseData.currentSweepNo, + ) - // if we receive a request for slack voltages from our inferior grids we want to answer it - case Event( - RequestSlackVoltageMessage(currentSweepNo, nodeUuids), - gridAgentBaseData: GridAgentBaseData, - ) => - log.debug( - s"Received Slack Voltages request from {} for nodes {} and sweepNo: {}", - sender(), - nodeUuids, - gridAgentBaseData.currentSweepNo, - ) + 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) + case None => + // this happens if this agent is either a) the superior grid agent, because it will always get a request for + // the next sweep, as it triggers calculations for the next sweep or b) at all other + // (non last downstream grid agents) in sweep 0 + ctx.log.debug( + "Unable to find slack voltage for nodes '{}' in sweep '{}'. Try to get voltage of previous sweep.", + nodeUuids, + currentSweepNo, + ) + gridAgentBaseData.sweepValueStores + .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 + val mainRefSystem = + gridAgentBaseData.gridEnv.gridModel.mainRefSystem + ( + mainRefSystem.vInSi(slackVoltageInPu.real), + mainRefSystem.vInSi(slackVoltageInPu.imag), + ) + case None => + throw new DBFSAlgorithmException( + s"Requested nodeUuid $nodeUuid " + + s"not found in sweep value store data for sweepNo: $sweepNo. This indicates" + + s"either a wrong processing of a previous sweep result or inconsistencies in grid model data!" + ) + } + }.getOrElse { + ctx.log.debug( + "Unable to get slack voltage for node '{}' in sweeps '{}' and '{}'. Returning target voltage.", + nodeUuid, + currentSweepNo, + currentSweepNo - 1, + ) - 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) - case None => - // this happens if this agent is either a) the superior grid agent, because it will always get a request for - // 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 nodes '{}' in sweep '{}'. Try to get voltage of previous sweep.", - nodeUuids, - currentSweepNo, - ) - gridAgentBaseData.sweepValueStores - .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 - val mainRefSystem = + val refSystem = gridAgentBaseData.gridEnv.gridModel.mainRefSystem + + /* Determine the slack node voltage under consideration of the target voltage set point */ + val vTarget = + gridAgentBaseData.gridEnv.gridModel.gridComponents.nodes + .find { case NodeModel(uuid, _, _, isSlack, _, _) => + uuid == nodeUuid && isSlack + } + .map(_.vTarget) + .getOrElse(Each(1d)) + val vSlack = + refSystem.nominalVoltage.multiplyWithDimensionles(vTarget) + ( - mainRefSystem.vInSi(slackVoltageInPu.real), - mainRefSystem.vInSi(slackVoltageInPu.imag), + vSlack, + refSystem.vInSi(0d), ) - case None => - throw new DBFSAlgorithmException( - s"Requested nodeUuid $nodeUuid " + - s"not found in sweep value store data for sweepNo: $sweepNo. This indicates" + - s"either a wrong processing of a previous sweep result or inconsistencies in grid model data!" + } match { + case (slackE, slackF) => + ctx.log.debug( + s"Provide {} to {} for node {} and sweepNo: {}", + s"$slackE, $slackF", + sender, + nodeUuid, + gridAgentBaseData.currentSweepNo, + ) + + ExchangeVoltage(nodeUuid, slackE, slackF) + } + } match { + case exchangeVoltages => + sender ! ProvideSlackVoltageMessage( + currentSweepNo, + exchangeVoltages, ) + Behaviors.same } - }.getOrElse { - log.debug( - "Unable to get slack voltage for node '{}' in sweeps '{}' and '{}'. Returning target voltage.", - nodeUuid, - currentSweepNo, - currentSweepNo - 1, - ) - val refSystem = - gridAgentBaseData.gridEnv.gridModel.mainRefSystem + // 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) + // or trigger a new run for the next sweepNo + case ( + msg @ WrappedPowerMessage( + RequestGridPowerMessage( + requestSweepNo, + _, + _, + ) + ), + gridAgentBaseData: GridAgentBaseData, + ) => + if (gridAgentBaseData.currentSweepNo == requestSweepNo) { + ctx.log.debug( + s"Received request for grid power values for sweepNo {} before my first power flow calc. Stashing away.", + requestSweepNo, + ) - /* Determine the slack node voltage under consideration of the target voltage set point */ - val vTarget = - gridAgentBaseData.gridEnv.gridModel.gridComponents.nodes - .find { case NodeModel(uuid, _, _, isSlack, _, _) => - uuid == nodeUuid && isSlack - } - .map(_.vTarget) - .getOrElse(Each(1d)) - val vSlack = - refSystem.nominalVoltage.multiplyWithDimensionles(vTarget) - - ( - vSlack, - refSystem.vInSi(0d), - ) - } match { - case (slackE, slackF) => - log.debug( - s"Provide {} to {} for node {} and sweepNo: {}", - s"$slackE, $slackF", - sender(), - nodeUuid, + buffer.stash(msg) + + Behaviors.same + } else { + ctx.log.debug( + s"Received request for grid power values for a NEW sweep (request: {}, my: {})", + requestSweepNo, gridAgentBaseData.currentSweepNo, ) + ctx.self ! PrepareNextSweepTrigger(currentTick) - ExchangeVoltage(nodeUuid, slackE, slackF) - } - } match { - case exchangeVoltages => - stay() replying ProvideSlackVoltageMessage( - currentSweepNo, - exchangeVoltages, - ) - } + buffer.stash(msg) - // 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) - // / or trigger a new run for the next sweepNo - case Event( - RequestGridPowerMessage(requestSweepNo, _), - gridAgentBaseData: GridAgentBaseData, - ) => - if (gridAgentBaseData.currentSweepNo == requestSweepNo) { - log.debug( - s"Received request for grid power values for sweepNo {} before my first power flow calc. Stashing away.", - requestSweepNo, - ) - stash() - stay() - } else { - log.debug( - s"Received request for grid power values for a NEW sweep (request: {}, my: {})", - requestSweepNo, - gridAgentBaseData.currentSweepNo, - ) - self ! PrepareNextSweepTrigger(currentTick) + simulateGrid( + gridAgentBaseData.copy(currentSweepNo = requestSweepNo), + currentTick, + ) + } - stash() - stay() using gridAgentBaseData.copy(currentSweepNo = requestSweepNo) - } + // after power flow calc for this sweepNo + case ( + WrappedPowerMessage( + RequestGridPowerMessage(_, requestedNodeUuids, sender) + ), + powerFlowDoneData @ PowerFlowDoneData( + gridAgentBaseData, + powerFlowResult, + pendingRequestAnswers, + ), + ) => + /* 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" + ) + ) - // / after power flow calc for this sweepNo - case Event( - RequestGridPowerMessage(_, requestedNodeUuids), - powerFlowDoneData @ PowerFlowDoneData( - gridAgentBaseData, - powerFlowResult, - pendingRequestAnswers, - ), - ) => - /* 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.subgridGateToActorRef - .map { case (subGridGate, _) => subGridGate.superiorNode } - .find(_.getUuid == firstRequestedNodeUuid) - .map(_.getSubnet) match { - case Some(requestingSubgridNumber) => - 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 - ) + gridAgentBaseData.gridEnv.subgridGateToActorRef + .map { case (subGridGate, _) => subGridGate.superiorNode } + .find(_.getUuid == firstRequestedNodeUuid) + .map(_.getSubnet) match { + case Some(requestingSubgridNumber) => + 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) * (-1), + refSystem.qInSi(qInPu) * (-1), + ) + case _ => + /* TODO: As long as there are no multiple slack nodes, provide "real" power only for the slack node */ + ( + Megawatts(0d), + Megavars(0d), + ) + } + .getOrElse { + throw new DBFSAlgorithmException( + s"Got a request for power @ node with uuid $requestedNodeUuids but cannot find it in my result data!" + ) + } } - .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) * (-1), - refSystem.qInSi(qInPu) * (-1), - ) - case _ => - /* TODO: As long as there are no multiple slack nodes, provide "real" power only for the slack node */ - ( - Megawatts(0d), - Megavars(0d), - ) + .map { case (nodeUuid, (p, q)) => + ProvideGridPowerMessage.ExchangePower( + nodeUuid, + p, + q, + ) } - .getOrElse { - throw new DBFSAlgorithmException( - s"Got a request for power @ node with uuid $requestedNodeUuids but cannot find it in my result data!" + + /* Determine the remaining replies */ + val stillPendingRequestAnswers = + 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 + // 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) { + gridAgentBaseData.storeSweepDataAndClearReceiveMaps( + validNewtonRaphsonPFResult, + gridAgentBaseData.superiorGridNodeUuids, + gridAgentBaseData.inferiorGridGates, + ) + } else { + powerFlowDoneData.copy(pendingRequestAnswers = + stillPendingRequestAnswers ) } - } - .map { case (nodeUuid, (p, q)) => - ProvideGridPowerMessage.ExchangePower( - nodeUuid, - p, - q, - ) - } - /* Determine the remaining replies */ - val stillPendingRequestAnswers = - 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 - // 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) { - gridAgentBaseData.storeSweepDataAndClearReceiveMaps( - validNewtonRaphsonPFResult, - gridAgentBaseData.superiorGridNodeUuids, - gridAgentBaseData.inferiorGridGates, - ) - } else { - powerFlowDoneData.copy(pendingRequestAnswers = - stillPendingRequestAnswers + sender ! WrappedPowerMessage( + ProvideGridPowerMessage(exchangePowers) ) - } + simulateGrid(updatedGridAgentBaseData, currentTick) - stay() replying - ProvideGridPowerMessage( - exchangePowers - ) using updatedGridAgentBaseData + case _: FailedNewtonRaphsonPFResult => + sender ! WrappedPowerMessage(FailedPowerFlow) + simulateGrid(gridAgentBaseData, currentTick) + } + case None => + /* It is not possible to determine, who has asked */ + ctx.log.error( + "I got a grid power request from a subgrid I don't know. Can't answer it properly." + ) - case _: FailedNewtonRaphsonPFResult => - stay() replying FailedPowerFlow using gridAgentBaseData + sender ! WrappedPowerMessage(FailedPowerFlow) + Behaviors.same } - case None => - /* It is not possible to determine, who has asked */ - log.error( - "I got a grid power request from a subgrid I don't know. Can't answer it properly." - ) - stay() replying FailedPowerFlow using gridAgentBaseData - } - // called when a grid power values request from a superior grid is received - // which is similar to a new sweep and causes a) a power flow with updated slack voltage values and - // b) afterwards a request for updated power values from inferior grids and assets with updated voltage values - // based on the just carried out power flow - case Event( - PrepareNextSweepTrigger(_), - gridAgentBaseData: GridAgentBaseData, - ) => - // request the updated slack voltages from the superior grid - askSuperiorGridsForSlackVoltages( - gridAgentBaseData.currentSweepNo, - gridAgentBaseData.gridEnv.subgridGateToActorRef, - gridAgentBaseData.superiorGridGates, - gridAgentBaseData.powerFlowParams.sweepTimeout, - ) - - log.debug(s"Going to {}", HandlePowerFlowCalculations) - - goto(HandlePowerFlowCalculations) using gridAgentBaseData - - // last step which should includes a) information on inferior grids about finish and - // b) cleanup of receiveMaps and sweepStore - case Event( - FinishGridSimulationTrigger(currentTick), - gridAgentBaseData: GridAgentBaseData, - ) => - // inform my child grids about the end of this grid simulation - gridAgentBaseData.inferiorGridGates - .map { - gridAgentBaseData.gridEnv.subgridGateToActorRef(_) - } - .distinct - .foreach(_ ! FinishGridSimulationTrigger(currentTick)) - - // inform every system participant about the end of this grid simulation - gridAgentBaseData.gridEnv.nodeToAssetAgents.foreach { case (_, actors) => - actors.foreach(actor => { - actor ! FinishGridSimulationTrigger( - currentTick - ) - }) - } + // called when a grid power values request from a superior grid is received + // which is similar to a new sweep and causes a) a power flow with updated slack voltage values and + // b) afterwards a request for updated power values from inferior grids and assets with updated voltage values + // based on the just carried out power flow + case ( + PrepareNextSweepTrigger(_), + gridAgentBaseData: GridAgentBaseData, + ) => + // request the updated slack voltages from the superior grid + askSuperiorGridsForSlackVoltages( + gridAgentBaseData.currentSweepNo, + gridAgentBaseData.gridEnv.subgridGateToActorRef, + gridAgentBaseData.superiorGridGates, + gridAgentBaseData.powerFlowParams.sweepTimeout, + )(ctx) + + ctx.log.debug(s"Going to HandlePowerFlowCalculation") + + handlePowerFlowCalculations(gridAgentBaseData, currentTick) + + // last step which should includes a) information on inferior grids about finish and + // b) cleanup of receiveMaps and sweepStore + case ( + FinishGridSimulationTrigger(currentTick), + gridAgentBaseData: GridAgentBaseData, + ) => + // inform my child grids about the end of this grid simulation + gridAgentBaseData.inferiorGridGates + .map { + gridAgentBaseData.gridEnv.subgridGateToActorRef(_) + } + .distinct + .foreach( + _ ! FinishGridSimulationTrigger(currentTick) + ) - // notify listener about the results - log.debug("Calculate results and sending the results to the listener ...") - createAndSendPowerFlowResults( - gridAgentBaseData, - currentTick.toDateTime(simStartTime), - ) + // inform every system participant about the end of this grid simulation + gridAgentBaseData.gridEnv.nodeToAssetAgents.foreach { + case (_, actors) => + actors.foreach { actor => + actor ! FinishParticipantSimulation(currentTick) + } + } - // do my cleanup stuff - log.debug("Doing my cleanup stuff") + // notify listener about the results + ctx.log.debug( + "Calculate results and sending the results to the listener ..." + ) + createAndSendPowerFlowResults( + gridAgentBaseData, + currentTick.toDateTime(constantData.simStartTime), + )(ctx.log, constantData) - // / clean copy of the gridAgentBaseData - val cleanedGridAgentBaseData = GridAgentBaseData.clean( - gridAgentBaseData, - gridAgentBaseData.superiorGridNodeUuids, - gridAgentBaseData.inferiorGridGates, - ) + // do my cleanup stuff + ctx.log.debug("Doing my cleanup stuff") - // / release tick for the whole simulation (StartGridSimulationTrigger) - releaseTick() + // / clean copy of the gridAgentBaseData + val cleanedGridAgentBaseData = GridAgentBaseData.clean( + gridAgentBaseData, + gridAgentBaseData.superiorGridNodeUuids, + gridAgentBaseData.inferiorGridGates, + ) - // / inform scheduler that we are done with the whole simulation and request new trigger for next time step - environmentRefs.scheduler ! Completion( - self.toTyped, - Some(currentTick + resolution), - ) + // / inform scheduler that we are done with the whole simulation and request new trigger for next time step + constantData.environmentRefs.scheduler ! Completion( + constantData.activationAdapter, + Some(currentTick + constantData.resolution), + ) - // return to Idle - goto(Idle) using cleanedGridAgentBaseData + // return to Idle + idle(cleanedGridAgentBaseData) + case _ => + // preventing "match may not be exhaustive" + Behaviors.unhandled + } } - /** Every power flow calculation should take place here. Generally used for - * power flow calculations only and only if all data required are already - * received as requested. + /** Method that defines the [[Behavior]] for handling the power flow + * calculations. Generally used for power flow calculations only and only if + * all data required are already received as requested. + * + * @param gridAgentData + * state data of the actor + * @param currentTick + * current simulation tick + * @return + * a [[Behavior]] */ - when(HandlePowerFlowCalculations) { - - // main method for power flow calculations - case Event( - DoPowerFlowTrigger(currentTick, _), - gridAgentBaseData: GridAgentBaseData, - ) => - log.debug( - "Received the following power values to the corresponding nodes: {}", - gridAgentBaseData.receivedValueStore.nodeToReceivedPower, - ) - - val gridModel = gridAgentBaseData.gridEnv.gridModel + private def handlePowerFlowCalculations( + gridAgentData: GridAgentData, + currentTick: Long, + )(implicit + constantData: GridAgentConstantData, + buffer: StashBuffer[GridAgentMessage], + ): Behavior[GridAgentMessage] = Behaviors.receivePartial { + case (ctx, message) => + (message, gridAgentData) match { + // main method for power flow calculations + case ( + DoPowerFlowTrigger(currentTick, _), + gridAgentBaseData: GridAgentBaseData, + ) => + ctx.log.debug( + "Received the following power values to the corresponding nodes: {}", + gridAgentBaseData.receivedValueStore.nodeToReceivedPower, + ) - val (operatingPoint, slackNodeVoltages) = composeOperatingPoint( - gridModel.gridComponents.nodes, - gridModel.gridComponents.transformers, - gridModel.gridComponents.transformers3w, - gridModel.nodeUuidToIndexMap, - gridAgentBaseData.receivedValueStore, - gridModel.mainRefSystem, - ) + val gridModel = gridAgentBaseData.gridEnv.gridModel - 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( - "{}", - composeValidNewtonRaphsonPFResultVoltagesDebugString( - validPowerFlowResult, - gridModel, - ), + val (operatingPoint, slackNodeVoltages) = composeOperatingPoint( + gridModel.gridComponents.nodes, + gridModel.gridComponents.transformers, + gridModel.gridComponents.transformers3w, + gridModel.nodeUuidToIndexMap, + gridAgentBaseData.receivedValueStore, + gridModel.mainRefSystem, ) - val powerFlowDoneData = - PowerFlowDoneData(gridAgentBaseData, validPowerFlowResult) + newtonRaphsonPF( + gridModel, + gridAgentBaseData.powerFlowParams.maxIterations, + operatingPoint, + slackNodeVoltages, + )(gridAgentBaseData.powerFlowParams.epsilon)(ctx.log) match { + // if res is valid, ask our assets (if any) for updated power values based on the newly determined nodal voltages + case validPowerFlowResult: ValidNewtonRaphsonPFResult => + ctx.log.debug( + "{}", + composeValidNewtonRaphsonPFResultVoltagesDebugString( + validPowerFlowResult, + gridModel, + ), + ) - val sweepValueStoreOpt = Some( - SweepValueStore( - validPowerFlowResult, - gridModel.gridComponents.nodes, - gridModel.nodeUuidToIndexMap, - ) - ) - askForAssetPowers( - currentTick, - sweepValueStoreOpt, - gridAgentBaseData.gridEnv.nodeToAssetAgents, - gridModel.mainRefSystem, - gridAgentBaseData.powerFlowParams.sweepTimeout, - ) match { - case None => - // when we don't have assets we can skip another request for different asset behaviour due to changed - // voltage values and go back to SimulateGrid directly - log.debug( - s"No generation or load assets in the grid. Going back to {}.", - SimulateGrid, + val powerFlowDoneData = + PowerFlowDoneData(gridAgentBaseData, validPowerFlowResult) + + val sweepValueStoreOpt = Some( + SweepValueStore( + validPowerFlowResult, + gridModel.gridComponents.nodes, + gridModel.nodeUuidToIndexMap, + ) ) - unstashAll() // we can answer the stashed grid power requests now - goto(SimulateGrid) using powerFlowDoneData - case Some(_) => - // will return a future based on the `ask-pattern` which will be processed below - stay() using powerFlowDoneData - } - case failedNewtonRaphsonPFResult: FailedNewtonRaphsonPFResult => - val powerFlowDoneData = - PowerFlowDoneData(gridAgentBaseData, failedNewtonRaphsonPFResult) - log.warning( - "Power flow calculation before asking for updated powers 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 - } + if ( + askForAssetPowers( + currentTick, + sweepValueStoreOpt, + gridAgentBaseData.gridEnv.nodeToAssetAgents, + gridModel.mainRefSystem, + gridAgentBaseData.powerFlowParams.sweepTimeout, + )(ctx) + ) { + // will return a future based on the `ask-pattern` which will be processed below + handlePowerFlowCalculations(powerFlowDoneData, currentTick) + } else { + // when we don't have assets we can skip another request for different asset behaviour due to changed + // voltage values and go back to SimulateGrid directly + ctx.log.debug( + s"No generation or load assets in the grid. Going back to SimulateGrid." + ) - // handler for the future provided by `askForAssetPowers` to check if there are any changes in generation/load - // of assets based on updated nodal voltages - case Event( - receivedPowerValues: ReceivedPowerValues, - powerFlowDoneData: PowerFlowDoneData, - ) => - val gridAgentBaseData = powerFlowDoneData.gridAgentBaseData - - // check if we have changed values from our assets - // 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 (_, _: AssetPowerChangedMessage) => true - case _ => false - } + // we can answer the stashed grid power requests now + buffer.unstashAll( + simulateGrid(powerFlowDoneData, currentTick) + ) + } - if (changed) { - log.debug( - "Assets have changed their exchanged power with the grid. Update nodal powers and prepare new power flow." - ) - val updatedGridAgentBaseData: GridAgentBaseData = - receivedPowerValues match { - case receivedPowers: ReceivedPowerValues => - gridAgentBaseData.updateWithReceivedPowerValues( - receivedPowers, - replace = true, - ) - case unknownValuesReceived => - throw new DBFSAlgorithmException( - s"Received unsuitable values: $unknownValuesReceived" + case failedNewtonRaphsonPFResult: FailedNewtonRaphsonPFResult => + val powerFlowDoneData = + PowerFlowDoneData( + gridAgentBaseData, + failedNewtonRaphsonPFResult, + ) + ctx.log.warn( + "Power flow calculation before asking for updated powers did finally not converge!" ) + // we can answer the stashed grid power requests now and report a failed power flow back + buffer.unstashAll(simulateGrid(powerFlowDoneData, currentTick)) } - // check if we have enough data for a power flow calculation - // if yes, go to the powerflow - // if no, stay and wait - val readyForPowerFlow = - updatedGridAgentBaseData.allRequestedDataReceived - log.debug( - "{}", - if (readyForPowerFlow) - "Got answers for all my requests for Slack Voltages and Power Values." - else - "Still waiting for answers my requests for Slack Voltages and Power Values.", - ) - - goToPowerFlowCalculationOrStay( - readyForPowerFlow, - updatedGridAgentBaseData, - ) - - } else { - // no changes from assets, we want to go back to SimulateGrid and report the LF results to our parent grids if any requests - log.debug( - s"Assets have not changed their exchanged power or no voltage dependent behaviour. Going back to {}.", - SimulateGrid, - ) - unstashAll() // we can answer the stashed grid power requests now - goto(SimulateGrid) using powerFlowDoneData + // handler for the future provided by `askForAssetPowers` to check if there are any changes in generation/load + // of assets based on updated nodal voltages + case ( + receivedPowerValues: ReceivedPowerValues, + powerFlowDoneData: PowerFlowDoneData, + ) => + val gridAgentBaseData = powerFlowDoneData.gridAgentBaseData + + // check if we have changed values from our assets + // 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 (_, _: AssetPowerChangedMessage) => true + case _ => false + } - } + if (changed) { + ctx.log.debug( + "Assets have changed their exchanged power with the grid. Update nodal powers and prepare new power flow." + ) + val updatedGridAgentBaseData: GridAgentBaseData = + receivedPowerValues match { + case receivedPowers: ReceivedPowerValues => + gridAgentBaseData.updateWithReceivedPowerValues( + receivedPowers, + replace = true, + ) + case unknownValuesReceived => + throw new DBFSAlgorithmException( + s"Received unsuitable values: $unknownValuesReceived" + ) + } - // executed after request from the superior grid to execute a new sweep (forward sweep state) - // 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: ReceivedSlackVoltageValues, - gridAgentBaseData: GridAgentBaseData, - ) => - log.debug( - "Received Slack values for new forward sweep with same power but updated voltage values" - ) + // check if we have enough data for a power flow calculation + // if yes, go to the powerflow + // if no, stay and wait + val readyForPowerFlow = + updatedGridAgentBaseData.allRequestedDataReceived + ctx.log.debug( + "{}", + if (readyForPowerFlow) + "Got answers for all my requests for Slack Voltages and Power Values." + else + "Still waiting for answers my requests for Slack Voltages and Power Values.", + ) - // power flow - val gridModel = gridAgentBaseData.gridEnv.gridModel - val previousSweepData = gridAgentBaseData.sweepValueStores.getOrElse( - gridAgentBaseData.currentSweepNo - 1, - throw new DBFSAlgorithmException( - s"$actorName Unable to get results from previous sweep ${gridAgentBaseData.currentSweepNo - 1}!" - ), - ) + goToPowerFlowCalculationOrStay( + readyForPowerFlow, + updatedGridAgentBaseData, + currentTick, + handlePowerFlowCalculations, + )(ctx, constantData, buffer) - val (operatingPoint, slackNodeVoltages) = - composeOperatingPointWithUpdatedSlackVoltages( - receivedSlackValues, - previousSweepData.sweepData, - gridModel.gridComponents.transformers, - gridModel.gridComponents.transformers3w, - gridModel.mainRefSystem, - ) + } else { + // no changes from assets, we want to go back to SimulateGrid and report the LF results to our parent grids if any requests + ctx.log.debug( + s"Assets have not changed their exchanged power or no voltage dependent behaviour. Going back to SimulateGrid." + ) + // we can answer the stashed grid power requests now + buffer.unstashAll(simulateGrid(powerFlowDoneData, currentTick)) + } - newtonRaphsonPF( - gridModel, - gridAgentBaseData.powerFlowParams.maxIterations, - operatingPoint, - slackNodeVoltages, - )(gridAgentBaseData.powerFlowParams.epsilon) match { - case validPowerFlowResult: ValidNewtonRaphsonPFResult => - log.debug( - "{}", - composeValidNewtonRaphsonPFResultVoltagesDebugString( - validPowerFlowResult, - gridModel, - ), + // executed after request from the superior grid to execute a new sweep (forward sweep state) + // 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 ( + receivedSlackValues: ReceivedSlackVoltageValues, + gridAgentBaseData: GridAgentBaseData, + ) => + ctx.log.debug( + "Received Slack values for new forward sweep with same power but updated voltage values" ) - // update the data - val sweepValueStore = SweepValueStore( - validPowerFlowResult, - gridModel.gridComponents.nodes, - gridModel.nodeUuidToIndexMap, + // power flow + val gridModel = gridAgentBaseData.gridEnv.gridModel + val previousSweepData = gridAgentBaseData.sweepValueStores.getOrElse( + gridAgentBaseData.currentSweepNo - 1, + throw new DBFSAlgorithmException( + s"${gridAgentBaseData.actorName}: Unable to get results from previous sweep ${gridAgentBaseData.currentSweepNo - 1}!" + ), ) - 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, + val (operatingPoint, slackNodeVoltages) = + composeOperatingPointWithUpdatedSlackVoltages( + receivedSlackValues, + previousSweepData.sweepData, + gridModel.gridComponents.transformers, + gridModel.gridComponents.transformers3w, gridModel.mainRefSystem, - gridAgentBaseData.powerFlowParams.sweepTimeout, ) - // 2. inferior grids p/q values - val askForInferiorGridPowersOpt = - askInferiorGridsForPowers( - gridAgentBaseData.currentSweepNo, - gridAgentBaseData.gridEnv.subgridGateToActorRef, - gridAgentBaseData.inferiorGridGates, - gridAgentBaseData.powerFlowParams.sweepTimeout, - ) + newtonRaphsonPF( + gridModel, + gridAgentBaseData.powerFlowParams.maxIterations, + operatingPoint, + slackNodeVoltages, + )(gridAgentBaseData.powerFlowParams.epsilon)(ctx.log) match { + case validPowerFlowResult: ValidNewtonRaphsonPFResult => + ctx.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, + )(ctx) + + // 2. inferior grids p/q values + val askForInferiorGridPowersOpt = + askInferiorGridsForPowers( + gridAgentBaseData.currentSweepNo, + gridAgentBaseData.gridEnv.subgridGateToActorRef, + gridAgentBaseData.inferiorGridGates, + gridAgentBaseData.powerFlowParams.sweepTimeout, + )(ctx) + + // 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 + if (!askForAssetPowersOpt && !askForInferiorGridPowersOpt) { + ctx.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." + ) - val powerFlowDoneData = - PowerFlowDoneData(gridAgentBaseData, validPowerFlowResult) + val powerFlowDoneData = + PowerFlowDoneData(gridAgentBaseData, validPowerFlowResult) - unstashAll() // we can answer the stashed grid power requests now - goto(SimulateGrid) using powerFlowDoneData + // we can answer the stashed grid power requests now + buffer.unstashAll( + simulateGrid(powerFlowDoneData, currentTick) + ) + } else { + ctx.log.debug( + "Going back to SimulateGrid and wait for my assets or inferior grids to return." + ) - case _ => - log.debug( - "Going back to SimulateGrid and wait for my assets or inferior grids to return." - ) + // go back to simulate grid + simulateGrid( + gridAgentBaseData + .updateWithReceivedSlackVoltages(receivedSlackValues) + .copy(sweepValueStores = updatedSweepValueStore), + currentTick, + ) + } - // go back to simulate grid - goto(SimulateGrid) using gridAgentBaseData - .updateWithReceivedSlackVoltages(receivedSlackValues) - .copy(sweepValueStores = updatedSweepValueStore) + case failedNewtonRaphsonPFResult: FailedNewtonRaphsonPFResult => + val powerFlowDoneData = + PowerFlowDoneData( + gridAgentBaseData, + failedNewtonRaphsonPFResult, + ) + ctx.log.warn( + "Power flow with updated slack voltage did finally not converge!" + ) + // we can answer the stashed grid power requests now and report a failed power flow back + buffer.unstashAll(simulateGrid(powerFlowDoneData, currentTick)) } - case failedNewtonRaphsonPFResult: FailedNewtonRaphsonPFResult => - val powerFlowDoneData = - PowerFlowDoneData( - gridAgentBaseData, - failedNewtonRaphsonPFResult, - ) - log.warning( - "Power flow with updated slack voltage did finally not converge!" + // happens only when we received slack data and power values before we received a request to provide grid data + // (only possible when first simulation triggered and this agent is faster in this state as the request + // by a superior grid arrives) + case ( + msg: WrappedPowerMessage, + _: GridAgentBaseData, + ) => + ctx.log.debug( + "Received Request for Grid Power too early. Stashing away" ) - unstashAll() // we can answer the stashed grid power requests now and report a failed power flow back - goto(SimulateGrid) using powerFlowDoneData - } + buffer.stash(msg) + Behaviors.same + + // happens only when we received slack data and power values before we received a request to provide grid + // (only possible when first simulation triggered and this agent is faster + // with its power flow calculation in this state as the request by a superior grid arrives) + case ( + msg: WrappedPowerMessage, + _: PowerFlowDoneData, + ) => + ctx.log.debug( + "Received Request for Grid Power too early. Stashing away" + ) - // happens only when we received slack data and power values before we received a request to provide grid data - // (only possible when first simulation triggered and this agent is faster in this state as the request - // by a superior grid arrives) - case Event( - _: RequestGridPowerMessage, - _: GridAgentBaseData, - ) => - log.debug("Received Request for Grid Power too early. Stashing away") - stash() - stay() - - // happens only when we received slack data and power values before we received a request to provide gride - // (only possible when first simulation triggered and this agent is faster - // with its power flow calculation in this state as the request by a superior grid arrives) - case Event( - _: RequestGridPowerMessage, - _: PowerFlowDoneData, - ) => - log.debug("Received Request for Grid Power too early. Stashing away") - stash() - stay() + buffer.stash(msg) + Behaviors.same + case _ => + // preventing "match may not be exhaustive" + Behaviors.unhandled + } } - // should be reached by the superior (dummy) grid agent only - when(CheckPowerDifferences) { + /** Method used for checking the power difference.

This method should only + * be reached by the superior (dummy) grid agent. + * @param gridAgentBaseData + * state data of the actor + * @return + * a [[Behavior]] + */ + private def checkPowerDifferences( + gridAgentBaseData: GridAgentBaseData + )(implicit + constantData: GridAgentConstantData, + buffer: StashBuffer[GridAgentMessage], + ): Behavior[GridAgentMessage] = Behaviors.receivePartial { - case Event( - CheckPowerDifferencesTrigger(currentTick), - gridAgentBaseData: GridAgentBaseData, - ) => - log.debug("Starting the power differences check ...") + case (ctx, CheckPowerDifferencesTrigger(currentTick)) => + ctx.log.debug("Starting the power differences check ...") val currentSweepNo = gridAgentBaseData.currentSweepNo val gridModel = gridAgentBaseData.gridEnv.gridModel @@ -770,7 +866,7 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { val nodeData = operationPoint.map(StateData(_)) ValidNewtonRaphsonPFResult(-1, nodeData, DenseMatrix(0d, 0d)) } else { - log.debug( + ctx.log.debug( "This grid contains a three winding transformer. Perform power flow calculations before assessing the power deviations." ) newtonRaphsonPF( @@ -778,9 +874,9 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { gridAgentBaseData.powerFlowParams.maxIterations, operationPoint, slackNodeVoltages, - )(gridAgentBaseData.powerFlowParams.epsilon) match { + )(gridAgentBaseData.powerFlowParams.epsilon)(ctx.log) match { case validPowerFlowResult: ValidNewtonRaphsonPFResult => - log.debug( + ctx.log.debug( "{}", composeValidNewtonRaphsonPFResultVoltagesDebugString( validPowerFlowResult, @@ -805,13 +901,17 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { // the difference is checked @ the higher nodes of our transformers => the slack nodes // if we are either in the first backward sweep OR if the deviation is bigger as allowed, we need a second sweep if (gridAgentBaseData.sweepValueStores.isEmpty) { - log.debug("Sweep value store is empty. Starting a second sweep ...") + ctx.log.debug( + "Sweep value store is empty. Starting a second sweep ..." + ) goToSimulateGridForNextSweepWith( updatedGridAgentBaseData, currentTick, ) } else { - log.debug("Sweep value store is not empty. Check for deviation ...") + ctx.log.debug( + "Sweep value store is not empty. Check for deviation ..." + ) // calculate deviation vector for all nodes val previousSweepNodePower: DenseVector[Complex] = @@ -850,7 +950,7 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { Math.abs(complex.imag) >= allowedDeviation }) match { case Some(deviation) => // next sweep - log.debug( + ctx.log.debug( "Deviation between the last two sweeps: {}", deviation, ) @@ -859,56 +959,59 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { currentTick, ) case None => // we're done - log.debug("We found a result! :-)") + ctx.log.debug("We found a result! :-)") - log.debug( + ctx.log.debug( "Final deviation: {}", (previousSweepNodePower - currentSweepNodePower).toScalaVector, ) // go back to SimulateGrid and trigger a finish - self ! FinishGridSimulationTrigger(currentTick) - goto(SimulateGrid) - + ctx.self ! FinishGridSimulationTrigger(currentTick) + simulateGrid(gridAgentBaseData, currentTick) } } case failedResult: PowerFlowResult.FailedPowerFlowResult => - log.warning( + ctx.log.warn( "Power flow for high voltage branch of three winding transformer failed after {} iterations. Cause: {}", failedResult.iteration, failedResult.cause, ) - self ! FinishGridSimulationTrigger(currentTick) - handlePowerFlowFailure(gridAgentBaseData) - goto(SimulateGrid) using gridAgentBaseData + ctx.self ! FinishGridSimulationTrigger(currentTick) + handlePowerFlowFailure(gridAgentBaseData, currentTick, ctx) } } /** Checks if all data has been received and if yes checks if the there are * any failed power flow indications from inferior grids. If both == true, - * then no state change is triggered but the sweep value store is updated - * with a [[FailedPowerFlow]] information as well, the now used data is set - * to [[PowerFlowDoneData]] and this is escalated to the superior grid(s). If - * there is no [[FailedPowerFlow]] in the [[GridAgentBaseData]] a state - * transition to [[HandlePowerFlowCalculations]] is triggered. + * then no [[Behavior]] change is triggered but the sweep value store is + * updated with a [[FailedPowerFlow]] information as well, the now used data + * is set to [[PowerFlowDoneData]] and this is escalated to the superior + * grid(s). If there is no [[FailedPowerFlow]] in the [[GridAgentBaseData]] a + * behavior transition to [[handlePowerFlowCalculations]] is triggered. * - * If allReceived == false, no state transition is triggered + * If allReceived == false, no [[Behavior]] transition is triggered * * @param allReceived * indicates if all requested data has been received * @param gridAgentBaseData * the current or updated data of the [[GridAgent]] * @return - * either the same state the agent is currently in or a transition to - * [[HandlePowerFlowCalculations]] + * either the same behavior the agent is currently in or a transition to + * [[handlePowerFlowCalculations]] */ private def goToPowerFlowCalculationOrStay( allReceived: Boolean, gridAgentBaseData: GridAgentBaseData, - ): FSM.State[AgentState, GridAgentData] = { - + currentTick: Long, + behavior: (GridAgentData, Long) => Behavior[GridAgentMessage], + )(implicit + ctx: ActorContext[GridAgentMessage], + constantData: GridAgentConstantData, + buffer: StashBuffer[GridAgentMessage], + ): Behavior[GridAgentMessage] = if (allReceived) { - log.debug( + ctx.log.debug( "All power values of inferior grids, assets + voltage superior grid slack voltages received." ) @@ -918,27 +1021,33 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { .exists(v => v.exists(k => k._2.contains(FailedPowerFlow))) if (powerFlowFailedSomewhereInInferior) { - log.warning("Received Failed Power Flow Result. Escalate to my parent.") - unstashAll() // we want to answer the requests from our parent - stay() using PowerFlowDoneData( + ctx.log.warn( + "Received Failed Power Flow Result. Escalate to my parent." + ) + + // we want to answer the requests from our parent + val powerFlowDoneData = PowerFlowDoneData( gridAgentBaseData, FailedNewtonRaphsonPFResult(-1, CalculationFailed), ) + + buffer.unstashAll(behavior(powerFlowDoneData, currentTick)) } else { - self ! DoPowerFlowTrigger(currentTick, gridAgentBaseData.currentSweepNo) - goto(HandlePowerFlowCalculations) using gridAgentBaseData + ctx.self ! DoPowerFlowTrigger( + currentTick, + gridAgentBaseData.currentSweepNo, + ) + handlePowerFlowCalculations(gridAgentBaseData, currentTick) } } else { - log.debug( + ctx.log.debug( "Still waiting for asset or grid power values or slack voltage information of inferior grids" ) - stay() using gridAgentBaseData + behavior(gridAgentBaseData, currentTick) } - } - /** Normally only reached by the superior (dummy) agent! * * Checks if all data has been received and if yes checks if the there are @@ -947,8 +1056,8 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { * this step is skipped and the simulation goes on or this leads to a * termination of the simulation due to a failed power flow calculation. * - * If there is no [[FailedPowerFlow]] in the [[GridAgentBaseData]] a state - * transition to [[CheckPowerDifferences]] is triggered. + * If there is no [[FailedPowerFlow]] in the [[GridAgentBaseData]] a + * [[Behavior]] transition to [[checkPowerDifferences]] is triggered. * * If allReceived == false, no state transition is triggered * @@ -959,15 +1068,23 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { * @param gridAgentBaseData * the current or updated data of the [[GridAgent]] * @return - * either the same state the agent is currently in or a transition to - * [[CheckPowerDifferences]] + * either the same behavior the agent is currently in or a transition to + * [[checkPowerDifferences]] */ private def goToCheckPowerDifferencesOrStay( allReceived: Boolean, gridAgentBaseData: GridAgentBaseData, - ): FSM.State[AgentState, GridAgentData] = { + currentTick: Long, + behavior: (GridAgentData, Long) => Behavior[GridAgentMessage], + )(implicit + ctx: ActorContext[GridAgentMessage], + constantData: GridAgentConstantData, + buffer: StashBuffer[GridAgentMessage], + ): Behavior[GridAgentMessage] = { if (allReceived) { - log.debug("All power values of child assets + inferior grids received.") + ctx.log.debug( + "All power values of child assets + inferior grids received." + ) // if our superior grid received a FailedPowerFlow from the inferior grids it has to trigger a finish // of the simulation which will either result in a skip of this time step OR a termination of the simulation run @@ -979,60 +1096,73 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { ) ) if (powerFlowFailedSomewhere) { - log.warning("Power flow failed! This incident will be reported!") - self ! FinishGridSimulationTrigger(currentTick) - handlePowerFlowFailure(gridAgentBaseData) - goto(SimulateGrid) using gridAgentBaseData + ctx.log.warn("Power flow failed! This incident will be reported!") + ctx.self ! FinishGridSimulationTrigger(currentTick) + + handlePowerFlowFailure(gridAgentBaseData, currentTick, ctx) } else { - self ! CheckPowerDifferencesTrigger(currentTick) - goto(CheckPowerDifferences) using gridAgentBaseData + ctx.self ! CheckPowerDifferencesTrigger(currentTick) + checkPowerDifferences(gridAgentBaseData) } } else { - log.debug( + ctx.log.debug( "Still waiting for asset or grid power values or slack voltage information of inferior grids" ) - stay() using gridAgentBaseData + behavior(gridAgentBaseData, currentTick) } } + /** Method for handling failed power flows. + * @param gridAgentBaseData + * state data of the actor + * @param currentTick + * of the simulation + */ private def handlePowerFlowFailure( - gridAgentBaseData: GridAgentBaseData - ): Unit = { - environmentRefs.runtimeEventListener ! PowerFlowFailed + gridAgentBaseData: GridAgentBaseData, + currentTick: Long, + ctx: ActorContext[GridAgentMessage], + )(implicit + constantData: GridAgentConstantData, + buffer: StashBuffer[GridAgentMessage], + ): Behavior[GridAgentMessage] = { + constantData.environmentRefs.runtimeEventListener ! PowerFlowFailed if (gridAgentBaseData.powerFlowParams.stopOnFailure) { - log.error("Stopping because of failed power flow.") - self ! PoisonPill + ctx.log.error("Stopping because of failed power flow.") + Behaviors.stopped } + + simulateGrid(gridAgentBaseData, currentTick) } /** Normally only reached by the superior (dummy) agent! * - * Triggers a state transition to [[SimulateGrid]], informs the + * Triggers a [[Behavior]] transition to [[simulateGrid]], informs the * [[edu.ie3.simona.scheduler.Scheduler]] about the finish of this sweep and * requests a new trigger for itself for a new sweep * * @param gridAgentBaseData * the [[GridAgentBaseData]] that should be used in the next sweep in - * [[SimulateGrid]] + * [[simulateGrid]] * @param currentTick * current tick the agent is in * @return - * a state transition to [[SimulateGrid]] + * a behavior transition to [[simulateGrid]] */ private def goToSimulateGridForNextSweepWith( gridAgentBaseData: GridAgentBaseData, currentTick: Long, - ): FSM.State[AgentState, GridAgentData] = { - - releaseTick() - environmentRefs.scheduler ! Completion( - self.toTyped, + )(implicit + constantData: GridAgentConstantData, + buffer: StashBuffer[GridAgentMessage], + ): Behavior[GridAgentMessage] = { + constantData.environmentRefs.scheduler ! Completion( + constantData.activationAdapter, Some(currentTick), ) - goto(SimulateGrid) using gridAgentBaseData - + simulateGrid(gridAgentBaseData, currentTick) } /** Triggers an execution of the pekko `ask` pattern for all power values of @@ -1044,7 +1174,7 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { * the current sweep value store containing the current node voltage for * the assets * @param nodeToAssetAgents - * a map contains a mapping between nodes and the [[ActorRef]] s located @ + * a map contains a mapping between nodes and the [[ActorRef]] s located \@ * those nodes * @param refSystem * the reference system of the [[edu.ie3.simona.model.grid.GridModel]] of @@ -1052,71 +1182,72 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { * @param askTimeout * a timeout for the request * @return - * Some(Future[ReceivedPowerValues]) if this grids contains assets or None - * if no request has been send due to non-existence of assets + * true if this grids contains assets or false if no request has been send + * due to non-existence of assets */ private def askForAssetPowers( currentTick: Long, sweepValueStore: Option[SweepValueStore], - nodeToAssetAgents: Map[UUID, Set[ActorRef]], + nodeToAssetAgents: Map[UUID, Set[ActorRef[ParticipantMessage]]], refSystem: RefSystem, askTimeout: Duration, - ): Option[Future[ReceivedPowerValues]] = { - + )(implicit + ctx: ActorContext[GridAgentMessage] + ): Boolean = { implicit val timeout: PekkoTimeout = PekkoTimeout.create(askTimeout) + implicit val ec: ExecutionContext = ctx.executionContext - log.debug(s"asking assets for power values: {}", nodeToAssetAgents) - - if (nodeToAssetAgents.values.flatten.nonEmpty) - Some( - Future - .sequence( - nodeToAssetAgents.flatten { case (nodeUuid, assetActorRefs) => - assetActorRefs.map(assetAgent => { - - val (eInPu, fInPU) = - sweepValueStore match { - case Some(sweepValueStore) => - val (eInSi, fInSi) = 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(eInSi), - refSystem.vInPu(fInSi), - ) - case None => - ( - Each(1d), - Each(0d), - ) - } + ctx.log.debug(s"asking assets for power values: {}", nodeToAssetAgents) - (assetAgent ? RequestAssetPowerMessage( - currentTick, - eInPu, - fInPU, - )).map { - case providedPowerValuesMessage: AssetPowerChangedMessage => - (assetAgent, providedPowerValuesMessage) - case assetPowerUnchangedMessage: AssetPowerUnchangedMessage => - (assetAgent, assetPowerUnchangedMessage) + if (nodeToAssetAgents.values.flatten.nonEmpty) { + val future = Future + .sequence( + nodeToAssetAgents.flatten { case (nodeUuid, assetActorRefs) => + assetActorRefs.map(assetAgent => { + + val (eInPu, fInPU) = + sweepValueStore match { + case Some(sweepValueStore) => + val (eInSi, fInSi) = 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(eInSi), + refSystem.vInPu(fInSi), + ) + case None => + ( + Each(1d), + Each(0d), + ) } - }) - }.toVector - ) - .map(ReceivedAssetPowerValues) - .pipeTo(self) - ) - else - None + + (assetAgent.toClassic ? RequestAssetPowerMessage( + currentTick, + eInPu, + fInPU, + )).map { + case providedPowerValuesMessage: AssetPowerChangedMessage => + (assetAgent, providedPowerValuesMessage) + case assetPowerUnchangedMessage: AssetPowerUnchangedMessage => + (assetAgent, assetPowerUnchangedMessage) + } + }) + }.toVector + ) + .map(res => ReceivedAssetPowerValues(res)) + + pipeToSelf(future, ctx) + true + } else false } /** Triggers an execution of the pekko `ask` pattern for all power values @ @@ -1130,22 +1261,28 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { * @param askTimeout * a timeout for the request * @return - * Some(Future[ReceivedPowerValues]) if this grids has connected inferior - * grids or None if this no inferior grids + * true if this grids has connected inferior grids or false if this no + * inferior grids */ private def askInferiorGridsForPowers( currentSweepNo: Int, - subGridGateToActorRef: Map[SubGridGate, ActorRef], + subGridGateToActorRef: Map[SubGridGate, ActorRef[GridAgentMessage]], inferiorGridGates: Seq[SubGridGate], askTimeout: Duration, - ): Option[Future[ReceivedPowerValues]] = { + )(implicit + ctx: ActorContext[GridAgentMessage] + ): Boolean = { implicit val timeout: PekkoTimeout = PekkoTimeout.create(askTimeout) - log.debug( + implicit val ec: ExecutionContext = ctx.executionContext + implicit val scheduler: Scheduler = ctx.system.scheduler + + ctx.log.debug( s"asking inferior grids for power values: {}", inferiorGridGates, ) - Option.when(inferiorGridGates.nonEmpty) { - Future + + if (inferiorGridGates.nonEmpty) { + val future = Future .sequence( inferiorGridGates .map { inferiorGridGate => @@ -1161,21 +1298,31 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { inferiorGridGates } .map { case (inferiorGridAgentRef, inferiorGridGateNodes) => - (inferiorGridAgentRef ? RequestGridPowerMessage( - currentSweepNo, - inferiorGridGateNodes.distinct, - )).map { - case provideGridPowerMessage: ProvideGridPowerMessage => - (inferiorGridAgentRef, provideGridPowerMessage) - case FailedPowerFlow => - (inferiorGridAgentRef, FailedPowerFlow) - } + inferiorGridAgentRef + .ask[GridAgentMessage](ref => + WrappedPowerMessage( + RequestGridPowerMessage( + currentSweepNo, + inferiorGridGateNodes.distinct, + ref, + ) + ) + ) + .map { + case WrappedPowerMessage( + provideGridPowerMessage: ProvideGridPowerMessage + ) => + (inferiorGridAgentRef, provideGridPowerMessage) + case WrappedPowerMessage(FailedPowerFlow) => + (inferiorGridAgentRef, FailedPowerFlow) + } } .toVector ) - .map(ReceivedGridPowerValues) - .pipeTo(self) - } + .map(res => ReceivedGridPowerValues(res)) + pipeToSelf(future, ctx) + true + } else false } /** Triggers an execution of the pekko `ask` pattern for all slack voltages of @@ -1189,39 +1336,50 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { * @param askTimeout * a timeout for the request * @return - * Some(Future[ReceivedSlackValues]) if this grids has connected superior - * grids or None if this no superior grids + * true if this grids has connected superior grids or false if this no + * superior grids */ private def askSuperiorGridsForSlackVoltages( currentSweepNo: Int, - subGridGateToActorRef: Map[SubGridGate, ActorRef], + subGridGateToActorRef: Map[SubGridGate, ActorRef[GridAgentMessage]], superiorGridGates: Vector[SubGridGate], askTimeout: Duration, - ): Option[Future[ReceivedSlackVoltageValues]] = { + )(implicit + ctx: ActorContext[GridAgentMessage] + ): Boolean = { implicit val timeout: PekkoTimeout = PekkoTimeout.create(askTimeout) - log.debug( + implicit val ec: ExecutionContext = ctx.executionContext + implicit val scheduler: Scheduler = ctx.system.scheduler + + ctx.log.debug( s"asking superior grids for slack voltage values: {}", superiorGridGates, ) - Option.when(superiorGridGates.nonEmpty) { - Future + if (superiorGridGates.nonEmpty) { + val future = Future .sequence( superiorGridGates .groupBy(subGridGateToActorRef(_)) .map { case (superiorGridAgent, gridGates) => - (superiorGridAgent ? RequestSlackVoltageMessage( - currentSweepNo, - gridGates.map(_.superiorNode.getUuid), - )).map { case providedSlackValues: ProvideSlackVoltageMessage => - (superiorGridAgent, providedSlackValues) - } + superiorGridAgent + .ask[GridAgentMessage](ref => + RequestSlackVoltageMessage( + currentSweepNo, + gridGates.map(_.superiorNode.getUuid), + ref, + ) + ) + .map { case providedSlackValues: ProvideSlackVoltageMessage => + (superiorGridAgent, providedSlackValues) + } } .toVector ) - .map(ReceivedSlackVoltageValues) - .pipeTo(self) - } + .map(res => ReceivedSlackVoltageValues(res)) + pipeToSelf(future, ctx) + true + } else false } /** Create an instance of @@ -1236,21 +1394,42 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { * @param currentTimestamp * the current time stamp */ - def createAndSendPowerFlowResults( + private def createAndSendPowerFlowResults( gridAgentBaseData: GridAgentBaseData, currentTimestamp: ZonedDateTime, + )(implicit + log: Logger, + constantData: GridAgentConstantData, ): Unit = { gridAgentBaseData.sweepValueStores.lastOption.foreach { case (_, valueStore) => - notifyListener( + constantData.notifyListeners( this.createResultModels( gridAgentBaseData.gridEnv.gridModel, valueStore, )( - currentTimestamp + currentTimestamp, + log, ) ) } } + /** This method uses [[ActorContext.pipeToSelf()]] to send a future message to + * itself. If the future is a [[Success]] the message is send, else a + * [[ReceivedFailure]] with the thrown error is send. + * @param future + * future message that should be send to the agent after it was processed + * @param ctx + * [[ActorContext]] of the receiving actor + */ + private def pipeToSelf( + future: Future[GridAgentMessage], + ctx: ActorContext[GridAgentMessage], + ): Unit = { + ctx.pipeToSelf[GridAgentMessage](future) { + case Success(value) => value + case Failure(exception) => ReceivedFailure(exception) + } + } } diff --git a/src/main/scala/edu/ie3/simona/agent/grid/GridAgent.scala b/src/main/scala/edu/ie3/simona/agent/grid/GridAgent.scala index 2b0ec33a23..8cd2a6b1f6 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/GridAgent.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/GridAgent.scala @@ -6,172 +6,106 @@ package edu.ie3.simona.agent.grid -import edu.ie3.simona.agent.grid.GridAgent.Create +import edu.ie3.simona.actor.SimonaActorNaming +import edu.ie3.simona.agent.EnvironmentRefs import edu.ie3.simona.agent.grid.GridAgentData.{ GridAgentBaseData, + GridAgentConstantData, GridAgentInitData, - GridAgentUninitializedData, } -import edu.ie3.simona.agent.state.AgentState.{Idle, Uninitialized} -import edu.ie3.simona.agent.state.GridAgentState.{Initializing, SimulateGrid} -import edu.ie3.simona.agent.{EnvironmentRefs, SimonaAgent} +import edu.ie3.simona.agent.grid.GridAgentMessage._ +import edu.ie3.simona.agent.participant.ParticipantAgent.ParticipantMessage import edu.ie3.simona.config.SimonaConfig +import edu.ie3.simona.event.ResultEvent import edu.ie3.simona.exceptions.agent.GridAgentInitializationException import edu.ie3.simona.model.grid.GridModel -import edu.ie3.simona.ontology.messages.PowerMessage.RequestGridPowerMessage +import edu.ie3.simona.ontology.messages.Activation import edu.ie3.simona.ontology.messages.SchedulerMessage.{ Completion, ScheduleActivation, } -import edu.ie3.simona.ontology.messages.Activation -import edu.ie3.simona.scheduler.ScheduleLock.ScheduleKey import edu.ie3.simona.util.SimonaConstants.INIT_SIM_TICK import edu.ie3.util.TimeUtil -import org.apache.pekko.actor.{ActorRef, Props, Stash} -import org.apache.pekko.actor.typed.scaladsl.adapter.ClassicActorRefOps +import org.apache.pekko.actor.typed.scaladsl.{Behaviors, StashBuffer} +import org.apache.pekko.actor.typed.{ActorRef, Behavior} import java.time.ZonedDateTime import java.time.temporal.ChronoUnit import java.util.UUID import scala.language.postfixOps -object GridAgent { - def props( +object GridAgent extends DBFSAlgorithm { + + def apply( environmentRefs: EnvironmentRefs, simonaConfig: SimonaConfig, - listener: Iterable[ActorRef], - ): Props = - Props( - new GridAgent( + listener: Iterable[ActorRef[ResultEvent]], + ): Behavior[GridAgentMessage] = Behaviors.withStash(100) { buffer => + Behaviors.setup[GridAgentMessage] { context => + context.messageAdapter(msg => WrappedPowerMessage(msg)) + val activationAdapter: ActorRef[Activation] = + context.messageAdapter[Activation](msg => WrappedActivation(msg)) + + // val initialization + val resolution: Long = simonaConfig.simona.powerflow.resolution.get( + ChronoUnit.SECONDS + ) // this determines the agents regular time bin it wants to be triggered e.g one hour + + val simStartTime: ZonedDateTime = TimeUtil.withDefaults + .toZonedDateTime(simonaConfig.simona.time.startDateTime) + + val agentValues = GridAgentConstantData( environmentRefs, simonaConfig, listener, - ) - ) - - /** GridAgent initialization data can only be constructed once all GridAgent - * actors are created. Thus, we need an extra initialization message. - * @param gridAgentInitData - * The initialization data - */ - final case class Create( - gridAgentInitData: GridAgentInitData, - unlockKey: ScheduleKey, - ) - - /** Trigger used inside of [[edu.ie3.simona.agent.grid.DBFSAlgorithm]] to - * execute a power flow calculation - * - * @param tick - * current tick - */ - final case class DoPowerFlowTrigger(tick: Long, currentSweepNo: Int) - - /** Trigger used inside of [[edu.ie3.simona.agent.grid.DBFSAlgorithm]] to - * activate the superior grid agent to check for deviation after two sweeps - * and see if the power flow converges - * - * @param tick - * current tick - */ - final case class CheckPowerDifferencesTrigger(tick: Long) - - /** Trigger used inside of [[edu.ie3.simona.agent.grid.DBFSAlgorithm]] to - * trigger the [[edu.ie3.simona.agent.grid.GridAgent]] s to prepare - * themselves for a new sweep - * - * @param tick - * current tick - */ - final case class PrepareNextSweepTrigger(tick: Long) - - /** Trigger used inside of [[edu.ie3.simona.agent.grid.DBFSAlgorithm]] to - * indicate that a result has been found and each - * [[edu.ie3.simona.agent.grid.GridAgent]] should do it's cleanup work - * - * @param tick - * current tick - */ - final case class FinishGridSimulationTrigger(tick: Long) -} - -class GridAgent( - val environmentRefs: EnvironmentRefs, - simonaConfig: SimonaConfig, - val listener: Iterable[ActorRef], -) extends SimonaAgent[GridAgentData] - with DBFSAlgorithm - with Stash { - - // val initialization - protected val resolution: Long = simonaConfig.simona.powerflow.resolution.get( - ChronoUnit.SECONDS - ) // this determines the agents regular time bin it wants to be triggered e.g one hour - - protected val simStartTime: ZonedDateTime = TimeUtil.withDefaults - .toZonedDateTime(simonaConfig.simona.time.startDateTime) - - private val gridAgentController = - new GridAgentController( - context, - environmentRefs, - simStartTime, - TimeUtil.withDefaults - .toZonedDateTime(simonaConfig.simona.time.endDateTime), - simonaConfig.simona.runtime.participant, - simonaConfig.simona.output.participant, - resolution, - listener, - log, - ) - - override def postStop(): Unit = { - log.debug("{} shutdown", self) - } - - override def preStart(): Unit = { - log.debug("{} started!", self) - } - - // general agent states - // first fsm state of the agent - startWith(Uninitialized, GridAgentUninitializedData) - - when(Uninitialized) { - case Event( - Create(gridAgentInitData, unlockKey), - _, - ) => - environmentRefs.scheduler ! ScheduleActivation( - self.toTyped, - INIT_SIM_TICK, - Some(unlockKey), + resolution, + simStartTime, + activationAdapter, ) - goto(Initializing) using gridAgentInitData + uninitialized(agentValues, buffer, simonaConfig) + } } - when(Initializing) { - case Event( - Activation(INIT_SIM_TICK), - gridAgentInitData: GridAgentInitData, - ) => + private def uninitialized(implicit + constantData: GridAgentConstantData, + buffer: StashBuffer[GridAgentMessage], + simonaConfig: SimonaConfig, + ): Behavior[GridAgentMessage] = + Behaviors.receiveMessagePartial { + case CreateGridAgent(gridAgentInitData, unlockKey) => + constantData.environmentRefs.scheduler ! ScheduleActivation( + constantData.activationAdapter, + INIT_SIM_TICK, + Some(unlockKey), + ) + initializing(gridAgentInitData, simonaConfig) + } + + private def initializing( + gridAgentInitData: GridAgentInitData, + simonaConfig: SimonaConfig, + )(implicit + constantData: GridAgentConstantData, + buffer: StashBuffer[GridAgentMessage], + ): Behavior[GridAgentMessage] = Behaviors.receivePartial { + case (ctx, WrappedActivation(Activation(INIT_SIM_TICK))) => // fail fast sanity checks - failFast(gridAgentInitData) + failFast(gridAgentInitData, SimonaActorNaming.actorName(ctx.self)) - log.debug( + ctx.log.debug( s"Inferior Subnets: {}; Inferior Subnet Nodes: {}", gridAgentInitData.inferiorGridIds, gridAgentInitData.inferiorGridNodeUuids, ) - log.debug( + ctx.log.debug( s"Superior Subnets: {}; Superior Subnet Nodes: {}", gridAgentInitData.superiorGridIds, gridAgentInitData.superiorGridNodeUuids, ) - log.debug("Received InitializeTrigger.") + ctx.log.debug("Received InitializeTrigger.") // build the assets concurrently val subGridContainer = gridAgentInitData.subGridContainer @@ -179,21 +113,37 @@ class GridAgent( val thermalGridsByBusId = gridAgentInitData.thermalIslandGrids.map { thermalGrid => thermalGrid.bus().getUuid -> thermalGrid }.toMap - log.debug(s"Thermal island grids: ${thermalGridsByBusId.size}") + ctx.log.debug(s"Thermal island grids: ${thermalGridsByBusId.size}") // get the [[GridModel]] val gridModel = GridModel( subGridContainer, refSystem, - TimeUtil.withDefaults - .toZonedDateTime(simonaConfig.simona.time.startDateTime), TimeUtil.withDefaults.toZonedDateTime( - simonaConfig.simona.time.endDateTime + constantData.simonaConfig.simona.time.startDateTime + ), + TimeUtil.withDefaults.toZonedDateTime( + constantData.simonaConfig.simona.time.endDateTime ), + simonaConfig, ) + val gridAgentController = + new GridAgentController( + ctx, + constantData.environmentRefs, + constantData.simStartTime, + TimeUtil.withDefaults + .toZonedDateTime(constantData.simonaConfig.simona.time.endDateTime), + constantData.simonaConfig.simona.runtime.participant, + constantData.simonaConfig.simona.output.participant, + constantData.resolution, + constantData.listener, + ctx.log, + ) + /* Reassure, that there are also calculation models for the given uuids */ - val nodeToAssetAgentsMap: Map[UUID, Set[ActorRef]] = + val nodeToAssetAgentsMap: Map[UUID, Set[ActorRef[ParticipantMessage]]] = gridAgentController .buildSystemParticipants(subGridContainer, thermalGridsByBusId) .map { case (uuid: UUID, actorSet) => @@ -216,53 +166,60 @@ class GridAgent( gridAgentInitData.superiorGridNodeUuids, gridAgentInitData.inferiorGridGates, PowerFlowParams( - simonaConfig.simona.powerflow.maxSweepPowerDeviation, - simonaConfig.simona.powerflow.newtonraphson.epsilon.toVector.sorted, - simonaConfig.simona.powerflow.newtonraphson.iterations, - simonaConfig.simona.powerflow.sweepTimeout, - simonaConfig.simona.powerflow.stopOnFailure, + constantData.simonaConfig.simona.powerflow.maxSweepPowerDeviation, + constantData.simonaConfig.simona.powerflow.newtonraphson.epsilon.toVector.sorted, + constantData.simonaConfig.simona.powerflow.newtonraphson.iterations, + constantData.simonaConfig.simona.powerflow.sweepTimeout, + constantData.simonaConfig.simona.powerflow.stopOnFailure, ), - log, - actorName, + SimonaActorNaming.actorName(ctx.self), ) - log.debug("Je suis initialized") + ctx.log.debug("Je suis initialized") - environmentRefs.scheduler ! Completion( - self.toTyped, - Some(resolution), + constantData.environmentRefs.scheduler ! Completion( + constantData.activationAdapter, + Some(constantData.resolution), ) - goto(Idle) using gridAgentBaseData + idle(gridAgentBaseData) } - when(Idle) { - - // needs to be set here to handle if the messages arrive too early - // before a transition to GridAgentBehaviour took place - case Event(RequestGridPowerMessage(_, _), _: GridAgentBaseData) => - stash() - stay() - - case Event( - Activation(tick), - gridAgentBaseData: GridAgentBaseData, - ) => - unstashAll() - - environmentRefs.scheduler ! Completion( - self.toTyped, - Some(tick), + /** Method that defines the idle [[Behavior]] of the agent. + * + * @param gridAgentBaseData + * state data of the actor + * @param constantData + * immutable [[GridAgent]] values + * @param buffer + * for [[GridAgentMessage]]s + * @return + * a [[Behavior]] + */ + private[grid] def idle( + gridAgentBaseData: GridAgentBaseData + )(implicit + constantData: GridAgentConstantData, + buffer: StashBuffer[GridAgentMessage], + ): Behavior[GridAgentMessage] = Behaviors.receivePartial { + case (_, msg: WrappedPowerMessage) => + // needs to be set here to handle if the messages arrive too early + // before a transition to GridAgentBehaviour took place + buffer.stash(msg) + Behaviors.same + + case (_, WrappedActivation(activation: Activation)) => + constantData.environmentRefs.scheduler ! Completion( + constantData.activationAdapter, + Some(activation.tick), ) - - goto(SimulateGrid) using gridAgentBaseData - + buffer.unstashAll(simulateGrid(gridAgentBaseData, activation.tick)) } - // everything else - whenUnhandled(myUnhandled()) - - private def failFast(gridAgentInitData: GridAgentInitData): Unit = { + private def failFast( + gridAgentInitData: GridAgentInitData, + actorName: String, + ): Unit = { if ( gridAgentInitData.superiorGridGates.isEmpty && gridAgentInitData.inferiorGridGates.isEmpty ) diff --git a/src/main/scala/edu/ie3/simona/agent/grid/GridAgentController.scala b/src/main/scala/edu/ie3/simona/agent/grid/GridAgentController.scala index a65fd6d9cf..a8904c9ef9 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/GridAgentController.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/GridAgentController.scala @@ -11,6 +11,7 @@ import edu.ie3.datamodel.models.input.container.{SubGridContainer, ThermalGrid} import edu.ie3.datamodel.models.input.system._ import edu.ie3.simona.actor.SimonaActorNaming._ import edu.ie3.simona.agent.EnvironmentRefs +import edu.ie3.simona.agent.participant.ParticipantAgent.ParticipantMessage import edu.ie3.simona.agent.participant.data.secondary.SecondaryDataService.{ ActorEvMovementsService, ActorWeatherService, @@ -24,15 +25,22 @@ import edu.ie3.simona.agent.participant.statedata.ParticipantStateData.Participa import edu.ie3.simona.agent.participant.wec.WecAgent import edu.ie3.simona.config.SimonaConfig import edu.ie3.simona.config.SimonaConfig._ +import edu.ie3.simona.event.ResultEvent import edu.ie3.simona.event.notifier.NotifierConfig import edu.ie3.simona.exceptions.agent.GridAgentInitializationException import edu.ie3.simona.ontology.messages.SchedulerMessage.ScheduleActivation import edu.ie3.simona.util.ConfigUtil import edu.ie3.simona.util.ConfigUtil._ import edu.ie3.simona.util.SimonaConstants.INIT_SIM_TICK -import org.apache.pekko.actor.typed.scaladsl.adapter.{ClassicActorRefOps, _} -import org.apache.pekko.actor.{ActorContext, ActorRef} -import org.apache.pekko.event.LoggingAdapter +import org.apache.pekko.actor.typed.ActorRef +import org.apache.pekko.actor.typed.scaladsl.ActorContext +import org.apache.pekko.actor.typed.scaladsl.adapter.{ + ClassicActorRefOps, + TypedActorContextOps, + TypedActorRefOps, +} +import org.apache.pekko.actor.{ActorRef => ClassicRef} +import org.slf4j.Logger import java.time.ZonedDateTime import java.util.UUID @@ -62,20 +70,20 @@ import scala.jdk.CollectionConverters._ * @since 2019-07-18 */ class GridAgentController( - gridAgentContext: ActorContext, + gridAgentContext: ActorContext[_], environmentRefs: EnvironmentRefs, simulationStartDate: ZonedDateTime, simulationEndDate: ZonedDateTime, participantsConfig: SimonaConfig.Simona.Runtime.Participant, outputConfig: SimonaConfig.Simona.Output.Participant, resolution: Long, - listener: Iterable[ActorRef], - log: LoggingAdapter, + listener: Iterable[ActorRef[ResultEvent]], + log: Logger, ) extends LazyLogging { def buildSystemParticipants( subGridContainer: SubGridContainer, thermalIslandGridsByBusId: Map[UUID, ThermalGrid], - ): Map[UUID, Set[ActorRef]] = { + ): Map[UUID, Set[ActorRef[ParticipantMessage]]] = { val systemParticipants = filterSysParts(subGridContainer, environmentRefs) @@ -127,7 +135,7 @@ class GridAgentController( // only include evcs if ev data service is present case evcsInput: EvcsInput if environmentRefs.evDataService.isEmpty => - log.warning( + log.warn( s"Evcs ${evcsInput.getId} has been removed because no ev movements service is present." ) (notProcessedElements, availableSystemParticipants) @@ -137,7 +145,7 @@ class GridAgentController( } if (notProcessedElements.nonEmpty) - log.warning( + log.warn( s"The following elements have been removed, " + s"as the agents are not implemented yet: $notProcessedElements" ) @@ -170,7 +178,7 @@ class GridAgentController( participants: Vector[SystemParticipantInput], thermalIslandGridsByBusId: Map[UUID, ThermalGrid], environmentRefs: EnvironmentRefs, - ): Map[UUID, Set[ActorRef]] = { + ): Map[UUID, Set[ActorRef[ParticipantMessage]]] = { /* Prepare the config util for the participant models, which (possibly) utilizes as map to speed up the initialization * phase */ val participantConfigUtil = @@ -194,7 +202,7 @@ class GridAgentController( // return uuid to actorRef node.getUuid -> actorRef }) - .toSet[(UUID, ActorRef)] + .toSet[(UUID, ActorRef[ParticipantMessage])] .groupMap(entry => entry._1)(entry => entry._2) } @@ -205,7 +213,7 @@ class GridAgentController( participantInputModel: SystemParticipantInput, thermalIslandGridsByBusId: Map[UUID, ThermalGrid], environmentRefs: EnvironmentRefs, - ): ActorRef = participantInputModel match { + ): ActorRef[ParticipantMessage] = participantInputModel match { case input: FixedFeedInInput => buildFixedFeedIn( input, @@ -330,31 +338,33 @@ class GridAgentController( private def buildFixedFeedIn( fixedFeedInInput: FixedFeedInInput, modelConfiguration: FixedFeedInRuntimeConfig, - primaryServiceProxy: ActorRef, + primaryServiceProxy: ClassicRef, simulationStartDate: ZonedDateTime, simulationEndDate: ZonedDateTime, resolution: Long, requestVoltageDeviationThreshold: Double, outputConfig: NotifierConfig, - ): ActorRef = - gridAgentContext.simonaActorOf( - FixedFeedInAgent.props( - environmentRefs.scheduler.toClassic, - ParticipantInitializeStateData( - fixedFeedInInput, - modelConfiguration, - primaryServiceProxy, - Iterable.empty, - simulationStartDate, - simulationEndDate, - resolution, - requestVoltageDeviationThreshold, - outputConfig, + ): ActorRef[ParticipantMessage] = + gridAgentContext.toClassic + .simonaActorOf( + FixedFeedInAgent.props( + environmentRefs.scheduler.toClassic, + ParticipantInitializeStateData( + fixedFeedInInput, + modelConfiguration, + primaryServiceProxy, + Iterable.empty, + simulationStartDate, + simulationEndDate, + resolution, + requestVoltageDeviationThreshold, + outputConfig, + ), + listener.map(_.toClassic), ), - listener, - ), - fixedFeedInInput.getId, - ) + fixedFeedInInput.getId, + ) + .toTyped /** Creates a load agent and determines the needed additional information for * later initialization of the agent. @@ -381,31 +391,33 @@ class GridAgentController( private def buildLoad( loadInput: LoadInput, modelConfiguration: LoadRuntimeConfig, - primaryServiceProxy: ActorRef, + primaryServiceProxy: ClassicRef, simulationStartDate: ZonedDateTime, simulationEndDate: ZonedDateTime, resolution: Long, requestVoltageDeviationThreshold: Double, outputConfig: NotifierConfig, - ): ActorRef = - gridAgentContext.simonaActorOf( - LoadAgent.props( - environmentRefs.scheduler.toClassic, - ParticipantInitializeStateData( - loadInput, - modelConfiguration, - primaryServiceProxy, - Iterable.empty, - simulationStartDate, - simulationEndDate, - resolution, - requestVoltageDeviationThreshold, - outputConfig, + ): ActorRef[ParticipantMessage] = + gridAgentContext.toClassic + .simonaActorOf( + LoadAgent.props( + environmentRefs.scheduler.toClassic, + ParticipantInitializeStateData( + loadInput, + modelConfiguration, + primaryServiceProxy, + Iterable.empty, + simulationStartDate, + simulationEndDate, + resolution, + requestVoltageDeviationThreshold, + outputConfig, + ), + listener.map(_.toClassic), ), - listener, - ), - loadInput.getId, - ) + loadInput.getId, + ) + .toTyped /** Creates a pv agent and determines the needed additional information for * later initialization of the agent. @@ -434,32 +446,34 @@ class GridAgentController( private def buildPv( pvInput: PvInput, modelConfiguration: PvRuntimeConfig, - primaryServiceProxy: ActorRef, - weatherService: ActorRef, + primaryServiceProxy: ClassicRef, + weatherService: ClassicRef, simulationStartDate: ZonedDateTime, simulationEndDate: ZonedDateTime, resolution: Long, requestVoltageDeviationThreshold: Double, outputConfig: NotifierConfig, - ): ActorRef = - gridAgentContext.simonaActorOf( - PvAgent.props( - environmentRefs.scheduler.toClassic, - ParticipantInitializeStateData( - pvInput, - modelConfiguration, - primaryServiceProxy, - Iterable(ActorWeatherService(weatherService)), - simulationStartDate, - simulationEndDate, - resolution, - requestVoltageDeviationThreshold, - outputConfig, + ): ActorRef[ParticipantMessage] = + gridAgentContext.toClassic + .simonaActorOf( + PvAgent.props( + environmentRefs.scheduler.toClassic, + ParticipantInitializeStateData( + pvInput, + modelConfiguration, + primaryServiceProxy, + Iterable(ActorWeatherService(weatherService)), + simulationStartDate, + simulationEndDate, + resolution, + requestVoltageDeviationThreshold, + outputConfig, + ), + listener.map(_.toClassic), ), - listener, - ), - pvInput.getId, - ) + pvInput.getId, + ) + .toTyped /** Creates an Evcs agent and determines the needed additional information for * later initialization of the agent. @@ -488,35 +502,37 @@ class GridAgentController( private def buildEvcs( evcsInput: EvcsInput, modelConfiguration: EvcsRuntimeConfig, - primaryServiceProxy: ActorRef, - evMovementsService: ActorRef, + primaryServiceProxy: ClassicRef, + evMovementsService: ClassicRef, simulationStartDate: ZonedDateTime, simulationEndDate: ZonedDateTime, resolution: Long, requestVoltageDeviationThreshold: Double, outputConfig: NotifierConfig, - ): ActorRef = - gridAgentContext.simonaActorOf( - EvcsAgent.props( - environmentRefs.scheduler.toClassic, - ParticipantInitializeStateData( - evcsInput, - modelConfiguration, - primaryServiceProxy, - Iterable( - ActorEvMovementsService( - evMovementsService - ) + ): ActorRef[ParticipantMessage] = + gridAgentContext.toClassic + .simonaActorOf( + EvcsAgent.props( + environmentRefs.scheduler.toClassic, + ParticipantInitializeStateData( + evcsInput, + modelConfiguration, + primaryServiceProxy, + Iterable( + ActorEvMovementsService( + evMovementsService + ) + ), + simulationStartDate, + simulationEndDate, + resolution, + requestVoltageDeviationThreshold, + outputConfig, ), - simulationStartDate, - simulationEndDate, - resolution, - requestVoltageDeviationThreshold, - outputConfig, - ), - listener, + listener.map(_.toClassic), + ) ) - ) + .toTyped /** Builds an [[HpAgent]] from given input * @param hpInput @@ -540,30 +556,32 @@ class GridAgentController( hpInput: HpInput, thermalGrid: ThermalGrid, modelConfiguration: HpRuntimeConfig, - primaryServiceProxy: ActorRef, - weatherService: ActorRef, + primaryServiceProxy: ClassicRef, + weatherService: ClassicRef, requestVoltageDeviationThreshold: Double, outputConfig: NotifierConfig, - ): ActorRef = - gridAgentContext.simonaActorOf( - HpAgent.props( - environmentRefs.scheduler.toClassic, - ParticipantInitializeStateData( - hpInput, - thermalGrid, - modelConfiguration, - primaryServiceProxy, - Iterable(ActorWeatherService(weatherService)), - simulationStartDate, - simulationEndDate, - resolution, - requestVoltageDeviationThreshold, - outputConfig, + ): ActorRef[ParticipantMessage] = + gridAgentContext.toClassic + .simonaActorOf( + HpAgent.props( + environmentRefs.scheduler.toClassic, + ParticipantInitializeStateData( + hpInput, + thermalGrid, + modelConfiguration, + primaryServiceProxy, + Iterable(ActorWeatherService(weatherService)), + simulationStartDate, + simulationEndDate, + resolution, + requestVoltageDeviationThreshold, + outputConfig, + ), + listener.map(_.toClassic), ), - listener, - ), - hpInput.getId, - ) + hpInput.getId, + ) + .toTyped /** Creates a pv agent and determines the needed additional information for * later initialization of the agent. @@ -592,32 +610,34 @@ class GridAgentController( private def buildWec( wecInput: WecInput, modelConfiguration: WecRuntimeConfig, - primaryServiceProxy: ActorRef, - weatherService: ActorRef, + primaryServiceProxy: ClassicRef, + weatherService: ClassicRef, simulationStartDate: ZonedDateTime, simulationEndDate: ZonedDateTime, resolution: Long, requestVoltageDeviationThreshold: Double, outputConfig: NotifierConfig, - ): ActorRef = - gridAgentContext.simonaActorOf( - WecAgent.props( - environmentRefs.scheduler.toClassic, - ParticipantInitializeStateData( - wecInput, - modelConfiguration, - primaryServiceProxy, - Iterable(ActorWeatherService(weatherService)), - simulationStartDate, - simulationEndDate, - resolution, - requestVoltageDeviationThreshold, - outputConfig, + ): ActorRef[ParticipantMessage] = + gridAgentContext.toClassic + .simonaActorOf( + WecAgent.props( + environmentRefs.scheduler.toClassic, + ParticipantInitializeStateData( + wecInput, + modelConfiguration, + primaryServiceProxy, + Iterable(ActorWeatherService(weatherService)), + simulationStartDate, + simulationEndDate, + resolution, + requestVoltageDeviationThreshold, + outputConfig, + ), + listener.map(_.toClassic), ), - listener, - ), - wecInput.getId, - ) + wecInput.getId, + ) + .toTyped /** Introduces the given agent to scheduler * @@ -625,11 +645,11 @@ class GridAgentController( * Reference to the actor to add to the environment */ private def introduceAgentToEnvironment( - actorRef: ActorRef + actorRef: ActorRef[ParticipantMessage] ): Unit = { gridAgentContext.watch(actorRef) environmentRefs.scheduler ! ScheduleActivation( - actorRef.toTyped, + actorRef.toClassic.toTyped, INIT_SIM_TICK, ) } 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 03ec0fd641..8f014f40e2 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/GridAgentData.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/GridAgentData.scala @@ -6,25 +6,30 @@ package edu.ie3.simona.agent.grid -import org.apache.pekko.actor.ActorRef -import org.apache.pekko.event.LoggingAdapter import edu.ie3.datamodel.graph.SubGridGate import edu.ie3.datamodel.models.input.container.{SubGridContainer, ThermalGrid} import edu.ie3.powerflow.model.PowerFlowResult import edu.ie3.powerflow.model.PowerFlowResult.SuccessFullPowerFlowResult.ValidNewtonRaphsonPFResult +import edu.ie3.simona.agent.EnvironmentRefs import edu.ie3.simona.agent.grid.ReceivedValues.{ ReceivedPowerValues, ReceivedSlackVoltageValues, } import edu.ie3.simona.agent.grid.ReceivedValuesStore.NodeToReceivedPower +import edu.ie3.simona.agent.participant.ParticipantAgent.ParticipantMessage +import edu.ie3.simona.config.SimonaConfig +import edu.ie3.simona.event.ResultEvent import edu.ie3.simona.model.grid.{GridModel, RefSystem} +import edu.ie3.simona.ontology.messages.Activation import edu.ie3.simona.ontology.messages.PowerMessage.{ FailedPowerFlow, PowerResponseMessage, ProvideGridPowerMessage, ProvidePowerMessage, } +import org.apache.pekko.actor.typed.ActorRef +import java.time.ZonedDateTime import java.util.UUID sealed trait GridAgentData @@ -33,14 +38,37 @@ sealed trait GridAgentData */ object GridAgentData { - /** Initial state data of the [[GridAgent]] + /** Class holding some [[GridAgent]] values that are immutable. + * @param environmentRefs + * environment actor refs + * @param simonaConfig + * config + * @param listener + * listeners + * @param resolution + * of the simulation + * @param simStartTime + * start time of the simulation + * @param activationAdapter + * adapter for [[Activation]] */ - final case object GridAgentUninitializedData extends GridAgentData + final case class GridAgentConstantData private ( + environmentRefs: EnvironmentRefs, + simonaConfig: SimonaConfig, + listener: Iterable[ActorRef[ResultEvent]], + resolution: Long, + simStartTime: ZonedDateTime, + activationAdapter: ActorRef[Activation], + ) { + def notifyListeners(event: ResultEvent): Unit = { + listener.foreach(listener => listener ! event) + } + } /** Data that is send to the [[GridAgent]] directly after startup. It contains * the main information for initialization. This data should include all * [[GridAgent]] individual data, for data that is the same for all - * [[GridAgent]] s please use [[GridAgent.props()]] + * [[GridAgent]] s please use [[GridAgent.apply()]] * * @param subGridContainer * raw grid information in the input data format @@ -54,7 +82,7 @@ object GridAgentData { final case class GridAgentInitData( subGridContainer: SubGridContainer, thermalIslandGrids: Seq[ThermalGrid], - subGridGateToActorRef: Map[SubGridGate, ActorRef], + subGridGateToActorRef: Map[SubGridGate, ActorRef[GridAgentMessage]], refSystem: RefSystem, ) extends GridAgentData with GridAgentDataHelper { @@ -101,12 +129,11 @@ object GridAgentData { def apply( gridModel: GridModel, - subgridGateToActorRef: Map[SubGridGate, ActorRef], - nodeToAssetAgents: Map[UUID, Set[ActorRef]], + subgridGateToActorRef: Map[SubGridGate, ActorRef[GridAgentMessage]], + nodeToAssetAgents: Map[UUID, Set[ActorRef[ParticipantMessage]]], superiorGridNodeUuids: Vector[UUID], inferiorGridGates: Vector[SubGridGate], powerFlowParams: PowerFlowParams, - log: LoggingAdapter, actorName: String, ): GridAgentBaseData = { @@ -129,7 +156,6 @@ object GridAgentData { superiorGridNodeUuids, ), sweepValueStores, - log, actorName, ) } @@ -194,7 +220,6 @@ object GridAgentData { currentSweepNo: Int, receivedValueStore: ReceivedValuesStore, sweepValueStores: Map[Int, SweepValueStore], - log: LoggingAdapter, actorName: String, ) extends GridAgentData with GridAgentDataHelper { @@ -215,14 +240,7 @@ object GridAgentData { val slackVoltageValuesReady = receivedValueStore.nodeToReceivedSlackVoltage.values .forall(_.isDefined) - log.debug( - "slackMap: {}", - receivedValueStore.nodeToReceivedSlackVoltage, - ) - log.debug( - "powerMap: {}", - receivedValueStore.nodeToReceivedPower, - ) + assetAndGridPowerValuesReady & slackVoltageValuesReady } @@ -301,7 +319,7 @@ object GridAgentData { private def updateNodalReceivedPower( powerResponse: PowerResponseMessage, nodeToReceived: NodeToReceivedPower, - senderRef: ActorRef, + senderRef: ActorRef[_], replace: Boolean, ): NodeToReceivedPower = { // extract the nodeUuid that corresponds to the sender's actorRef and check if we expect a message from the sender @@ -359,7 +377,7 @@ object GridAgentData { */ private def getNodeUuidForSender( nodeToReceivedPower: NodeToReceivedPower, - senderRef: ActorRef, + senderRef: ActorRef[_], replace: Boolean, ): Option[UUID] = nodeToReceivedPower diff --git a/src/main/scala/edu/ie3/simona/agent/grid/GridAgentMessage.scala b/src/main/scala/edu/ie3/simona/agent/grid/GridAgentMessage.scala new file mode 100644 index 0000000000..a40d5bb9e8 --- /dev/null +++ b/src/main/scala/edu/ie3/simona/agent/grid/GridAgentMessage.scala @@ -0,0 +1,87 @@ +/* + * © 2024. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.agent.grid + +import edu.ie3.simona.agent.grid.GridAgentData.GridAgentInitData +import edu.ie3.simona.ontology.messages.{Activation, PowerMessage} +import edu.ie3.simona.scheduler.ScheduleLock.ScheduleKey + +/** Trait for [[GridAgent]] messages. + */ +sealed trait GridAgentMessage + +/** Defines all messages that can be received by a [[GridAgent]] without the + * need for an adapter. + */ +object GridAgentMessage { + + /** Necessary because we want to extend [[GridAgentMessage]] in other classes, + * but we do want to keep [[GridAgentMessage]] sealed. + */ + private[grid] trait InternalMessage extends GridAgentMessage + + /** GridAgent initialization data can only be constructed once all GridAgent + * actors are created. Thus, we need an extra initialization message. + * + * @param gridAgentInitData + * The initialization data + */ + final case class CreateGridAgent( + gridAgentInitData: GridAgentInitData, + unlockKey: ScheduleKey, + ) extends GridAgentMessage + + /** Trigger used inside of [[edu.ie3.simona.agent.grid.DBFSAlgorithm]] to + * execute a power flow calculation + * + * @param tick + * current tick + */ + final case class DoPowerFlowTrigger(tick: Long, currentSweepNo: Int) + extends GridAgentMessage + + /** Trigger used inside of [[edu.ie3.simona.agent.grid.DBFSAlgorithm]] to + * activate the superior grid agent to check for deviation after two sweeps + * and see if the power flow converges + * + * @param tick + * current tick + */ + final case class CheckPowerDifferencesTrigger(tick: Long) + extends GridAgentMessage + + /** Trigger used inside of [[edu.ie3.simona.agent.grid.DBFSAlgorithm]] to + * trigger the [[edu.ie3.simona.agent.grid.GridAgent]] s to prepare + * themselves for a new sweep + * + * @param tick + * current tick + */ + final case class PrepareNextSweepTrigger(tick: Long) extends GridAgentMessage + + /** Trigger used inside of [[edu.ie3.simona.agent.grid.DBFSAlgorithm]] to + * indicate that a result has been found and each + * [[edu.ie3.simona.agent.grid.GridAgent]] should do it's cleanup work + * + * @param tick + * current tick + */ + final case class FinishGridSimulationTrigger(tick: Long) + extends GridAgentMessage + + /** Wrapper for activation values + * + * @param activation + * the tick + */ + final case class WrappedActivation(activation: Activation) + extends GridAgentMessage + + final case class WrappedPowerMessage(msg: PowerMessage) + extends GridAgentMessage + +} 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 622cf4f614..76f5ff1ed8 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/GridEnvironment.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/GridEnvironment.scala @@ -6,11 +6,12 @@ package edu.ie3.simona.agent.grid -import java.util.UUID - -import org.apache.pekko.actor.ActorRef import edu.ie3.datamodel.graph.SubGridGate +import edu.ie3.simona.agent.participant.ParticipantAgent.ParticipantMessage import edu.ie3.simona.model.grid.GridModel +import org.apache.pekko.actor.typed.ActorRef + +import java.util.UUID /** Wrapper class containing all information on the grid environment a * [[GridAgent]] has access to @@ -25,6 +26,6 @@ import edu.ie3.simona.model.grid.GridModel */ final case class GridEnvironment( gridModel: GridModel, - subgridGateToActorRef: Map[SubGridGate, ActorRef], - nodeToAssetAgents: Map[UUID, Set[ActorRef]], + subgridGateToActorRef: Map[SubGridGate, ActorRef[GridAgentMessage]], + nodeToAssetAgents: Map[UUID, Set[ActorRef[ParticipantMessage]]], ) 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 1118041f93..c872cc8e54 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/GridResultsSupport.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/GridResultsSupport.scala @@ -6,7 +6,6 @@ package edu.ie3.simona.agent.grid -import org.apache.pekko.event.LoggingAdapter import breeze.math.Complex import edu.ie3.datamodel.models.input.connector.ConnectorPort import edu.ie3.datamodel.models.result.NodeResult @@ -29,6 +28,7 @@ import edu.ie3.simona.model.grid.Transformer3wPowerFlowCase.{ import edu.ie3.simona.model.grid._ import edu.ie3.util.quantities.PowerSystemUnits import edu.ie3.util.scala.quantities.QuantityUtil +import org.slf4j.Logger import squants.space.Degrees import squants.{Amperes, Angle, ElectricCurrent} import tech.units.indriya.quantity.Quantities @@ -43,8 +43,6 @@ import scala.math._ */ private[grid] trait GridResultsSupport { - protected val log: LoggingAdapter - /** Creates a tuple as [[PowerFlowResultEvent]] s entities based on the * provided grid data * @@ -61,7 +59,7 @@ private[grid] trait GridResultsSupport { def createResultModels( grid: GridModel, sweepValueStore: SweepValueStore, - )(implicit timestamp: ZonedDateTime): PowerFlowResultEvent = { + )(implicit timestamp: ZonedDateTime, log: Logger): PowerFlowResultEvent = { // no sanity check for duplicated uuid result data as we expect valid data at this point implicit val sweepValueStoreData: Map[UUID, SweepValueStoreData] = sweepValueStore.sweepData @@ -112,6 +110,7 @@ private[grid] trait GridResultsSupport { sweepValueStoreData: Map[UUID, SweepValueStoreData], iNominal: squants.ElectricCurrent, timestamp: ZonedDateTime, + log: Logger, ): Set[LineResult] = { lines.flatMap(lineModel => { sweepValueStoreData @@ -128,7 +127,7 @@ private[grid] trait GridResultsSupport { ) ) case None => - log.warning( + log.warn( "Cannot find power flow result data for line {} with nodeA {} and nodeB {}", lineModel.uuid, lineModel.nodeAUuid, @@ -159,6 +158,7 @@ private[grid] trait GridResultsSupport { sweepValueStoreData: Map[UUID, SweepValueStoreData], iNominal: ElectricCurrent, timestamp: ZonedDateTime, + log: Logger, ): Set[Transformer2WResult] = { transformers.flatMap(trafo2w => { sweepValueStoreData @@ -175,7 +175,7 @@ private[grid] trait GridResultsSupport { ) ) case None => - log.warning( + log.warn( "Cannot find power flow result data for transformer2w {} with hvNode {} and lvNode {}", trafo2w.uuid, trafo2w.hvNodeUuid, @@ -206,6 +206,7 @@ private[grid] trait GridResultsSupport { sweepValueStoreData: Map[UUID, SweepValueStoreData], iNominal: ElectricCurrent, timestamp: ZonedDateTime, + log: Logger, ): Set[PartialTransformer3wResult] = transformers3w.flatMap { trafo3w => { (trafo3w.powerFlowCase match { @@ -233,7 +234,7 @@ private[grid] trait GridResultsSupport { ) ) case None => - log.warning( + log.warn( s"Cannot find power flow result data for transformer3w {} with nodeHv {}, nodeMv {}, nodeLv {} and internalNode ${trafo3w.nodeInternalUuid}", trafo3w.uuid, trafo3w.hvNodeUuid, 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 f06ab993c7..5d34d06bd4 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 org.apache.pekko.event.LoggingAdapter import breeze.math.Complex import edu.ie3.powerflow.NewtonRaphsonPF import edu.ie3.powerflow.model.NodeData.{PresetData, StateData} @@ -18,8 +17,9 @@ 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 -import edu.ie3.simona.ontology.messages.VoltageMessage.ProvideSlackVoltageMessage.ExchangeVoltage +import VoltageMessage.ProvideSlackVoltageMessage.ExchangeVoltage import edu.ie3.util.scala.quantities.Kilovars +import org.slf4j.Logger import squants.electro.ElectricPotential import squants.energy.Kilowatts @@ -32,8 +32,6 @@ import scala.util.{Failure, Success, Try} */ trait PowerFlowSupport { - protected val log: LoggingAdapter - /** Composes the current operation point needed by * [[edu.ie3.powerflow.NewtonRaphsonPF.calculate()]] * @@ -561,7 +559,7 @@ trait PowerFlowSupport { maxIterations: Int, operatingPoint: Array[PresetData], slackVoltages: WithForcedStartVoltages, - )(epsilons: Vector[Double]): PowerFlowResult = { + )(epsilons: Vector[Double])(implicit log: Logger): PowerFlowResult = { epsilons.headOption match { case Some(epsilon) => val admittanceMatrix = 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 53f04d5af8..c1accbb471 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/ReceivedValues.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/ReceivedValues.scala @@ -6,22 +6,30 @@ package edu.ie3.simona.agent.grid -import org.apache.pekko.actor.ActorRef import edu.ie3.simona.ontology.messages.PowerMessage.PowerResponseMessage -import edu.ie3.simona.ontology.messages.VoltageMessage.ProvideSlackVoltageMessage +import VoltageMessage.ProvideSlackVoltageMessage +import edu.ie3.simona.agent.grid.GridAgentMessage.InternalMessage +import org.apache.pekko.actor.typed.ActorRef /** Serves as a wrapper class that allows for matches against received values in * [[DBFSAlgorithm]] */ -sealed trait ReceivedValues +sealed trait ReceivedValues extends InternalMessage object ReceivedValues { - type ActorPowerRequestResponse = (ActorRef, PowerResponseMessage) - type ActorSlackVoltageRequestResponse = (ActorRef, ProvideSlackVoltageMessage) + private type ParticipantPowerRequestResponse = + ( + ActorRef[_], + PowerResponseMessage, + ) // necessary, because participants are still classic actors + private type GridPowerRequestResponse = + (ActorRef[GridAgentMessage], PowerResponseMessage) + private type ActorSlackVoltageRequestResponse = + (ActorRef[GridAgentMessage], ProvideSlackVoltageMessage) sealed trait ReceivedPowerValues extends ReceivedValues { - def values: Vector[ActorPowerRequestResponse] + def values: Vector[(ActorRef[_], PowerResponseMessage)] } /** Wrapper for received asset power values (p, q) @@ -30,7 +38,7 @@ object ReceivedValues { * the asset power values and their senders */ final case class ReceivedAssetPowerValues( - values: Vector[ActorPowerRequestResponse] + values: Vector[ParticipantPowerRequestResponse] ) extends ReceivedPowerValues /** Wrapper for received grid power values (p, q) @@ -39,7 +47,7 @@ object ReceivedValues { * the grid power values and their senders */ final case class ReceivedGridPowerValues( - values: Vector[ActorPowerRequestResponse] + values: Vector[GridPowerRequestResponse] ) extends ReceivedPowerValues /** Wrapper for received slack voltage values (v) @@ -51,4 +59,11 @@ object ReceivedValues { values: Vector[ActorSlackVoltageRequestResponse] ) extends ReceivedValues + /** Wrapper for received exception. + * @param exception + * that was received + */ + final case class ReceivedFailure( + exception: Throwable + ) extends ReceivedValues } 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 e74f0f82df..a576045703 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/ReceivedValuesStore.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/ReceivedValuesStore.scala @@ -6,17 +6,20 @@ package edu.ie3.simona.agent.grid -import org.apache.pekko.actor.ActorRef import edu.ie3.datamodel.graph.SubGridGate import edu.ie3.simona.agent.grid.ReceivedValuesStore.{ NodeToReceivedPower, NodeToReceivedSlackVoltage, } +import edu.ie3.simona.agent.participant.ParticipantAgent.ParticipantMessage import edu.ie3.simona.ontology.messages.PowerMessage.{ PowerResponseMessage, ProvidePowerMessage, } -import edu.ie3.simona.ontology.messages.VoltageMessage.ProvideSlackVoltageMessage.ExchangeVoltage +import VoltageMessage.ProvideSlackVoltageMessage.ExchangeVoltage +import org.apache.pekko.actor.typed.ActorRef +import org.apache.pekko.actor.typed.scaladsl.adapter.ClassicActorRefOps +import org.apache.pekko.actor.{ActorRef => classicRef} import java.util.UUID @@ -47,7 +50,7 @@ final case class ReceivedValuesStore private ( object ReceivedValuesStore { type NodeToReceivedPower = - Map[UUID, Map[ActorRef, Option[PowerResponseMessage]]] + Map[UUID, Map[ActorRef[_], Option[PowerResponseMessage]]] type NodeToReceivedSlackVoltage = Map[UUID, Option[ExchangeVoltage]] @@ -68,8 +71,10 @@ object ReceivedValuesStore { * `empty` [[ReceivedValuesStore]] with pre-initialized options as `None` */ def empty( - nodeToAssetAgents: Map[UUID, Set[ActorRef]], - inferiorSubGridGateToActorRef: Map[SubGridGate, ActorRef], + nodeToAssetAgents: Map[UUID, Set[ActorRef[ParticipantMessage]]], + inferiorSubGridGateToActorRef: Map[SubGridGate, ActorRef[ + GridAgentMessage + ]], superiorGridNodeUuids: Vector[UUID], ): ReceivedValuesStore = { val (nodeToReceivedPower, nodeToReceivedSlackVoltage) = @@ -94,12 +99,14 @@ object ReceivedValuesStore { * `empty` [[NodeToReceivedPower]] with pre-initialized options as `None` */ private def buildEmptyNodeToReceivedPowerMap( - nodeToAssetAgents: Map[UUID, Set[ActorRef]], - inferiorSubGridGateToActorRef: Map[SubGridGate, ActorRef], + nodeToAssetAgents: Map[UUID, Set[ActorRef[ParticipantMessage]]], + inferiorSubGridGateToActorRef: Map[SubGridGate, ActorRef[ + GridAgentMessage + ]], ): NodeToReceivedPower = { /* Collect everything, that I expect from my asset agents */ val assetsToReceivedPower: NodeToReceivedPower = nodeToAssetAgents.collect { - case (uuid: UUID, actorRefs: Set[ActorRef]) => + case (uuid: UUID, actorRefs: Set[ActorRef[ParticipantMessage]]) => (uuid, actorRefs.map(actorRef => actorRef -> None).toMap) } @@ -118,7 +125,7 @@ object ReceivedValuesStore { val actorRefToMessage = subordinateToReceivedPower .getOrElse( couplingNodeUuid, - Map.empty[ActorRef, Option[ProvidePowerMessage]], + Map.empty[ActorRef[_], Option[ProvidePowerMessage]], ) + (inferiorSubGridRef -> None) /* Update the existing map */ @@ -156,8 +163,10 @@ object ReceivedValuesStore { * `empty` [[NodeToReceivedSlackVoltage]] and [[NodeToReceivedPower]] */ private def buildEmptyReceiveMaps( - nodeToAssetAgents: Map[UUID, Set[ActorRef]], - inferiorSubGridGateToActorRef: Map[SubGridGate, ActorRef], + nodeToAssetAgents: Map[UUID, Set[ActorRef[ParticipantMessage]]], + inferiorSubGridGateToActorRef: Map[SubGridGate, ActorRef[ + GridAgentMessage + ]], superiorGridNodeUuids: Vector[UUID], ): (NodeToReceivedPower, NodeToReceivedSlackVoltage) = { ( diff --git a/src/main/scala/edu/ie3/simona/ontology/messages/VoltageMessage.scala b/src/main/scala/edu/ie3/simona/agent/grid/VoltageMessage.scala similarity index 83% rename from src/main/scala/edu/ie3/simona/ontology/messages/VoltageMessage.scala rename to src/main/scala/edu/ie3/simona/agent/grid/VoltageMessage.scala index eea6675941..41c311f61d 100644 --- a/src/main/scala/edu/ie3/simona/ontology/messages/VoltageMessage.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/VoltageMessage.scala @@ -4,14 +4,16 @@ * Research group Distribution grid planning and operation */ -package edu.ie3.simona.ontology.messages +package edu.ie3.simona.agent.grid -import edu.ie3.simona.ontology.messages.VoltageMessage.ProvideSlackVoltageMessage.ExchangeVoltage +import edu.ie3.simona.agent.grid.GridAgentMessage.InternalMessage +import edu.ie3.simona.agent.grid.VoltageMessage.ProvideSlackVoltageMessage.ExchangeVoltage +import org.apache.pekko.actor.typed.ActorRef +import squants.electro.ElectricPotential import java.util.UUID -import squants.electro.ElectricPotential -sealed trait VoltageMessage +sealed trait VoltageMessage extends InternalMessage /** Message that is send between [[edu.ie3.simona.agent.grid.GridAgent]] s to * provide voltage information for nodes @@ -28,6 +30,7 @@ object VoltageMessage { final case class RequestSlackVoltageMessage( currentSweepNo: Int, nodeUuids: Seq[UUID], + sender: ActorRef[GridAgentMessage], ) extends VoltageMessage /** Provide complex voltage at the nodes that the sender's sub grid shares diff --git a/src/main/scala/edu/ie3/simona/agent/participant/ParticipantAgent.scala b/src/main/scala/edu/ie3/simona/agent/participant/ParticipantAgent.scala index 3eae4e4a55..a01c5677a1 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant/ParticipantAgent.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant/ParticipantAgent.scala @@ -7,9 +7,8 @@ package edu.ie3.simona.agent.participant import edu.ie3.datamodel.models.input.system.SystemParticipantInput -import edu.ie3.simona.agent.{SimonaAgent, ValueStore} -import edu.ie3.simona.agent.grid.GridAgent.FinishGridSimulationTrigger import edu.ie3.simona.agent.participant.ParticipantAgent.{ + FinishParticipantSimulation, StartCalculationTrigger, getAndCheckNodalVoltage, } @@ -33,6 +32,7 @@ import edu.ie3.simona.agent.state.ParticipantAgentState.{ Calculate, HandleInformation, } +import edu.ie3.simona.agent.{SimonaAgent, ValueStore} import edu.ie3.simona.config.SimonaConfig import edu.ie3.simona.event.notifier.NotifierConfig import edu.ie3.simona.exceptions.agent.InconsistentStateException @@ -44,13 +44,13 @@ import edu.ie3.simona.model.participant.{ SystemParticipant, } import edu.ie3.simona.ontology.messages.Activation +import edu.ie3.simona.ontology.messages.PowerMessage.RequestAssetPowerMessage +import edu.ie3.simona.ontology.messages.SchedulerMessage.ScheduleActivation import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage.{ FlexResponse, IssueFlexControl, RequestFlexOptions, } -import edu.ie3.simona.ontology.messages.PowerMessage.RequestAssetPowerMessage -import edu.ie3.simona.ontology.messages.SchedulerMessage.ScheduleActivation import edu.ie3.simona.ontology.messages.services.ServiceMessage.RegistrationResponseMessage.RegistrationSuccessfulMessage import edu.ie3.simona.ontology.messages.services.ServiceMessage.{ PrimaryServiceRegistrationMessage, @@ -215,7 +215,7 @@ abstract class ParticipantAgent[ ) case Event( - FinishGridSimulationTrigger(tick), + FinishParticipantSimulation(tick), baseStateData: BaseStateData[PD], ) => // clean up agent result value store @@ -870,6 +870,11 @@ abstract class ParticipantAgent[ object ParticipantAgent { + trait ParticipantMessage + + final case class FinishParticipantSimulation(tick: Long) + extends ParticipantMessage + final case class StartCalculationTrigger(tick: Long) /** Verifies that a nodal voltage value has been provided in the model diff --git a/src/main/scala/edu/ie3/simona/agent/state/GridAgentState.scala b/src/main/scala/edu/ie3/simona/agent/state/GridAgentState.scala deleted file mode 100644 index f602b4dabe..0000000000 --- a/src/main/scala/edu/ie3/simona/agent/state/GridAgentState.scala +++ /dev/null @@ -1,21 +0,0 @@ -/* - * © 2020. TU Dortmund University, - * Institute of Energy Systems, Energy Efficiency and Energy Economics, - * Research group Distribution grid planning and operation - */ - -package edu.ie3.simona.agent.state - -sealed trait GridAgentState extends AgentState - -object GridAgentState { - - case object Initializing extends GridAgentState - - case object SimulateGrid extends GridAgentState - - case object CheckPowerDifferences extends GridAgentState - - case object HandlePowerFlowCalculations extends GridAgentState - -} diff --git a/src/main/scala/edu/ie3/simona/config/ConfigFailFast.scala b/src/main/scala/edu/ie3/simona/config/ConfigFailFast.scala index e2ca6a0cb6..09f2bf62ae 100644 --- a/src/main/scala/edu/ie3/simona/config/ConfigFailFast.scala +++ b/src/main/scala/edu/ie3/simona/config/ConfigFailFast.scala @@ -13,6 +13,8 @@ import edu.ie3.simona.config.SimonaConfig.{ BaseOutputConfig, RefSystemConfig, ResultKafkaParams, + Simona, + TransformerControlGroup, } import edu.ie3.simona.exceptions.InvalidConfigParameterException import edu.ie3.simona.io.result.ResultSinkType @@ -143,6 +145,9 @@ case object ConfigFailFast extends LazyLogging { /* Check power flow resolution configuration */ checkPowerFlowResolutionConfiguration(simonaConfig.simona.powerflow) + + /* Check control scheme definitions */ + simonaConfig.simona.control.foreach(checkControlSchemes) } /** Checks for valid sink configuration @@ -583,6 +588,61 @@ case object ConfigFailFast extends LazyLogging { } } + /** Check the validity of control scheme definitions + * + * @param control + * Control scheme definitions + */ + private def checkControlSchemes(control: Simona.Control): Unit = { + control.transformer.foreach(checkTransformerControl) + } + + /** Check the suitability of transformer control group definition. + * + * One important check cannot be performed at this place, as input data is + * not available, yet: Do the measurements belong to a region, that can be + * influenced by the transformer? This is partly addressed in + * [[edu.ie3.simona.agent.grid.GridAgentFailFast]] + * + * @param transformerControlGroup + * Transformer control group definition + */ + private def checkTransformerControl( + transformerControlGroup: TransformerControlGroup + ): Unit = { + val lowerBoundary = 0.8 + val upperBoundary = 1.2 + transformerControlGroup match { + case TransformerControlGroup(measurements, transformers, vMax, vMin) => + if (measurements.isEmpty) + throw new InvalidConfigParameterException( + s"A transformer control group (${transformerControlGroup.toString}) cannot have no measurements assigned." + ) + if (transformers.isEmpty) + throw new InvalidConfigParameterException( + s"A transformer control group (${transformerControlGroup.toString}) cannot have no transformers assigned." + ) + if (vMin < 0) + throw new InvalidConfigParameterException( + "The minimum permissible voltage magnitude of a transformer control group has to be positive." + ) + if (vMax < vMin) + throw new InvalidConfigParameterException( + s"The minimum permissible voltage magnitude of a transformer control group (${transformerControlGroup.toString}) must be smaller than the maximum permissible voltage magnitude." + ) + if (vMin < lowerBoundary) + throw new InvalidConfigParameterException( + s"A control group (${transformerControlGroup.toString}) which control boundaries exceed the limit of +- 20% of nominal voltage! This may be caused " + + s"by invalid parametrization of one control groups where vMin is lower than the lower boundary (0.8 of nominal Voltage)!" + ) + if (vMax > upperBoundary) + throw new InvalidConfigParameterException( + s"A control group (${transformerControlGroup.toString}) which control boundaries exceed the limit of +- 20% of nominal voltage! This may be caused " + + s"by invalid parametrization of one control groups where vMax is higher than the upper boundary (1.2 of nominal Voltage)!" + ) + } + } + /** Check the default config * * @param config diff --git a/src/main/scala/edu/ie3/simona/config/SimonaConfig.scala b/src/main/scala/edu/ie3/simona/config/SimonaConfig.scala index e437569147..f122d05d39 100644 --- a/src/main/scala/edu/ie3/simona/config/SimonaConfig.scala +++ b/src/main/scala/edu/ie3/simona/config/SimonaConfig.scala @@ -802,6 +802,45 @@ object SimonaConfig { } + final case class TransformerControlGroup( + measurements: scala.List[java.lang.String], + transformers: scala.List[java.lang.String], + vMax: scala.Double, + vMin: scala.Double, + ) + object TransformerControlGroup { + def apply( + c: com.typesafe.config.Config, + parentPath: java.lang.String, + $tsCfgValidator: $TsCfgValidator, + ): SimonaConfig.TransformerControlGroup = { + SimonaConfig.TransformerControlGroup( + measurements = + $_L$_str(c.getList("measurements"), parentPath, $tsCfgValidator), + transformers = + $_L$_str(c.getList("transformers"), parentPath, $tsCfgValidator), + vMax = $_reqDbl(parentPath, c, "vMax", $tsCfgValidator), + vMin = $_reqDbl(parentPath, c, "vMin", $tsCfgValidator), + ) + } + private def $_reqDbl( + parentPath: java.lang.String, + c: com.typesafe.config.Config, + path: java.lang.String, + $tsCfgValidator: $TsCfgValidator, + ): scala.Double = { + if (c == null) 0 + else + try c.getDouble(path) + catch { + case e: com.typesafe.config.ConfigException => + $tsCfgValidator.addBadPath(parentPath + path, e) + 0 + } + } + + } + final case class VoltLvlConfig( id: java.lang.String, vNom: java.lang.String, @@ -896,6 +935,7 @@ object SimonaConfig { } final case class Simona( + control: scala.Option[SimonaConfig.Simona.Control], event: SimonaConfig.Simona.Event, gridConfig: SimonaConfig.Simona.GridConfig, input: SimonaConfig.Simona.Input, @@ -906,6 +946,41 @@ object SimonaConfig { time: SimonaConfig.Simona.Time, ) object Simona { + final case class Control( + transformer: scala.List[SimonaConfig.TransformerControlGroup] + ) + object Control { + def apply( + c: com.typesafe.config.Config, + parentPath: java.lang.String, + $tsCfgValidator: $TsCfgValidator, + ): SimonaConfig.Simona.Control = { + SimonaConfig.Simona.Control( + transformer = $_LSimonaConfig_TransformerControlGroup( + c.getList("transformer"), + parentPath, + $tsCfgValidator, + ) + ) + } + private def $_LSimonaConfig_TransformerControlGroup( + cl: com.typesafe.config.ConfigList, + parentPath: java.lang.String, + $tsCfgValidator: $TsCfgValidator, + ): scala.List[SimonaConfig.TransformerControlGroup] = { + import scala.jdk.CollectionConverters._ + cl.asScala + .map(cv => + SimonaConfig.TransformerControlGroup( + cv.asInstanceOf[com.typesafe.config.ConfigObject].toConfig, + parentPath, + $tsCfgValidator, + ) + ) + .toList + } + } + final case class Event( listener: scala.Option[ scala.List[SimonaConfig.Simona.Event.Listener$Elm] @@ -2585,6 +2660,16 @@ object SimonaConfig { $tsCfgValidator: $TsCfgValidator, ): SimonaConfig.Simona = { SimonaConfig.Simona( + control = + if (c.hasPathOrNull("control")) + scala.Some( + SimonaConfig.Simona.Control( + c.getConfig("control"), + parentPath + "control.", + $tsCfgValidator, + ) + ) + else None, event = SimonaConfig.Simona.Event( if (c.hasPathOrNull("event")) c.getConfig("event") else com.typesafe.config.ConfigFactory.parseString("event{}"), diff --git a/src/main/scala/edu/ie3/simona/event/notifier/Notifier.scala b/src/main/scala/edu/ie3/simona/event/notifier/Notifier.scala index 916a97f9cb..b271a913e9 100644 --- a/src/main/scala/edu/ie3/simona/event/notifier/Notifier.scala +++ b/src/main/scala/edu/ie3/simona/event/notifier/Notifier.scala @@ -6,10 +6,10 @@ package edu.ie3.simona.event.notifier -import org.apache.pekko.actor.{Actor, ActorRef} import edu.ie3.simona.event.Event +import org.apache.pekko.actor.ActorRef -trait Notifier extends Actor { +trait Notifier { def listener: Iterable[ActorRef] diff --git a/src/main/scala/edu/ie3/simona/model/control/GridControls.scala b/src/main/scala/edu/ie3/simona/model/control/GridControls.scala new file mode 100644 index 0000000000..b2d00cba17 --- /dev/null +++ b/src/main/scala/edu/ie3/simona/model/control/GridControls.scala @@ -0,0 +1,23 @@ +/* + * © 2024. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.model.control + +/** Collection of grid-related control strategies + * + * @param transformerControlGroups + * Transformer control groups + */ +final case class GridControls( + transformerControlGroups: Set[TransformerControlGroupModel] +) + +object GridControls { + + /** Represents an empty GridControls group + */ + def empty: GridControls = GridControls(Set.empty) +} diff --git a/src/main/scala/edu/ie3/simona/model/control/TransformerControlGroupModel.scala b/src/main/scala/edu/ie3/simona/model/control/TransformerControlGroupModel.scala new file mode 100644 index 0000000000..2354770480 --- /dev/null +++ b/src/main/scala/edu/ie3/simona/model/control/TransformerControlGroupModel.scala @@ -0,0 +1,161 @@ +/* + * © 2021. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.model.control + +import breeze.math.Complex +import edu.ie3.datamodel.models.input.MeasurementUnitInput +import edu.ie3.powerflow.model.NodeData.StateData +import edu.ie3.powerflow.model.PowerFlowResult.SuccessFullPowerFlowResult +import edu.ie3.simona.config.SimonaConfig +import edu.ie3.simona.config.SimonaConfig.TransformerControlGroup +import edu.ie3.simona.model.control.TransformerControlGroupModel.{ + RegulationCriterion, + harmonizeRegulationNeeds, +} +import squants.{Dimensionless, Each} + +import java.util.UUID + +/** Business logic for a transformer control group. It's main purpose is to + * determine, if there is any regulation need and if yes, to what extent (here: + * voltage raise or reduction to achieve) + * + * @param measuredNodes + * The nodes (with voltage measurement) to consider + * @param regulationCriterion + * Function that determines the regulation need + */ +final case class TransformerControlGroupModel( + measuredNodes: Set[UUID], + regulationCriterion: RegulationCriterion, +) { + + /** Based on the given successful power flow result, determine the difference + * in voltage magnitude, that needs to be achieved by regulating the + * transformer tap position + * + * @param result + * Power flow result to account for + * @param uuidToIndex + * Mapping from node's uuid to nodal index + * @return + * Optional voltage magnitude, that a transformer tap regulation needs to + * achieve + */ + def determineRegulationNeed( + result: SuccessFullPowerFlowResult, + uuidToIndex: Map[UUID, Int], + ): Option[Dimensionless] = { + val regulationNeeds = result.nodeData + .filter { case StateData(resultNodeIndex, _, _, _) => + measuredNodes.exists { uuid => + resultNodeIndex == uuidToIndex(uuid) + } + } + .flatMap { case StateData(_, _, voltage, _) => + regulationCriterion(voltage) + } + harmonizeRegulationNeeds(regulationNeeds) + } +} + +object TransformerControlGroupModel { + type RegulationCriterion = + Complex => Option[Dimensionless] + + /** Build business models for control groups + * + * @param measurementUnitInput + * Set of [[MeasurementUnitInput]] s + * @param config + * List of configs for control groups + * @return + * A set of control group business models + */ + def buildControlGroups( + measurementUnitInput: Set[MeasurementUnitInput], + config: Iterable[SimonaConfig.TransformerControlGroup], + ): Set[TransformerControlGroupModel] = config.map { + case TransformerControlGroup(measurements, _, vMax, vMin) => + val nodeUuids = + determineNodeUuids(measurementUnitInput, measurements.toSet) + TransformerControlGroupModel(nodeUuids, regulationFunction(vMax, vMin)) + }.toSet + + /** Determine the uuids of the nodes to control + * + * @param measurementUnitInput + * Collection of all known [[MeasurementUnitInput]] s + * @param measurementConfigs + * Collection of all uuids, denoting which of the [[MeasurementUnitInput]] + * s does belong to this control group + * @return + * A set of relevant nodal uuids + */ + private def determineNodeUuids( + measurementUnitInput: Set[MeasurementUnitInput], + measurementConfigs: Set[String], + ): Set[UUID] = + measurementUnitInput + .filter(input => + measurementConfigs.contains(input.getUuid.toString) && input.getVMag + ) + .map(_.getNode.getUuid) + + /** Determine the regulation criterion of the nodes to control + * + * @param vMax + * Maximum voltage limit + * @param vMin + * Minimum voltage limit + * @return + * The regulation need, if applicable + */ + private def regulationFunction( + vMax: Double, + vMin: Double, + ): RegulationCriterion = { (voltage: Complex) => + voltage.abs match { + case vMag if vMag > vMax => + Some(vMax - vMag).map(Each(_)) + case vMag if vMag < vMin => + Some(vMin - vMag).map(Each(_)) + case _ => None + } + } + + /** Function to harmonize contrary requests for regulation + * + * @param regulationRequests: + * Array of all regulation requests + * @return + * None in case of contrary requests, else the highest or lowest voltage + * depending of the direction for regulation + */ + private def harmonizeRegulationNeeds( + regulationRequests: Array[Dimensionless] + ): Option[Dimensionless] = { + val negativeRequests = regulationRequests.filter(_ < Each(0d)) + val positiveRequests = regulationRequests.filter(_ > Each(0d)) + + (negativeRequests.nonEmpty, positiveRequests.nonEmpty) match { + case (true, true) => + /* There are requests for higher and lower voltages at the same time => do nothing! */ + None + case (true, false) => + /* There are only requests for lower voltages => decide for the lowest required voltage */ + negativeRequests.minOption + case (false, true) => + /* There are only requests for higher voltages => decide for the highest required voltage */ + positiveRequests.maxOption + case _ => + None + } + + } + +} 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 27a3f556ea..f2eab5ff1d 100644 --- a/src/main/scala/edu/ie3/simona/model/grid/GridModel.scala +++ b/src/main/scala/edu/ie3/simona/model/grid/GridModel.scala @@ -11,8 +11,11 @@ import breeze.math.Complex import edu.ie3.datamodel.exceptions.InvalidGridException import edu.ie3.datamodel.models.input.connector._ import edu.ie3.datamodel.models.input.container.SubGridContainer +import edu.ie3.simona.config.SimonaConfig import edu.ie3.simona.exceptions.GridInconsistencyException +import edu.ie3.simona.exceptions.agent.GridAgentInitializationException import edu.ie3.simona.model.SystemComponent +import edu.ie3.simona.model.control.{GridControls, TransformerControlGroupModel} import edu.ie3.simona.model.grid.GridModel.GridComponents import edu.ie3.simona.model.grid.Transformer3wPowerFlowCase.{ PowerFlowCaseA, @@ -37,6 +40,7 @@ final case class GridModel( subnetNo: Int, mainRefSystem: RefSystem, gridComponents: GridComponents, + gridControls: GridControls, ) { // init nodeUuidToIndexMap @@ -58,16 +62,21 @@ final case class GridModel( def nodeUuidToIndexMap: Map[UUID, Int] = _nodeUuidToIndexMap } -case object GridModel { +object GridModel { def apply( subGridContainer: SubGridContainer, refSystem: RefSystem, startDate: ZonedDateTime, endDate: ZonedDateTime, - ): GridModel = { - buildAndValidate(subGridContainer, refSystem, startDate, endDate) - } + simonaConfig: SimonaConfig, + ): GridModel = buildAndValidate( + subGridContainer, + refSystem, + startDate, + endDate, + simonaConfig, + ) /** structure that represents all grid components that are needed by a grid * model @@ -446,11 +455,68 @@ case object GridModel { } + /** Checks all ControlGroups if a) Transformer of ControlGroup and Measurement + * belongs to the same sub grid. b) Measurements are measure voltage + * magnitude. + * + * @param subGridContainer + * Container of all models for this sub grid + * @param maybeControlConfig + * Config of ControlGroup + */ + private def validateControlGroups( + subGridContainer: SubGridContainer, + maybeControlConfig: Option[SimonaConfig.Simona.Control], + ): Unit = { + maybeControlConfig.foreach { control => + val measurementUnits = + subGridContainer.getRawGrid.getMeasurementUnits.asScala + .map(measurement => measurement.getUuid -> measurement) + .toMap + + val transformerUnits2W = + subGridContainer.getRawGrid.getTransformer2Ws.asScala + .map(transformer2w => transformer2w.getUuid -> transformer2w) + .toMap + + val transformerUnits3W = + subGridContainer.getRawGrid.getTransformer3Ws.asScala + .map(transformer3w => transformer3w.getUuid -> transformer3w) + .toMap + + control.transformer.foreach { controlGroup => + controlGroup.transformers.map(UUID.fromString).foreach { transformer => + val transformerUnit2W = transformerUnits2W.get(transformer) + val transformerUnit3W = transformerUnits3W.get(transformer) + + if (transformerUnit2W.isDefined || transformerUnit3W.isDefined) { + controlGroup.measurements + .map(UUID.fromString) + .foreach { measurement => + val measurementUnit = measurementUnits.getOrElse( + measurement, + throw new GridAgentInitializationException( + s"${subGridContainer.getGridName} has a transformer control group (${control.transformer.toString}) with a measurement unit whose UUID does not exist in this subnet." + ), + ) + if (!measurementUnit.getVMag) + throw new GridAgentInitializationException( + s"${subGridContainer.getGridName} has a transformer control group (${control.transformer.toString}) with a measurement unit which does not measure voltage magnitude." + ) + } + } + + } + } + } + } + private def buildAndValidate( subGridContainer: SubGridContainer, refSystem: RefSystem, startDate: ZonedDateTime, endDate: ZonedDateTime, + simonaConfig: SimonaConfig, ): GridModel = { // build @@ -534,12 +600,36 @@ case object GridModel { switches, ) - val gridModel = - GridModel(subGridContainer.getSubnet, refSystem, gridComponents) + /* Build transformer control groups */ + val transformerControlGroups = simonaConfig.simona.control + .map { controlConfig => + TransformerControlGroupModel.buildControlGroups( + subGridContainer.getRawGrid.getMeasurementUnits.asScala.toSet, + controlConfig.transformer, + ) + } + .getOrElse(Set.empty) + + val gridModel = GridModel( + subGridContainer.getSubnet, + refSystem, + gridComponents, + GridControls(transformerControlGroups), + ) + + /** Check and validates the grid. Especially the consistency of the grid + * model the connectivity of the grid model if there is InitData for + * superior or inferior GridGates if there exits voltage measurements for + * transformerControlGroups + */ // validate validateConsistency(gridModel) validateConnectivity(gridModel) + validateControlGroups( + subGridContainer, + simonaConfig.simona.control, + ) // return gridModel 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 3d10fe3f5a..2c9ad3fb47 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,10 @@ package edu.ie3.simona.ontology.messages +import edu.ie3.simona.agent.grid.GridAgentMessage import edu.ie3.simona.ontology.messages.PowerMessage.ProvideGridPowerMessage.ExchangePower import edu.ie3.util.scala.quantities.ReactivePower +import org.apache.pekko.actor.typed.ActorRef import squants.{Dimensionless, Power} import java.util.UUID @@ -80,6 +82,7 @@ object PowerMessage { final case class RequestGridPowerMessage( currentSweepNo: Int, nodeUuids: Seq[UUID], + sender: ActorRef[GridAgentMessage], ) extends PowerRequestMessage /** Provide complex power at the nodes that the sender's sub grid shares with diff --git a/src/main/scala/edu/ie3/simona/sim/SimonaSim.scala b/src/main/scala/edu/ie3/simona/sim/SimonaSim.scala index a026cd9a9e..2c800b833c 100644 --- a/src/main/scala/edu/ie3/simona/sim/SimonaSim.scala +++ b/src/main/scala/edu/ie3/simona/sim/SimonaSim.scala @@ -117,7 +117,7 @@ object SimonaSim { extSimulationData.extDataServices.values .map(_.toTyped) .foreach(ctx.watch) - gridAgents.foreach(ref => ctx.watch(ref.toTyped)) + gridAgents.foreach(ref => ctx.watch(ref)) // Start simulation timeAdvancer ! TimeAdvancer.Start() @@ -128,7 +128,7 @@ object SimonaSim { primaryServiceProxy.toTyped, weatherService.toTyped, ) ++ - gridAgents.map(_.toTyped) ++ + gridAgents ++ extSimulationData.extDataServices.values.map(_.toTyped) idle( diff --git a/src/main/scala/edu/ie3/simona/sim/setup/SetupHelper.scala b/src/main/scala/edu/ie3/simona/sim/setup/SetupHelper.scala index 0df5aa30e1..55d907ae78 100644 --- a/src/main/scala/edu/ie3/simona/sim/setup/SetupHelper.scala +++ b/src/main/scala/edu/ie3/simona/sim/setup/SetupHelper.scala @@ -14,6 +14,7 @@ import edu.ie3.datamodel.models.result.ResultEntity import edu.ie3.datamodel.models.result.system.FlexOptionsResult import edu.ie3.datamodel.utils.ContainerUtils import edu.ie3.simona.agent.grid.GridAgentData.GridAgentInitData +import edu.ie3.simona.agent.grid.GridAgentMessage import edu.ie3.simona.config.RefSystemParser.ConfigRefSystems import edu.ie3.simona.config.SimonaConfig import edu.ie3.simona.exceptions.InitializationException @@ -23,8 +24,8 @@ import edu.ie3.simona.model.grid.RefSystem import edu.ie3.simona.util.ConfigUtil.{GridOutputConfigUtil, OutputConfigUtil} import edu.ie3.simona.util.ResultFileHierarchy.ResultEntityPathConfig import edu.ie3.simona.util.{EntityMapperUtil, ResultFileHierarchy} +import org.apache.pekko.actor.typed.ActorRef import edu.ie3.util.quantities.PowerSystemUnits -import org.apache.pekko.actor.ActorRef import squants.electro.Kilovolts /** Methods to support the setup of a simona simulation @@ -57,7 +58,7 @@ trait SetupHelper extends LazyLogging { */ def buildGridAgentInitData( subGridContainer: SubGridContainer, - subGridToActorRef: Map[Int, ActorRef], + subGridToActorRef: Map[Int, ActorRef[GridAgentMessage]], gridGates: Set[SubGridGate], configRefSystems: ConfigRefSystems, thermalGrids: Seq[ThermalGrid], @@ -98,10 +99,10 @@ trait SetupHelper extends LazyLogging { * A mapping from [[SubGridGate]] to corresponding actor reference */ def buildGateToActorRef( - subGridToActorRefMap: Map[Int, ActorRef], + subGridToActorRefMap: Map[Int, ActorRef[GridAgentMessage]], subGridGates: Set[SubGridGate], currentSubGrid: Int, - ): Map[SubGridGate, ActorRef] = + ): Map[SubGridGate, ActorRef[GridAgentMessage]] = subGridGates .groupBy(gate => (gate.superiorNode, gate.inferiorNode)) .flatMap(_._2.headOption) @@ -143,10 +144,10 @@ trait SetupHelper extends LazyLogging { * The actor reference of the sub grid to look for */ def getActorRef( - subGridToActorRefMap: Map[Int, ActorRef], + subGridToActorRefMap: Map[Int, ActorRef[GridAgentMessage]], currentSubGrid: Int, queriedSubGrid: Int, - ): ActorRef = { + ): ActorRef[GridAgentMessage] = { subGridToActorRefMap.get(queriedSubGrid) match { case Some(hit) => hit case _ => diff --git a/src/main/scala/edu/ie3/simona/sim/setup/SimonaSetup.scala b/src/main/scala/edu/ie3/simona/sim/setup/SimonaSetup.scala index 6626ffbffb..75eba20628 100644 --- a/src/main/scala/edu/ie3/simona/sim/setup/SimonaSetup.scala +++ b/src/main/scala/edu/ie3/simona/sim/setup/SimonaSetup.scala @@ -9,6 +9,7 @@ package edu.ie3.simona.sim.setup import edu.ie3.datamodel.graph.SubGridGate import edu.ie3.datamodel.models.input.connector.Transformer3WInput import edu.ie3.simona.agent.EnvironmentRefs +import edu.ie3.simona.agent.grid.GridAgentMessage import edu.ie3.simona.event.listener.{ResultEventListener, RuntimeEventListener} import edu.ie3.simona.event.{ResultEvent, RuntimeEvent} import edu.ie3.simona.ontology.messages.SchedulerMessage @@ -151,7 +152,7 @@ trait SimonaSetup { context: ActorContext[_], environmentRefs: EnvironmentRefs, resultEventListeners: Seq[ActorRef[ResultEvent]], - ): Iterable[ClassicRef] + ): Iterable[ActorRef[GridAgentMessage]] /** SIMONA links sub grids connected by a three winding transformer a bit * different. Therefore, the internal node has to be set as superior node. diff --git a/src/main/scala/edu/ie3/simona/sim/setup/SimonaStandaloneSetup.scala b/src/main/scala/edu/ie3/simona/sim/setup/SimonaStandaloneSetup.scala index 74ccd33975..71900875dd 100644 --- a/src/main/scala/edu/ie3/simona/sim/setup/SimonaStandaloneSetup.scala +++ b/src/main/scala/edu/ie3/simona/sim/setup/SimonaStandaloneSetup.scala @@ -11,9 +11,10 @@ import com.typesafe.scalalogging.LazyLogging import edu.ie3.datamodel.graph.SubGridTopologyGraph import edu.ie3.datamodel.models.input.container.{GridContainer, ThermalGrid} import edu.ie3.datamodel.models.input.thermal.ThermalBusInput -import edu.ie3.simona.actor.SimonaActorNaming._ +import edu.ie3.simona.actor.SimonaActorNaming.RichActorRefFactory import edu.ie3.simona.agent.EnvironmentRefs -import edu.ie3.simona.agent.grid.GridAgent +import edu.ie3.simona.agent.grid.GridAgentMessage.CreateGridAgent +import edu.ie3.simona.agent.grid.{GridAgent, GridAgentMessage} import edu.ie3.simona.api.ExtSimAdapter import edu.ie3.simona.api.data.ExtData import edu.ie3.simona.api.data.ev.{ExtEvData, ExtEvSimulation} @@ -51,11 +52,12 @@ import edu.ie3.simona.util.TickUtil.RichZonedDateTime import edu.ie3.util.TimeUtil import org.apache.pekko.actor.typed.ActorRef import org.apache.pekko.actor.typed.scaladsl.ActorContext -import org.apache.pekko.actor.typed.scaladsl.adapter._ -import org.apache.pekko.actor.{ - ActorContext => ClassicContext, - ActorRef => ClassicRef, +import org.apache.pekko.actor.typed.scaladsl.adapter.{ + ClassicActorRefOps, + TypedActorContextOps, + TypedActorRefOps, } +import org.apache.pekko.actor.{ActorRef => ClassicRef} import java.util.concurrent.LinkedBlockingQueue import scala.jdk.CollectionConverters._ @@ -78,7 +80,7 @@ class SimonaStandaloneSetup( context: ActorContext[_], environmentRefs: EnvironmentRefs, resultEventListeners: Seq[ActorRef[ResultEvent]], - ): Iterable[ClassicRef] = { + ): Iterable[ActorRef[GridAgentMessage]] = { /* get the grid */ val subGridTopologyGraph = GridProvider @@ -98,9 +100,9 @@ class SimonaStandaloneSetup( /* Create all agents and map the sub grid id to their actor references */ val subGridToActorRefMap = buildSubGridToActorRefMap( subGridTopologyGraph, - context.toClassic, + context, environmentRefs, - resultEventListeners.map(_.toClassic), + resultEventListeners, ) val keys = ScheduleLock.multiKey( @@ -143,7 +145,7 @@ class SimonaStandaloneSetup( thermalGrids, ) - currentActorRef ! GridAgent.Create(gridAgentInitData, key) + currentActorRef ! CreateGridAgent(gridAgentInitData, key) currentActorRef } @@ -379,20 +381,20 @@ class SimonaStandaloneSetup( def buildSubGridToActorRefMap( subGridTopologyGraph: SubGridTopologyGraph, - context: ClassicContext, + context: ActorContext[_], environmentRefs: EnvironmentRefs, - systemParticipantListener: Seq[ClassicRef], - ): Map[Int, ClassicRef] = { + resultEventListeners: Seq[ActorRef[ResultEvent]], + ): Map[Int, ActorRef[GridAgentMessage]] = { subGridTopologyGraph .vertexSet() .asScala .map(subGridContainer => { val gridAgentRef = - context.simonaActorOf( - GridAgent.props( + context.spawn( + GridAgent( environmentRefs, simonaConfig, - systemParticipantListener, + resultEventListeners, ), subGridContainer.getSubnet.toString, ) diff --git a/src/test/java/testutils/TestObjectFactory.java b/src/test/java/testutils/TestObjectFactory.java index c565347bcc..12ca8f6a23 100644 --- a/src/test/java/testutils/TestObjectFactory.java +++ b/src/test/java/testutils/TestObjectFactory.java @@ -37,7 +37,7 @@ public static NodeInput buildNodeInput( boolean isSlack, CommonVoltageLevel voltageLvl, int subnet) { return new NodeInput( UUID.randomUUID(), - "TEST_NODE_" + TEST_OBJECT_COUNTER, + "TEST_NODE_" + TEST_OBJECT_COUNTER++, OperatorInput.NO_OPERATOR_ASSIGNED, OperationTime.notLimited(), Quantities.getQuantity(1d, PU), @@ -103,7 +103,7 @@ public static LineTypeInput buildLineTypeInput(VoltageLevel voltageLvl) { public static SwitchInput buildSwitchInput(NodeInput nodeA, NodeInput nodeB) { return new SwitchInput( UUID.randomUUID(), - "TEST_SWITCH" + TEST_OBJECT_COUNTER, + "TEST_SWITCH" + TEST_OBJECT_COUNTER++, OperatorInput.NO_OPERATOR_ASSIGNED, OperationTime.notLimited(), nodeA, 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 6573f32bc0..d1715cb356 100644 --- a/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmCenGridSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmCenGridSpec.scala @@ -6,35 +6,32 @@ package edu.ie3.simona.agent.grid -import org.apache.pekko.actor.ActorSystem -import org.apache.pekko.actor.typed.scaladsl.adapter.ClassicActorRefOps -import org.apache.pekko.testkit.TestProbe -import com.typesafe.config.ConfigFactory import edu.ie3.datamodel.models.input.container.ThermalGrid import edu.ie3.simona.agent.EnvironmentRefs -import edu.ie3.simona.agent.grid.GridAgent.FinishGridSimulationTrigger import edu.ie3.simona.agent.grid.GridAgentData.GridAgentInitData -import edu.ie3.simona.agent.state.GridAgentState.SimulateGrid +import edu.ie3.simona.agent.grid.GridAgentMessage._ +import edu.ie3.simona.agent.grid.VoltageMessage.ProvideSlackVoltageMessage +import edu.ie3.simona.agent.grid.VoltageMessage.ProvideSlackVoltageMessage.ExchangeVoltage import edu.ie3.simona.event.ResultEvent.PowerFlowResultEvent +import edu.ie3.simona.event.{ResultEvent, RuntimeEvent} import edu.ie3.simona.model.grid.RefSystem -import edu.ie3.simona.ontology.messages.Activation import edu.ie3.simona.ontology.messages.PowerMessage.ProvideGridPowerMessage import edu.ie3.simona.ontology.messages.PowerMessage.ProvideGridPowerMessage.ExchangePower import edu.ie3.simona.ontology.messages.SchedulerMessage.{ Completion, ScheduleActivation, } -import edu.ie3.simona.ontology.messages.VoltageMessage.ProvideSlackVoltageMessage -import edu.ie3.simona.ontology.messages.VoltageMessage.ProvideSlackVoltageMessage.ExchangeVoltage +import edu.ie3.simona.ontology.messages.{Activation, SchedulerMessage} import edu.ie3.simona.scheduler.ScheduleLock import edu.ie3.simona.test.common.model.grid.DbfsTestGrid -import edu.ie3.simona.test.common.{ - ConfigTestData, - TestKitWithShutdown, - TestSpawnerClassic, -} +import edu.ie3.simona.test.common.{ConfigTestData, TestSpawnerTyped} import edu.ie3.simona.util.SimonaConstants.INIT_SIM_TICK import edu.ie3.util.scala.quantities.Megavars +import org.apache.pekko.actor.testkit.typed.scaladsl.{ + ScalaTestWithActorTestKit, + TestProbe, +} +import org.apache.pekko.actor.typed.scaladsl.adapter.TypedActorRefOps import squants.electro.Kilovolts import squants.energy.Megawatts @@ -49,23 +46,15 @@ import scala.language.postfixOps * interaction or cover this behaviour by another (integration) test! */ class DBFSAlgorithmCenGridSpec - extends TestKitWithShutdown( - ActorSystem( - "DBFSAlgorithmCenGridSpec", - ConfigFactory - .parseString(""" - |pekko.loggers =["org.apache.pekko.event.slf4j.Slf4jLogger"] - |pekko.loglevel="OFF" - """.stripMargin), - ) - ) + extends ScalaTestWithActorTestKit with DBFSMockGridAgents with ConfigTestData with DbfsTestGrid - with TestSpawnerClassic { + with TestSpawnerTyped { - private val scheduler = TestProbe("scheduler") - private val runtimeEvents = TestProbe("runtimeEvents") + private val scheduler: TestProbe[SchedulerMessage] = TestProbe("scheduler") + private val runtimeEvents: TestProbe[RuntimeEvent] = + TestProbe("runtimeEvents") private val primaryService = TestProbe("primaryService") private val weatherService = TestProbe("weatherService") @@ -86,20 +75,20 @@ class DBFSAlgorithmCenGridSpec ) private val environmentRefs = EnvironmentRefs( - scheduler = scheduler.ref.toTyped, + scheduler = scheduler.ref, runtimeEventListener = runtimeEvents.ref, - primaryServiceProxy = primaryService.ref, - weather = weatherService.ref, + primaryServiceProxy = primaryService.ref.toClassic, + weather = weatherService.ref.toClassic, evDataService = None, ) - val resultListener: TestProbe = TestProbe("resultListener") + val resultListener: TestProbe[ResultEvent] = TestProbe("resultListener") "A GridAgent actor in center position with async test" should { val centerGridAgent = - system.actorOf( - GridAgent.props( + testKit.spawn( + GridAgent( environmentRefs, simonaConfig, listener = Iterable(resultListener.ref), @@ -129,41 +118,30 @@ class DBFSAlgorithmCenGridSpec RefSystem("2000 MVA", "110 kV"), ) - val key = - ScheduleLock.singleKey(TSpawner, scheduler.ref.toTyped, INIT_SIM_TICK) - scheduler.expectMsgType[ScheduleActivation] // lock activation scheduled + val key = ScheduleLock.singleKey(TSpawner, scheduler.ref, INIT_SIM_TICK) + // lock activation scheduled + scheduler.expectMessageType[ScheduleActivation] - centerGridAgent ! GridAgent.Create( + centerGridAgent ! CreateGridAgent( gridAgentInitData, key, ) - scheduler.expectMsg( - ScheduleActivation(centerGridAgent.toTyped, INIT_SIM_TICK, Some(key)) - ) - scheduler.send(centerGridAgent, Activation(INIT_SIM_TICK)) - scheduler.expectMsg( - Completion( - centerGridAgent.toTyped, - Some(3600), - ) - ) + val scheduleActivationMsg = + scheduler.expectMessageType[ScheduleActivation] + scheduleActivationMsg.tick shouldBe INIT_SIM_TICK + scheduleActivationMsg.unlockKey shouldBe Some(key) + val gridAgentActivation = scheduleActivationMsg.actor + centerGridAgent ! WrappedActivation(Activation(INIT_SIM_TICK)) + scheduler.expectMessage(Completion(gridAgentActivation, Some(3600))) } - s"go to $SimulateGrid when it receives an activity start trigger" in { + s"go to SimulateGrid when it receives an activity start trigger" in { - scheduler.send( - centerGridAgent, - Activation(3600), - ) + centerGridAgent ! WrappedActivation(Activation(3600)) - scheduler.expectMsg( - Completion( - centerGridAgent.toTyped, - Some(3600), - ) - ) + scheduler.expectMessageType[Completion].newTick shouldBe Some(3600) } s"start the simulation when activation is sent" in { @@ -171,7 +149,7 @@ class DBFSAlgorithmCenGridSpec val firstSweepNo = 0 // send the start grid simulation trigger - scheduler.send(centerGridAgent, Activation(3600)) + centerGridAgent ! WrappedActivation(Activation(3600)) /* We expect one grid power request message per inferior grid */ @@ -188,7 +166,6 @@ 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) inferiorGrid12.requestSlackVoltage(centerGridAgent, firstSweepNo) @@ -239,8 +216,7 @@ class DBFSAlgorithmCenGridSpec // we now answer the request of our centerGridAgent // with three fake grid power messages and one fake slack voltage message - inferiorGrid11.gaProbe.send( - firstPowerRequestSender11, + firstPowerRequestSender11 ! WrappedPowerMessage( ProvideGridPowerMessage( inferiorGrid11.nodeUuids.map(nodeUuid => ExchangePower( @@ -249,11 +225,10 @@ class DBFSAlgorithmCenGridSpec Megavars(0.0), ) ) - ), + ) ) - inferiorGrid12.gaProbe.send( - firstPowerRequestSender12, + firstPowerRequestSender12 ! WrappedPowerMessage( ProvideGridPowerMessage( inferiorGrid12.nodeUuids.map(nodeUuid => ExchangePower( @@ -262,11 +237,10 @@ class DBFSAlgorithmCenGridSpec Megavars(0.0), ) ) - ), + ) ) - inferiorGrid13.gaProbe.send( - firstPowerRequestSender13, + firstPowerRequestSender13 ! WrappedPowerMessage( ProvideGridPowerMessage( inferiorGrid13.nodeUuids.map(nodeUuid => ExchangePower( @@ -275,24 +249,21 @@ class DBFSAlgorithmCenGridSpec Megavars(0.0), ) ) - ), + ) ) - superiorGridAgent.gaProbe.send( - firstSlackVoltageRequestSender, - ProvideSlackVoltageMessage( - firstSweepNo, - Seq( - ExchangeVoltage( - supNodeA.getUuid, - Kilovolts(380d), - Kilovolts(0d), - ), - ExchangeVoltage( - supNodeB.getUuid, - Kilovolts(380d), - Kilovolts(0d), - ), + firstSlackVoltageRequestSender ! ProvideSlackVoltageMessage( + firstSweepNo, + Seq( + ExchangeVoltage( + supNodeA.getUuid, + Kilovolts(380d), + Kilovolts(0d), + ), + ExchangeVoltage( + supNodeB.getUuid, + Kilovolts(380d), + Kilovolts(0d), ), ), ) @@ -327,21 +298,18 @@ class DBFSAlgorithmCenGridSpec superiorGridAgent.expectSlackVoltageRequest(secondSweepNo) // the superior grid would answer with updated slack voltage values - superiorGridAgent.gaProbe.send( - secondSlackAskSender, - ProvideSlackVoltageMessage( - secondSweepNo, - Seq( - ExchangeVoltage( - supNodeB.getUuid, - Kilovolts(374.22694614463d), // 380 kV @ 10° - Kilovolts(65.9863075134335d), // 380 kV @ 10° - ), - ExchangeVoltage( // this one should currently be ignored anyways - supNodeA.getUuid, - Kilovolts(380d), - Kilovolts(0d), - ), + secondSlackAskSender ! ProvideSlackVoltageMessage( + secondSweepNo, + Seq( + ExchangeVoltage( + supNodeB.getUuid, + Kilovolts(374.22694614463d), // 380 kV @ 10° + Kilovolts(65.9863075134335d), // 380 kV @ 10° + ), + ExchangeVoltage( // this one should currently be ignored anyways + supNodeA.getUuid, + Kilovolts(380d), + Kilovolts(0d), ), ), ) @@ -411,8 +379,8 @@ class DBFSAlgorithmCenGridSpec // we now answer the requests of our centerGridAgent // with three fake grid power message - inferiorGrid11.gaProbe.send( - secondPowerRequestSender11, + + secondPowerRequestSender11 ! WrappedPowerMessage( ProvideGridPowerMessage( inferiorGrid11.nodeUuids.map(nodeUuid => ExchangePower( @@ -421,11 +389,10 @@ class DBFSAlgorithmCenGridSpec Megavars(0.0), ) ) - ), + ) ) - inferiorGrid12.gaProbe.send( - secondPowerRequestSender12, + secondPowerRequestSender12 ! WrappedPowerMessage( ProvideGridPowerMessage( inferiorGrid12.nodeUuids.map(nodeUuid => ExchangePower( @@ -434,11 +401,10 @@ class DBFSAlgorithmCenGridSpec Megavars(0.0), ) ) - ), + ) ) - inferiorGrid13.gaProbe.send( - secondPowerRequestSender13, + secondPowerRequestSender13 ! WrappedPowerMessage( ProvideGridPowerMessage( inferiorGrid13.nodeUuids.map(nodeUuid => ExchangePower( @@ -447,7 +413,7 @@ class DBFSAlgorithmCenGridSpec Megavars(0.0), ) ) - ), + ) ) // we expect that the GridAgent unstashes the messages and return a value for our power request @@ -468,27 +434,22 @@ class DBFSAlgorithmCenGridSpec // normally the slack node would send a FinishGridSimulationTrigger to all // connected inferior grids, because the slack node is just a mock, we imitate this behavior - superiorGridAgent.gaProbe.send( - centerGridAgent, - FinishGridSimulationTrigger(3600), - ) + centerGridAgent ! FinishGridSimulationTrigger(3600) // after a FinishGridSimulationTrigger is send the inferior grids, they themselves will send the // Trigger forward the trigger to their connected inferior grids. Therefore the inferior grid // agent should receive a FinishGridSimulationTrigger - inferiorGrid11.gaProbe.expectMsg(FinishGridSimulationTrigger(3600)) - inferiorGrid12.gaProbe.expectMsg(FinishGridSimulationTrigger(3600)) - inferiorGrid13.gaProbe.expectMsg(FinishGridSimulationTrigger(3600)) - - // after all grids have received a FinishGridSimulationTrigger, the scheduler should receive a CompletionMessage - scheduler.expectMsg( - Completion( - centerGridAgent.toTyped, - Some(7200), - ) - ) + inferiorGrid11.gaProbe.expectMessage(FinishGridSimulationTrigger(3600)) + + inferiorGrid12.gaProbe.expectMessage(FinishGridSimulationTrigger(3600)) + + inferiorGrid13.gaProbe.expectMessage(FinishGridSimulationTrigger(3600)) + + // after all grids have received a FinishGridSimulationTrigger, the scheduler should receive a Completion + scheduler.expectMessageType[Completion].newTick shouldBe Some(7200) - resultListener.expectMsgPF() { + val resultMessage = resultListener.expectMessageType[ResultEvent] + resultMessage match { case powerFlowResultEvent: PowerFlowResultEvent => // we expect results for 4 nodes, 5 lines and 2 transformer2ws powerFlowResultEvent.nodeResults.size shouldBe 4 diff --git a/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmFailedPowerFlowSpec.scala b/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmFailedPowerFlowSpec.scala index 49c601a8c2..5e24639625 100644 --- a/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmFailedPowerFlowSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmFailedPowerFlowSpec.scala @@ -6,17 +6,14 @@ package edu.ie3.simona.agent.grid -import org.apache.pekko.actor.ActorSystem -import org.apache.pekko.actor.typed.scaladsl.adapter.ClassicActorRefOps -import org.apache.pekko.testkit.{ImplicitSender, TestProbe} -import com.typesafe.config.ConfigFactory import edu.ie3.datamodel.models.input.container.ThermalGrid import edu.ie3.simona.agent.EnvironmentRefs -import edu.ie3.simona.agent.grid.GridAgent.FinishGridSimulationTrigger import edu.ie3.simona.agent.grid.GridAgentData.GridAgentInitData -import edu.ie3.simona.agent.state.GridAgentState.SimulateGrid +import edu.ie3.simona.agent.grid.GridAgentMessage._ +import edu.ie3.simona.agent.grid.VoltageMessage.ProvideSlackVoltageMessage +import edu.ie3.simona.agent.grid.VoltageMessage.ProvideSlackVoltageMessage.ExchangeVoltage +import edu.ie3.simona.event.{ResultEvent, RuntimeEvent} import edu.ie3.simona.model.grid.RefSystem -import edu.ie3.simona.ontology.messages.Activation import edu.ie3.simona.ontology.messages.PowerMessage.ProvideGridPowerMessage.ExchangePower import edu.ie3.simona.ontology.messages.PowerMessage.{ FailedPowerFlow, @@ -26,17 +23,17 @@ import edu.ie3.simona.ontology.messages.SchedulerMessage.{ Completion, ScheduleActivation, } -import edu.ie3.simona.ontology.messages.VoltageMessage.ProvideSlackVoltageMessage -import edu.ie3.simona.ontology.messages.VoltageMessage.ProvideSlackVoltageMessage.ExchangeVoltage +import edu.ie3.simona.ontology.messages.{Activation, SchedulerMessage} import edu.ie3.simona.scheduler.ScheduleLock import edu.ie3.simona.test.common.model.grid.DbfsTestGrid -import edu.ie3.simona.test.common.{ - ConfigTestData, - TestKitWithShutdown, - TestSpawnerClassic, -} +import edu.ie3.simona.test.common.{ConfigTestData, TestSpawnerTyped} import edu.ie3.simona.util.SimonaConstants.INIT_SIM_TICK import edu.ie3.util.scala.quantities.Megavars +import org.apache.pekko.actor.testkit.typed.scaladsl.{ + ScalaTestWithActorTestKit, + TestProbe, +} +import org.apache.pekko.actor.typed.scaladsl.adapter.TypedActorRefOps import squants.electro.Kilovolts import squants.energy.Megawatts @@ -44,24 +41,15 @@ import scala.concurrent.duration.DurationInt import scala.language.postfixOps class DBFSAlgorithmFailedPowerFlowSpec - extends TestKitWithShutdown( - ActorSystem( - "DBFSAlgorithmSpec", - ConfigFactory - .parseString(""" - |pekko.loggers =["org.apache.pekko.event.slf4j.Slf4jLogger"] - |pekko.loglevel="OFF" - """.stripMargin), - ) - ) + extends ScalaTestWithActorTestKit with DBFSMockGridAgents with ConfigTestData - with ImplicitSender with DbfsTestGrid - with TestSpawnerClassic { + with TestSpawnerTyped { - private val scheduler = TestProbe("scheduler") - private val runtimeEvents = TestProbe("runtimeEvents") + private val scheduler: TestProbe[SchedulerMessage] = TestProbe("scheduler") + private val runtimeEvents: TestProbe[RuntimeEvent] = + TestProbe("runtimeEvents") private val primaryService = TestProbe("primaryService") private val weatherService = TestProbe("weatherService") @@ -74,20 +62,20 @@ class DBFSAlgorithmFailedPowerFlowSpec InferiorGA(TestProbe("inferiorGridAgent"), Seq(node1.getUuid)) private val environmentRefs = EnvironmentRefs( - scheduler = scheduler.ref.toTyped, + scheduler = scheduler.ref, runtimeEventListener = runtimeEvents.ref, - primaryServiceProxy = primaryService.ref, - weather = weatherService.ref, + primaryServiceProxy = primaryService.ref.toClassic, + weather = weatherService.ref.toClassic, evDataService = None, ) - val resultListener: TestProbe = TestProbe("resultListener") + val resultListener: TestProbe[ResultEvent] = TestProbe("resultListener") "A GridAgent actor in center position with async test" should { val centerGridAgent = - system.actorOf( - GridAgent.props( + testKit.spawn( + GridAgent( environmentRefs, simonaConfig, listener = Iterable(resultListener.ref), @@ -115,49 +103,39 @@ class DBFSAlgorithmFailedPowerFlowSpec ) val key = - ScheduleLock.singleKey(TSpawner, scheduler.ref.toTyped, INIT_SIM_TICK) - scheduler.expectMsgType[ScheduleActivation] // lock activation scheduled + ScheduleLock.singleKey(TSpawner, scheduler.ref, INIT_SIM_TICK) + // lock activation scheduled + scheduler.expectMessageType[ScheduleActivation] - centerGridAgent ! GridAgent.Create( + centerGridAgent ! CreateGridAgent( gridAgentInitData, key, ) - scheduler.expectMsg( - ScheduleActivation(centerGridAgent.toTyped, INIT_SIM_TICK, Some(key)) - ) - scheduler.send(centerGridAgent, Activation(INIT_SIM_TICK)) - scheduler.expectMsg( - Completion( - centerGridAgent.toTyped, - Some(3600), - ) - ) + val scheduleActivationMsg = + scheduler.expectMessageType[ScheduleActivation] + scheduleActivationMsg.tick shouldBe INIT_SIM_TICK + scheduleActivationMsg.unlockKey shouldBe Some(key) + val gridAgentActivation = scheduleActivationMsg.actor + centerGridAgent ! WrappedActivation(Activation(INIT_SIM_TICK)) + scheduler.expectMessage(Completion(gridAgentActivation, Some(3600))) } - s"go to $SimulateGrid when it receives an activation" in { + s"go to SimulateGrid when it receives an activation" in { // send init data to agent - scheduler.send( - centerGridAgent, - Activation(3600), - ) + centerGridAgent ! WrappedActivation(Activation(3600)) // we expect a completion message - scheduler.expectMsg( - Completion( - centerGridAgent.toTyped, - Some(3600), - ) - ) + scheduler.expectMessageType[Completion].newTick shouldBe Some(3600) } s"start the simulation when an activation is sent is sent, handle failed power flow if it occurs" in { val sweepNo = 0 // send the start grid simulation trigger - scheduler.send(centerGridAgent, Activation(3600)) + centerGridAgent ! WrappedActivation(Activation(3600)) // we expect a request for grid power values here for sweepNo $sweepNo val powerRequestSender = inferiorGridAgent.expectGridPowerRequest() @@ -186,8 +164,7 @@ class DBFSAlgorithmFailedPowerFlowSpec // we now answer the request of our centerGridAgent // with a fake grid power message and one fake slack voltage message - inferiorGridAgent.gaProbe.send( - powerRequestSender, + powerRequestSender ! WrappedPowerMessage( ProvideGridPowerMessage( inferiorGridAgent.nodeUuids.map(nodeUuid => ExchangePower( @@ -196,20 +173,17 @@ class DBFSAlgorithmFailedPowerFlowSpec Megavars(0.0), ) ) - ), + ) ) - superiorGridAgent.gaProbe.send( - slackVoltageRequestSender, - ProvideSlackVoltageMessage( - sweepNo, - Seq( - ExchangeVoltage( - supNodeA.getUuid, - Kilovolts(380d), - Kilovolts(0d), - ) - ), + slackVoltageRequestSender ! ProvideSlackVoltageMessage( + sweepNo, + Seq( + ExchangeVoltage( + supNodeA.getUuid, + Kilovolts(380d), + Kilovolts(0d), + ) ), ) @@ -221,27 +195,22 @@ class DBFSAlgorithmFailedPowerFlowSpec // the requested power is to high for the grid to handle, therefore the superior grid agent // receives a FailedPowerFlow message // wait 30 seconds max for power flow to finish - superiorGridAgent.gaProbe.expectMsg(30 seconds, FailedPowerFlow) + superiorGridAgent.gaProbe.expectMessage( + 30 seconds, + WrappedPowerMessage(FailedPowerFlow), + ) // normally the slack node would send a FinishGridSimulationTrigger to all // connected inferior grids, because the slack node is just a mock, we imitate this behavior - superiorGridAgent.gaProbe.send( - centerGridAgent, - FinishGridSimulationTrigger(3600), - ) + centerGridAgent ! FinishGridSimulationTrigger(3600) // after a FinishGridSimulationTrigger is send to the inferior grids, they themselves will // forward the trigger to their connected inferior grids. Therefore the inferior grid agent // should receive a FinishGridSimulationTrigger - inferiorGridAgent.gaProbe.expectMsg(FinishGridSimulationTrigger(3600)) + inferiorGridAgent.gaProbe.expectMessage(FinishGridSimulationTrigger(3600)) - // after all grids have received a FinishGridSimulationTrigger, the scheduler should receive a CompletionMessage - scheduler.expectMsg( - Completion( - centerGridAgent.toTyped, - Some(7200), - ) - ) + // after all grids have received a FinishGridSimulationTrigger, the scheduler should receive a Completion + scheduler.expectMessageType[Completion].newTick shouldBe Some(7200) resultListener.expectNoMessage() diff --git a/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmParticipantSpec.scala b/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmParticipantSpec.scala index 362d5ce692..4bcbe85b42 100644 --- a/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmParticipantSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmParticipantSpec.scala @@ -6,74 +6,67 @@ package edu.ie3.simona.agent.grid -import org.apache.pekko.actor.typed.scaladsl.adapter.{ - ClassicActorRefOps, - TypedActorRefOps, -} -import org.apache.pekko.actor.{ActorRef, ActorSystem} -import org.apache.pekko.testkit.{ImplicitSender, TestProbe} -import com.typesafe.config.ConfigFactory import edu.ie3.datamodel.graph.SubGridGate import edu.ie3.simona.agent.EnvironmentRefs -import edu.ie3.simona.agent.grid.GridAgent.FinishGridSimulationTrigger import edu.ie3.simona.agent.grid.GridAgentData.GridAgentInitData -import edu.ie3.simona.agent.state.GridAgentState.SimulateGrid +import edu.ie3.simona.agent.grid.GridAgentMessage.{ + CreateGridAgent, + FinishGridSimulationTrigger, + WrappedActivation, +} +import edu.ie3.simona.agent.grid.VoltageMessage.ProvideSlackVoltageMessage +import edu.ie3.simona.agent.grid.VoltageMessage.ProvideSlackVoltageMessage.ExchangeVoltage +import edu.ie3.simona.event.{ResultEvent, RuntimeEvent} import edu.ie3.simona.model.grid.RefSystem -import edu.ie3.simona.ontology.messages.Activation import edu.ie3.simona.ontology.messages.PowerMessage.ProvideGridPowerMessage.ExchangePower import edu.ie3.simona.ontology.messages.SchedulerMessage.{ Completion, ScheduleActivation, } -import edu.ie3.simona.ontology.messages.VoltageMessage.ProvideSlackVoltageMessage -import edu.ie3.simona.ontology.messages.VoltageMessage.ProvideSlackVoltageMessage.ExchangeVoltage +import edu.ie3.simona.ontology.messages.services.ServiceMessage import edu.ie3.simona.ontology.messages.services.ServiceMessage.PrimaryServiceRegistrationMessage import edu.ie3.simona.ontology.messages.services.ServiceMessage.RegistrationResponseMessage.RegistrationFailedMessage +import edu.ie3.simona.ontology.messages.{Activation, SchedulerMessage} import edu.ie3.simona.scheduler.ScheduleLock import edu.ie3.simona.test.common.model.grid.DbfsTestGridWithParticipants -import edu.ie3.simona.test.common.{ - ConfigTestData, - TestKitWithShutdown, - TestSpawnerClassic, -} +import edu.ie3.simona.test.common.{ConfigTestData, TestSpawnerTyped} import edu.ie3.simona.util.SimonaConstants.INIT_SIM_TICK import edu.ie3.util.scala.quantities.Megavars +import org.apache.pekko.actor.testkit.typed.scaladsl.{ + ScalaTestWithActorTestKit, + TestProbe, +} +import org.apache.pekko.actor.typed.ActorRef +import org.apache.pekko.actor.typed.scaladsl.adapter.TypedActorRefOps import squants.electro.Kilovolts import squants.energy.Megawatts import scala.language.postfixOps class DBFSAlgorithmParticipantSpec - extends TestKitWithShutdown( - ActorSystem( - "DBFSAlgorithmSpec", - ConfigFactory - .parseString(""" - |pekko.loggers =["org.apache.pekko.event.slf4j.Slf4jLogger"] - |pekko.loglevel="OFF" - """.stripMargin), - ) - ) + extends ScalaTestWithActorTestKit with DBFSMockGridAgents with ConfigTestData - with ImplicitSender with DbfsTestGridWithParticipants - with TestSpawnerClassic { + with TestSpawnerTyped { - private val scheduler = TestProbe("scheduler") - private val runtimeEvents = TestProbe("runtimeEvents") - private val primaryService = TestProbe("primaryService") + private val scheduler: TestProbe[SchedulerMessage] = TestProbe("scheduler") + private val runtimeEvents: TestProbe[RuntimeEvent] = + TestProbe("runtimeEvents") + private val primaryService: TestProbe[ServiceMessage] = + TestProbe("primaryService") private val weatherService = TestProbe("weatherService") private val environmentRefs = EnvironmentRefs( - scheduler = scheduler.ref.toTyped, + scheduler = scheduler.ref, runtimeEventListener = runtimeEvents.ref, - primaryServiceProxy = primaryService.ref, - weather = weatherService.ref, + primaryServiceProxy = primaryService.ref.toClassic, + weather = weatherService.ref.toClassic, evDataService = None, ) - protected val resultListener: TestProbe = TestProbe("resultListener") + protected val resultListener: TestProbe[ResultEvent] = + TestProbe("resultListener") private val superiorGridAgent = SuperiorGA( TestProbe("superiorGridAgent_1000"), @@ -81,8 +74,8 @@ class DBFSAlgorithmParticipantSpec ) "Test participant" should { - val gridAgentWithParticipants = system.actorOf( - GridAgent.props( + val gridAgentWithParticipants = testKit.spawn( + GridAgent( environmentRefs, simonaConfig, Iterable(resultListener.ref), @@ -92,7 +85,7 @@ class DBFSAlgorithmParticipantSpec s"initialize itself when it receives an init activation" in { // this subnet has 1 superior grid (ehv) and 3 inferior grids (mv). Map the gates to test probes accordingly - val subGridGateToActorRef: Map[SubGridGate, ActorRef] = + val subGridGateToActorRef: Map[SubGridGate, ActorRef[GridAgentMessage]] = hvSubGridGates.map { gate => gate -> superiorGridAgent.ref }.toMap @@ -105,77 +98,54 @@ class DBFSAlgorithmParticipantSpec ) val key = - ScheduleLock.singleKey(TSpawner, scheduler.ref.toTyped, INIT_SIM_TICK) - scheduler.expectMsgType[ScheduleActivation] // lock activation scheduled - - gridAgentWithParticipants ! GridAgent.Create(gridAgentInitData, key) - scheduler.expectMsg( - ScheduleActivation( - gridAgentWithParticipants.toTyped, - INIT_SIM_TICK, - Some(key), - ) - ) + ScheduleLock.singleKey(TSpawner, scheduler.ref, INIT_SIM_TICK) + scheduler + .expectMessageType[ScheduleActivation] // lock activation scheduled - // send init data to agent and expect a CompletionMessage - scheduler.send(gridAgentWithParticipants, Activation(INIT_SIM_TICK)) - - val loadAgent = - scheduler.expectMsgPF() { - case ScheduleActivation( - loadAgent, - INIT_SIM_TICK, - _, - ) => - loadAgent - } - - scheduler.expectMsg( - Completion( - gridAgentWithParticipants.toTyped, - Some(3600), - ) - ) + gridAgentWithParticipants ! CreateGridAgent(gridAgentInitData, key) + + val scheduleActivationMsg = + scheduler.expectMessageType[ScheduleActivation] + scheduleActivationMsg.tick shouldBe INIT_SIM_TICK + scheduleActivationMsg.unlockKey shouldBe Some(key) + val gridAgentActivation = scheduleActivationMsg.actor + + // send init data to agent and expect a Completion + gridAgentWithParticipants ! WrappedActivation(Activation(INIT_SIM_TICK)) + + val scheduleLoadAgentMsg = scheduler.expectMessageType[ScheduleActivation] + scheduleLoadAgentMsg.tick shouldBe INIT_SIM_TICK + val loadAgent = scheduleLoadAgentMsg.actor - scheduler.send(loadAgent.toClassic, Activation(INIT_SIM_TICK)) + scheduler.expectMessage(Completion(gridAgentActivation, Some(3600))) - primaryService.expectMsg( + loadAgent ! Activation(INIT_SIM_TICK) + + primaryService.expectMessage( PrimaryServiceRegistrationMessage(load1.getUuid) ) - primaryService.send( - loadAgent.toClassic, - RegistrationFailedMessage(primaryService.ref), + loadAgent.toClassic ! RegistrationFailedMessage( + primaryService.ref.toClassic ) - scheduler.expectMsg(Completion(loadAgent, Some(0))) + scheduler.expectMessage(Completion(loadAgent, Some(0))) // triggering the loadAgent's calculation - scheduler.send( - loadAgent.toClassic, - Activation(0), - ) - // the load agent should send a CompletionMessage - scheduler.expectMsg(Completion(loadAgent, None)) + loadAgent ! Activation(0) + + // the load agent should send a Completion + scheduler.expectMessage(Completion(loadAgent, None)) } - s"go to $SimulateGrid when it receives an activity start trigger" in { + s"go to SimulateGrid when it receives an activity start trigger" in { // send init data to agent - scheduler.send( - gridAgentWithParticipants, - Activation(3600), - ) + gridAgentWithParticipants ! WrappedActivation(Activation(3600)) // we expect a completion message - scheduler.expectMsg( - Completion( - gridAgentWithParticipants.toTyped, - Some(3600), - ) - ) - + scheduler.expectMessageType[Completion].newTick shouldBe Some(3600) } s"check the request asset power message indirectly" in { @@ -184,7 +154,7 @@ class DBFSAlgorithmParticipantSpec // send the start grid simulation trigger // the gird agent should send a RequestAssetPowerMessage to the load agent - scheduler.send(gridAgentWithParticipants, Activation(3600)) + gridAgentWithParticipants ! WrappedActivation(Activation(3600)) // we expect a request for voltage values of our slack node // (voltages are requested by our agent under test from the superior grid) @@ -193,17 +163,14 @@ class DBFSAlgorithmParticipantSpec // we now answer the request of our gridAgentsWithParticipants // with a fake slack voltage message - superiorGridAgent.gaProbe.send( - firstSlackVoltageRequestSender, - ProvideSlackVoltageMessage( - firstSweepNo, - Seq( - ExchangeVoltage( - supNodeA.getUuid, - Kilovolts(380d), - Kilovolts(0d), - ) - ), + firstSlackVoltageRequestSender ! ProvideSlackVoltageMessage( + firstSweepNo, + Seq( + ExchangeVoltage( + supNodeA.getUuid, + Kilovolts(380d), + Kilovolts(0d), + ) ), ) @@ -241,17 +208,14 @@ class DBFSAlgorithmParticipantSpec superiorGridAgent.expectSlackVoltageRequest(secondSweepNo) // the superior grid would answer with updated slack voltage values - superiorGridAgent.gaProbe.send( - secondSlackAskSender, - ProvideSlackVoltageMessage( - secondSweepNo, - Seq( - ExchangeVoltage( - supNodeA.getUuid, - Kilovolts(374.2269461446d), - Kilovolts(65.9863075134d), - ) - ), + secondSlackAskSender ! ProvideSlackVoltageMessage( + secondSweepNo, + Seq( + ExchangeVoltage( + supNodeA.getUuid, + Kilovolts(374.2269461446d), + Kilovolts(65.9863075134d), + ) ), ) @@ -269,17 +233,9 @@ class DBFSAlgorithmParticipantSpec // normally the superior grid agent would send a FinishGridSimulationTrigger to the inferior grid agent after the convergence // (here we do it by hand) - superiorGridAgent.gaProbe.send( - gridAgentWithParticipants, - FinishGridSimulationTrigger(3600L), - ) + gridAgentWithParticipants ! FinishGridSimulationTrigger(3600L) - scheduler.expectMsg( - Completion( - gridAgentWithParticipants.toTyped, - Some(7200), - ) - ) + scheduler.expectMessageType[Completion].newTick shouldBe Some(7200) } } } 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 fd5974b755..1c2907a411 100644 --- a/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmSupGridSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmSupGridSpec.scala @@ -6,19 +6,19 @@ package edu.ie3.simona.agent.grid -import org.apache.pekko.actor.typed.scaladsl.adapter.ClassicActorRefOps -import org.apache.pekko.actor.{ActorRef, ActorSystem} -import org.apache.pekko.testkit.{ImplicitSender, TestProbe} -import com.typesafe.config.ConfigFactory import edu.ie3.datamodel.graph.SubGridGate import edu.ie3.datamodel.models.input.container.ThermalGrid import edu.ie3.simona.agent.EnvironmentRefs -import edu.ie3.simona.agent.grid.GridAgent.FinishGridSimulationTrigger import edu.ie3.simona.agent.grid.GridAgentData.GridAgentInitData -import edu.ie3.simona.agent.state.GridAgentState.SimulateGrid +import edu.ie3.simona.agent.grid.GridAgentMessage.{ + CreateGridAgent, + FinishGridSimulationTrigger, + WrappedActivation, + WrappedPowerMessage, +} import edu.ie3.simona.event.ResultEvent.PowerFlowResultEvent +import edu.ie3.simona.event.{ResultEvent, RuntimeEvent} import edu.ie3.simona.model.grid.RefSystem -import edu.ie3.simona.ontology.messages.Activation import edu.ie3.simona.ontology.messages.PowerMessage.ProvideGridPowerMessage.ExchangePower import edu.ie3.simona.ontology.messages.PowerMessage.{ ProvideGridPowerMessage, @@ -28,16 +28,19 @@ import edu.ie3.simona.ontology.messages.SchedulerMessage.{ Completion, ScheduleActivation, } +import edu.ie3.simona.ontology.messages.services.ServiceMessage +import edu.ie3.simona.ontology.messages.{Activation, SchedulerMessage} import edu.ie3.simona.scheduler.ScheduleLock import edu.ie3.simona.test.common.model.grid.DbfsTestGrid -import edu.ie3.simona.test.common.{ - ConfigTestData, - TestKitWithShutdown, - TestSpawnerClassic, - UnitSpec, -} +import edu.ie3.simona.test.common.{ConfigTestData, TestSpawnerTyped, UnitSpec} import edu.ie3.simona.util.SimonaConstants.INIT_SIM_TICK import edu.ie3.util.scala.quantities.Megavars +import org.apache.pekko.actor.testkit.typed.scaladsl.{ + ScalaTestWithActorTestKit, + TestProbe, +} +import org.apache.pekko.actor.typed.ActorRef +import org.apache.pekko.actor.typed.scaladsl.adapter.TypedActorRefOps import squants.energy.Megawatts import java.util.UUID @@ -50,41 +53,33 @@ import scala.language.postfixOps * [[GridAgent]] are simulated by the TestKit. */ class DBFSAlgorithmSupGridSpec - extends TestKitWithShutdown( - ActorSystem( - "DBFSAlgorithmSpec", - ConfigFactory - .parseString(""" - |pekko.loggers =["org.apache.pekko.event.slf4j.Slf4jLogger"] - |pekko.loglevel="OFF" - """.stripMargin), - ) - ) + extends ScalaTestWithActorTestKit with UnitSpec with ConfigTestData - with ImplicitSender with DbfsTestGrid - with TestSpawnerClassic { + with TestSpawnerTyped { - private val scheduler: TestProbe = TestProbe("scheduler") - private val runtimeEvents = TestProbe("runtimeEvents") - private val primaryService: TestProbe = TestProbe("primaryService") - private val weatherService: TestProbe = TestProbe("weatherService") - private val hvGrid: TestProbe = TestProbe("hvGrid") + private val scheduler: TestProbe[SchedulerMessage] = TestProbe("scheduler") + private val runtimeEvents: TestProbe[RuntimeEvent] = + TestProbe("runtimeEvents") + private val primaryService: TestProbe[ServiceMessage] = + TestProbe("primaryService") + private val weatherService = TestProbe("weatherService") + private val hvGrid: TestProbe[GridAgentMessage] = TestProbe("hvGrid") private val environmentRefs = EnvironmentRefs( - scheduler = scheduler.ref.toTyped, + scheduler = scheduler.ref, runtimeEventListener = runtimeEvents.ref, - primaryServiceProxy = primaryService.ref, - weather = weatherService.ref, + primaryServiceProxy = primaryService.ref.toClassic, + weather = weatherService.ref.toClassic, evDataService = None, ) - val resultListener: TestProbe = TestProbe("resultListener") + val resultListener: TestProbe[ResultEvent] = TestProbe("resultListener") "A GridAgent actor in superior position with async test" should { - val superiorGridAgentFSM: ActorRef = system.actorOf( - GridAgent.props( + val superiorGridAgentFSM: ActorRef[GridAgentMessage] = testKit.spawn( + GridAgent( environmentRefs, simonaConfig, listener = Iterable(resultListener.ref), @@ -92,7 +87,7 @@ class DBFSAlgorithmSupGridSpec ) s"initialize itself when it receives an init activation" in { - val subnetGatesToActorRef: Map[SubGridGate, ActorRef] = + val subnetGatesToActorRef: Map[SubGridGate, ActorRef[GridAgentMessage]] = ehvSubGridGates.map(gate => gate -> hvGrid.ref).toMap val gridAgentInitData = @@ -104,30 +99,28 @@ class DBFSAlgorithmSupGridSpec ) val key = - ScheduleLock.singleKey(TSpawner, scheduler.ref.toTyped, INIT_SIM_TICK) - scheduler.expectMsgType[ScheduleActivation] // lock activation scheduled - - superiorGridAgentFSM ! GridAgent.Create(gridAgentInitData, key) - scheduler.expectMsg( - ScheduleActivation( - superiorGridAgentFSM.toTyped, - INIT_SIM_TICK, - Some(key), - ) - ) + ScheduleLock.singleKey(TSpawner, scheduler.ref, INIT_SIM_TICK) + // lock activation scheduled + scheduler.expectMessageType[ScheduleActivation] + + superiorGridAgentFSM ! CreateGridAgent(gridAgentInitData, key) - scheduler.send(superiorGridAgentFSM, Activation(INIT_SIM_TICK)) - scheduler.expectMsg(Completion(superiorGridAgentFSM.toTyped, Some(3600))) + val scheduleActivationMsg = + scheduler.expectMessageType[ScheduleActivation] + scheduleActivationMsg.tick shouldBe INIT_SIM_TICK + scheduleActivationMsg.unlockKey shouldBe Some(key) + val gridAgentActivation = scheduleActivationMsg.actor + superiorGridAgentFSM ! WrappedActivation(Activation(INIT_SIM_TICK)) + scheduler.expectMessage(Completion(gridAgentActivation, Some(3600))) } - s"go to $SimulateGrid when it receives an activity start trigger" in { + s"go to SimulateGrid when it receives an activity start trigger" in { // send init data to agent - scheduler.send(superiorGridAgentFSM, Activation(3600)) + superiorGridAgentFSM ! WrappedActivation(Activation(3600)) // we expect a completion message - scheduler.expectMsg(Completion(superiorGridAgentFSM.toTyped, Some(3600))) - + scheduler.expectMessageType[Completion].newTick shouldBe Some(3600) } s"start the simulation, do 2 sweeps and should end afterwards when no deviation on nodal " + @@ -139,25 +132,29 @@ class DBFSAlgorithmSupGridSpec Vector(UUID.fromString("9fe5fa33-6d3b-4153-a829-a16f4347bc4e")) // send the start grid simulation trigger - scheduler.send(superiorGridAgentFSM, Activation(3600)) + superiorGridAgentFSM ! WrappedActivation(Activation(3600)) // we expect a request for grid power values here for sweepNo $sweepNo - hvGrid.expectMsgPF() { - case requestGridPowerMessage: RequestGridPowerMessage => + val message = hvGrid.expectMessageType[WrappedPowerMessage] + + val lastSender = message match { + case WrappedPowerMessage( + requestGridPowerMessage: RequestGridPowerMessage + ) => requestGridPowerMessage.currentSweepNo shouldBe sweepNo requestGridPowerMessage.nodeUuids should contain allElementsOf requestedConnectionNodeUuids + + requestGridPowerMessage.sender 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 - hvGrid.send( - hvGrid.lastSender, + lastSender ! WrappedPowerMessage( ProvideGridPowerMessage( requestedConnectionNodeUuids.map { uuid => ExchangePower( @@ -166,18 +163,20 @@ class DBFSAlgorithmSupGridSpec Megavars(0.0), ) } - ), + ) ) // we expect a completion message here and that the agent goes back to simulate grid // and waits until the newly scheduled StartGridSimulationTrigger is send // wait 30 seconds max for power flow to finish - scheduler.expectMsgPF(30 seconds) { + scheduler.expectMessageType[Completion](130 seconds) match { case Completion(_, Some(3600)) => // we expect another completion message when the agent is in SimulateGrid again case Completion(_, Some(7200)) => // agent should be in Idle again and listener should contain power flow result data - resultListener.expectMsgPF() { + val resultMessage = resultListener.expectMessageType[ResultEvent] + + resultMessage match { case powerFlowResultEvent: PowerFlowResultEvent => powerFlowResultEvent.nodeResults.headOption match { case Some(value) => @@ -198,7 +197,7 @@ class DBFSAlgorithmSupGridSpec // no failed power flow runtimeEvents.expectNoMessage() - hvGrid.expectMsg(FinishGridSimulationTrigger(3600)) + hvGrid.expectMessage(FinishGridSimulationTrigger(3600)) case x => fail( @@ -242,12 +241,10 @@ class DBFSAlgorithmSupGridSpec ) // bring agent in simulate grid state - scheduler.send(superiorGridAgentFSM, Activation(3600)) + superiorGridAgentFSM ! WrappedActivation(Activation(3600)) // we expect a completion message - scheduler.expectMsg( - Completion(superiorGridAgentFSM.toTyped, Some(3600)) - ) + scheduler.expectMessageType[Completion].newTick shouldBe Some(3600) // go on with testing the sweep behaviour for (sweepNo <- 0 to maxNumberOfTestSweeps) { @@ -256,13 +253,19 @@ class DBFSAlgorithmSupGridSpec Vector(UUID.fromString("9fe5fa33-6d3b-4153-a829-a16f4347bc4e")) // send the start grid simulation trigger - scheduler.send(superiorGridAgentFSM, Activation(3600)) + superiorGridAgentFSM ! WrappedActivation(Activation(3600)) // we expect a request for grid power values here for sweepNo $sweepNo - hvGrid.expectMsgPF() { - case requestGridPowerMessage: RequestGridPowerMessage => + val message = hvGrid.expectMessageType[GridAgentMessage] + + val lastSender = message match { + case WrappedPowerMessage( + requestGridPowerMessage: RequestGridPowerMessage + ) => requestGridPowerMessage.currentSweepNo shouldBe sweepNo requestGridPowerMessage.nodeUuids should contain allElementsOf requestedConnectionNodeUuids + + requestGridPowerMessage.sender case x => fail( s"Invalid message received when expecting a request for grid power values! Message was $x" @@ -272,8 +275,7 @@ class DBFSAlgorithmSupGridSpec // 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 - hvGrid.send( - hvGrid.lastSender, + lastSender ! WrappedPowerMessage( ProvideGridPowerMessage( requestedConnectionNodeUuids.map { uuid => ExchangePower( @@ -282,7 +284,7 @@ class DBFSAlgorithmSupGridSpec deviations(sweepNo)._2, ) } - ), + ) ) // we expect a completion message here and that the agent goes back to simulate grid @@ -290,13 +292,16 @@ class DBFSAlgorithmSupGridSpec // Simulate Grid // wait 30 seconds max for power flow to finish - scheduler.expectMsgPF(30 seconds) { + scheduler.expectMessageType[Completion](30 seconds) match { case Completion(_, Some(3600)) => // when we received a FinishGridSimulationTrigger (as inferior grid agent) // we expect another completion message then as well (scheduler view) case Completion(_, Some(7200)) => // after doing cleanup stuff, our agent should go back to idle again and listener should contain power flow result data - resultListener.expectMsgPF() { + val resultMessage = + resultListener.expectMessageType[ResultEvent] + + resultMessage match { case powerFlowResultEvent: PowerFlowResultEvent => powerFlowResultEvent.nodeResults.headOption match { case Some(value) => @@ -317,7 +322,7 @@ class DBFSAlgorithmSupGridSpec // no failed power flow runtimeEvents.expectNoMessage() - hvGrid.expectMsg(FinishGridSimulationTrigger(3600)) + hvGrid.expectMessage(FinishGridSimulationTrigger(3600)) case x => fail( diff --git a/src/test/scala/edu/ie3/simona/agent/grid/DBFSMockGridAgents.scala b/src/test/scala/edu/ie3/simona/agent/grid/DBFSMockGridAgents.scala index 6a001d6e1b..27c542611c 100644 --- a/src/test/scala/edu/ie3/simona/agent/grid/DBFSMockGridAgents.scala +++ b/src/test/scala/edu/ie3/simona/agent/grid/DBFSMockGridAgents.scala @@ -6,20 +6,21 @@ package edu.ie3.simona.agent.grid -import org.apache.pekko.actor.ActorRef -import org.apache.pekko.testkit.TestProbe +import edu.ie3.simona.agent.grid.GridAgentMessage.WrappedPowerMessage +import edu.ie3.simona.agent.grid.VoltageMessage.ProvideSlackVoltageMessage.ExchangeVoltage +import edu.ie3.simona.agent.grid.VoltageMessage.{ + ProvideSlackVoltageMessage, + RequestSlackVoltageMessage, +} import edu.ie3.simona.ontology.messages.PowerMessage.ProvideGridPowerMessage.ExchangePower import edu.ie3.simona.ontology.messages.PowerMessage.{ ProvideGridPowerMessage, RequestGridPowerMessage, } -import edu.ie3.simona.ontology.messages.VoltageMessage.ProvideSlackVoltageMessage.ExchangeVoltage -import edu.ie3.simona.ontology.messages.VoltageMessage.{ - ProvideSlackVoltageMessage, - RequestSlackVoltageMessage, -} import edu.ie3.simona.test.common.UnitSpec import edu.ie3.util.scala.quantities.{Megavars, ReactivePower} +import org.apache.pekko.actor.testkit.typed.scaladsl.TestProbe +import org.apache.pekko.actor.typed.ActorRef import squants.Power import squants.electro.Volts import squants.energy.Megawatts @@ -40,36 +41,39 @@ trait DBFSMockGridAgents extends UnitSpec { : squants.electro.ElectricPotential = Volts(1e-6) sealed trait GAActorAndModel { - val gaProbe: TestProbe + val gaProbe: TestProbe[GridAgentMessage] val nodeUuids: Seq[UUID] - def ref: ActorRef = gaProbe.ref + def ref: ActorRef[GridAgentMessage] = gaProbe.ref } final case class InferiorGA( - override val gaProbe: TestProbe, + override val gaProbe: TestProbe[GridAgentMessage], override val nodeUuids: Seq[UUID], ) extends GAActorAndModel { - def expectGridPowerRequest(): ActorRef = { - gaProbe - .expectMsgType[RequestGridPowerMessage] - .nodeUuids should contain allElementsOf nodeUuids + def expectGridPowerRequest(): ActorRef[GridAgentMessage] = { + gaProbe.expectMessageType[GridAgentMessage] match { + case WrappedPowerMessage( + requestGridPowerMessage: RequestGridPowerMessage + ) => + requestGridPowerMessage.nodeUuids should contain allElementsOf nodeUuids - gaProbe.lastSender + requestGridPowerMessage.sender + } } def expectSlackVoltageProvision( expectedSweepNo: Int, expectedExchangedVoltages: Seq[ExchangeVoltage], ): Unit = { - inside(gaProbe.expectMsgType[ProvideSlackVoltageMessage]) { - case ProvideSlackVoltageMessage(sweepNo, exchangedVoltages) => - sweepNo shouldBe expectedSweepNo + gaProbe.expectMessageType[GridAgentMessage] match { + case msg: ProvideSlackVoltageMessage => + msg.currentSweepNo shouldBe expectedSweepNo - exchangedVoltages.size shouldBe expectedExchangedVoltages.size + msg.nodalSlackVoltages.size shouldBe expectedExchangedVoltages.size expectedExchangedVoltages.foreach { expectedVoltage => - exchangedVoltages.find( + msg.nodalSlackVoltages.find( _.nodeUuid == expectedVoltage.nodeUuid ) match { case Some(ExchangeVoltage(_, actualE, actualF)) => @@ -85,42 +89,43 @@ trait DBFSMockGridAgents extends UnitSpec { } } - def requestSlackVoltage(receiver: ActorRef, sweepNo: Int): Unit = - gaProbe.send( - receiver, - RequestSlackVoltageMessage(sweepNo, nodeUuids), - ) + def requestSlackVoltage( + receiver: ActorRef[GridAgentMessage], + sweepNo: Int, + ): Unit = + receiver ! RequestSlackVoltageMessage(sweepNo, nodeUuids, gaProbe.ref) } final case class SuperiorGA( - override val gaProbe: TestProbe, + override val gaProbe: TestProbe[GridAgentMessage], 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 - } + def expectSlackVoltageRequest( + expectedSweepNo: Int + ): ActorRef[GridAgentMessage] = { + gaProbe.expectMessageType[GridAgentMessage] match { + case requestSlackVoltageMessage: RequestSlackVoltageMessage => + requestSlackVoltageMessage.currentSweepNo shouldBe expectedSweepNo + requestSlackVoltageMessage.nodeUuids should have size nodeUuids.size + requestSlackVoltageMessage.nodeUuids should contain allElementsOf nodeUuids - gaProbe.lastSender + requestSlackVoltageMessage.sender + } } def expectGridPowerProvision( expectedExchangedPowers: Seq[ExchangePower], maxDuration: FiniteDuration = 30 seconds, ): Unit = { - inside(gaProbe.expectMsgType[ProvideGridPowerMessage](maxDuration)) { - case ProvideGridPowerMessage(exchangedPower) => - exchangedPower should have size expectedExchangedPowers.size + gaProbe.expectMessageType[GridAgentMessage](maxDuration) match { + case WrappedPowerMessage(msg: ProvideGridPowerMessage) => + msg.nodalResidualPower should have size expectedExchangedPowers.size expectedExchangedPowers.foreach { expectedPower => - exchangedPower.find(_.nodeUuid == expectedPower.nodeUuid) match { + msg.nodalResidualPower.find( + _.nodeUuid == expectedPower.nodeUuid + ) match { case Some(ExchangePower(_, actualP, actualQ)) => actualP should approximate(expectedPower.p) actualQ should approximate(expectedPower.q) @@ -131,17 +136,15 @@ trait DBFSMockGridAgents extends UnitSpec { ) } } - } } - def requestGridPower(receiver: ActorRef, sweepNo: Int): Unit = { - gaProbe.send( - receiver, - RequestGridPowerMessage( - sweepNo, - nodeUuids, - ), + def requestGridPower( + receiver: ActorRef[GridAgentMessage], + sweepNo: Int, + ): Unit = { + receiver ! WrappedPowerMessage( + RequestGridPowerMessage(sweepNo, nodeUuids, gaProbe.ref) ) } } diff --git a/src/test/scala/edu/ie3/simona/agent/grid/GridAgentSetup2WSpec.scala b/src/test/scala/edu/ie3/simona/agent/grid/GridAgentSetup2WSpec.scala deleted file mode 100644 index 58320c23e8..0000000000 --- a/src/test/scala/edu/ie3/simona/agent/grid/GridAgentSetup2WSpec.scala +++ /dev/null @@ -1,125 +0,0 @@ -/* - * © 2020. TU Dortmund University, - * Institute of Energy Systems, Energy Efficiency and Energy Economics, - * Research group Distribution grid planning and operation - */ - -package edu.ie3.simona.agent.grid - -import com.typesafe.config.ConfigFactory -import com.typesafe.scalalogging.LazyLogging -import edu.ie3.datamodel.models.result.ResultEntity -import edu.ie3.simona.agent.EnvironmentRefs -import edu.ie3.simona.io.result.ResultSinkType -import edu.ie3.simona.sim.setup.SimonaStandaloneSetup -import edu.ie3.simona.test.common.input.TransformerInputTestData -import edu.ie3.simona.test.common.{ConfigTestData, TestKitWithShutdown} -import edu.ie3.simona.util.ResultFileHierarchy -import edu.ie3.simona.util.ResultFileHierarchy.ResultEntityPathConfig -import org.apache.pekko.actor.typed.scaladsl.adapter._ -import org.apache.pekko.actor.{ - Actor, - ActorIdentity, - ActorRef, - ActorSystem, - Identify, - Props, -} -import org.apache.pekko.testkit.ImplicitSender -import org.apache.pekko.util.Timeout -import org.scalatest.wordspec.AnyWordSpecLike - -import java.util.concurrent.TimeUnit -import scala.concurrent.Await -import scala.concurrent.duration.Duration - -class GridAgentSetup2WSpec - extends TestKitWithShutdown( - ActorSystem( - "GridAgentSetupSpec", - ConfigFactory - .parseString(""" - |pekko.loggers =["org.apache.pekko.event.slf4j.Slf4jLogger"] - |pekko.loglevel="OFF" - """.stripMargin), - ) - ) - with ImplicitSender - with AnyWordSpecLike - with TransformerInputTestData - with ConfigTestData - with LazyLogging { - - "The setup of grid agents" must { - "provide two grid agents on presence of a two winding transformer" in { - - import org.apache.pekko.pattern._ - implicit val timeout: Timeout = Timeout(1, TimeUnit.SECONDS) - - // in order to get an actor system we need a tmp actor that calls the corresponding method - Await.ready( - system.actorOf(Props(new Actor { - override def receive: Receive = { case "setup" => - val environmentRefs = EnvironmentRefs( - scheduler = self.toTyped, - runtimeEventListener = self, - primaryServiceProxy = self, - weather = ActorRef.noSender, - evDataService = None, - ) - - SimonaStandaloneSetup( - typesafeConfig, - ResultFileHierarchy( - "test/tmp", - "GridAgentSetup2WSpec", - ResultEntityPathConfig( - Set.empty[Class[_ <: ResultEntity]], - ResultSinkType( - simonaConfig.simona.output.sink, - simonaConfig.simona.simulationName, - ), - ), - ), - ).buildSubGridToActorRefMap( - gridContainer.getSubGridTopologyGraph, - context, - environmentRefs, - Seq.empty[ActorRef], - ) - sender() ! "done" - } - })) ? "setup", - Duration(1, TimeUnit.SECONDS), - ) - - val sel = system.actorSelection("user/**/GridAgent_*") - sel ! Identify(0) - - logger.debug("Waiting 500 ms to collect all responses") - val responses: Seq[ActorIdentity] = - receiveWhile( - max = Duration.create(500, "ms"), - idle = Duration.create(250, "ms"), - ) { case msg: ActorIdentity => - msg - } - logger.debug("All responses received. Evaluating...") - - responses.size should be(2) - - val regex = """GridAgent_\d*""".r - val expectedSenders = Vector("GridAgent_1", "GridAgent_2") - val actualSenders = responses - .collect { case actorId: ActorIdentity => - val actorRefString = actorId.getActorRef.toString - regex.findFirstIn(actorRefString) - } - .flatten - .sorted - .toVector - - actualSenders should be(expectedSenders) - } - } -} diff --git a/src/test/scala/edu/ie3/simona/agent/grid/GridAgentSetup3WSpec.scala b/src/test/scala/edu/ie3/simona/agent/grid/GridAgentSetup3WSpec.scala deleted file mode 100644 index 14d8540ecd..0000000000 --- a/src/test/scala/edu/ie3/simona/agent/grid/GridAgentSetup3WSpec.scala +++ /dev/null @@ -1,127 +0,0 @@ -/* - * © 2020. TU Dortmund University, - * Institute of Energy Systems, Energy Efficiency and Energy Economics, - * Research group Distribution grid planning and operation - */ - -package edu.ie3.simona.agent.grid - -import com.typesafe.config.ConfigFactory -import com.typesafe.scalalogging.LazyLogging -import edu.ie3.datamodel.models.result.ResultEntity -import edu.ie3.simona.agent.EnvironmentRefs -import edu.ie3.simona.io.result.ResultSinkType -import edu.ie3.simona.sim.setup.SimonaStandaloneSetup -import edu.ie3.simona.test.common.{ - ConfigTestData, - TestKitWithShutdown, - ThreeWindingTestData, -} -import edu.ie3.simona.util.ResultFileHierarchy -import edu.ie3.simona.util.ResultFileHierarchy.ResultEntityPathConfig -import org.apache.pekko.actor.typed.scaladsl.adapter._ -import org.apache.pekko.actor.{ - Actor, - ActorIdentity, - ActorRef, - ActorSystem, - Identify, - Props, -} -import org.apache.pekko.testkit.ImplicitSender -import org.apache.pekko.util.Timeout -import org.scalatest.wordspec.AnyWordSpecLike - -import java.util.concurrent.TimeUnit -import scala.concurrent.Await -import scala.concurrent.duration.Duration - -class GridAgentSetup3WSpec - extends TestKitWithShutdown( - ActorSystem( - "GridAgentSetupSpec", - ConfigFactory - .parseString(""" - |pekko.loggers =["org.apache.pekko.event.slf4j.Slf4jLogger"] - |pekko.loglevel="DEBUG" - """.stripMargin), - ) - ) - with ImplicitSender - with AnyWordSpecLike - with ThreeWindingTestData - with ConfigTestData - with LazyLogging { - - "The setup of grid agents" must { - "provide three grid agents on presence of a three winding transformer" in { - import org.apache.pekko.pattern._ - implicit val timeout: Timeout = Timeout(1, TimeUnit.SECONDS) - - // in order to get an actor system we need a tmp actor that calls the corresponding method - Await.ready( - system.actorOf(Props(new Actor { - override def receive: Receive = { case "setup" => - val environmentRefs = EnvironmentRefs( - scheduler = self.toTyped, - runtimeEventListener = self, - primaryServiceProxy = self, - weather = ActorRef.noSender, - evDataService = None, - ) - - SimonaStandaloneSetup( - typesafeConfig, - ResultFileHierarchy( - "test/tmp", - "GridAgentSetup3WSpec", - ResultEntityPathConfig( - Set.empty[Class[_ <: ResultEntity]], - ResultSinkType( - simonaConfig.simona.output.sink, - simonaConfig.simona.simulationName, - ), - ), - ), - ).buildSubGridToActorRefMap( - threeWindingTestGrid.getSubGridTopologyGraph, - context, - environmentRefs, - Seq.empty[ActorRef], - ) - sender() ! "done" - } - })) ? "setup", - Duration(1, TimeUnit.SECONDS), - ) - - val sel = system.actorSelection("user/**/GridAgent_*") - sel ! Identify(0) - - logger.debug("Waiting 500ms to collect all responses") - val responses: Seq[ActorIdentity] = - receiveWhile( - max = Duration.create(500, "ms"), - idle = Duration.create(250, "ms"), - ) { case msg: ActorIdentity => - msg - } - logger.debug("All responses received. Evaluating...") - - responses.size should be(3) - val regex = """GridAgent_\d*""".r - val expectedSenders = Vector("GridAgent_1", "GridAgent_2", "GridAgent_3") - val actualSenders = responses - .collect { case actorId: ActorIdentity => - val actorRefString = actorId.getActorRef.toString - regex.findFirstIn(actorRefString) - } - .flatten - .sorted - .toVector - - actualSenders should be(expectedSenders) - } - } - -} diff --git a/src/test/scala/edu/ie3/simona/agent/grid/GridAgentSetupSpec.scala b/src/test/scala/edu/ie3/simona/agent/grid/GridAgentSetupSpec.scala new file mode 100644 index 0000000000..5d7eea87c5 --- /dev/null +++ b/src/test/scala/edu/ie3/simona/agent/grid/GridAgentSetupSpec.scala @@ -0,0 +1,88 @@ +/* + * © 2020. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.agent.grid + +import edu.ie3.simona.agent.EnvironmentRefs +import edu.ie3.simona.sim.setup.SimonaStandaloneSetup +import edu.ie3.simona.test.common.input.TransformerInputTestData +import edu.ie3.simona.test.common.{ + ConfigTestData, + ThreeWindingTestData, + UnitSpec, +} +import edu.ie3.simona.util.ResultFileHierarchy +import org.apache.pekko.actor.testkit.typed.Effect.Spawned +import org.apache.pekko.actor.testkit.typed.scaladsl.BehaviorTestKit +import org.apache.pekko.actor.typed.scaladsl.Behaviors +import org.scalatestplus.mockito.MockitoSugar + +class GridAgentSetupSpec + extends UnitSpec + with MockitoSugar + with TransformerInputTestData + with ConfigTestData + with ThreeWindingTestData { + + "The setup of grid agents" must { + + "provide two grid agents on presence of a two winding transformer" in { + + val testKit = BehaviorTestKit(Behaviors.setup[AnyRef] { ctx => + SimonaStandaloneSetup( + typesafeConfig, + mock[ResultFileHierarchy], + ).buildSubGridToActorRefMap( + gridContainer.getSubGridTopologyGraph, + ctx, + mock[EnvironmentRefs], + Seq.empty, + ) + + Behaviors.stopped + }) + + // two actor should be spawned + testKit.expectEffectPF { case Spawned(_, actorName, _) => + actorName shouldBe "1" + } + + testKit.expectEffectPF { case Spawned(_, actorName, _) => + actorName shouldBe "2" + } + } + + "provide three grid agents on presence of a three winding transformer" in { + + val testKit = BehaviorTestKit(Behaviors.setup[AnyRef] { ctx => + SimonaStandaloneSetup( + typesafeConfig, + mock[ResultFileHierarchy], + ).buildSubGridToActorRefMap( + threeWindingTestGrid.getSubGridTopologyGraph, + ctx, + mock[EnvironmentRefs], + Seq.empty, + ) + + Behaviors.stopped + }) + + // three actor should be spawned + testKit.expectEffectPF { case Spawned(_, actorName, _) => + actorName shouldBe "1" + } + + testKit.expectEffectPF { case Spawned(_, actorName, _) => + actorName shouldBe "2" + } + + testKit.expectEffectPF { case Spawned(_, actorName, _) => + actorName shouldBe "3" + } + } + } +} 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 ce67a10c04..e273b06976 100644 --- a/src/test/scala/edu/ie3/simona/agent/grid/GridResultsSupportSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/grid/GridResultsSupportSpec.scala @@ -6,7 +6,6 @@ package edu.ie3.simona.agent.grid -import org.apache.pekko.event.{LoggingAdapter, NoLogging} import breeze.math.Complex import edu.ie3.datamodel.models.StandardUnits import edu.ie3.datamodel.models.input.connector.ConnectorPort @@ -61,7 +60,6 @@ class GridResultsSupportSpec with GridInputTestData with TableDrivenPropertyChecks { - override protected val log: LoggingAdapter = NoLogging implicit val currentTolerance: squants.electro.ElectricCurrent = Amperes(1e-6) implicit val angleTolerance: squants.Angle = Degrees(1e-6) diff --git a/src/test/scala/edu/ie3/simona/agent/grid/PowerFlowSupportSpec.scala b/src/test/scala/edu/ie3/simona/agent/grid/PowerFlowSupportSpec.scala index 3be73dc240..719930b288 100644 --- a/src/test/scala/edu/ie3/simona/agent/grid/PowerFlowSupportSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/grid/PowerFlowSupportSpec.scala @@ -6,16 +6,20 @@ package edu.ie3.simona.agent.grid -import org.apache.pekko.actor.ActorRef -import org.apache.pekko.event.{LoggingAdapter, NoLogging} import edu.ie3.powerflow.model.PowerFlowResult.SuccessFullPowerFlowResult.ValidNewtonRaphsonPFResult import edu.ie3.simona.model.grid.GridModel import edu.ie3.simona.ontology.messages.PowerMessage.ProvideGridPowerMessage.ExchangePower -import edu.ie3.simona.ontology.messages.VoltageMessage.ProvideSlackVoltageMessage.ExchangeVoltage +import VoltageMessage.ProvideSlackVoltageMessage.ExchangeVoltage import edu.ie3.simona.test.common.UnitSpec import edu.ie3.simona.test.common.model.grid.BasicGridWithSwitches import edu.ie3.util.quantities.QuantityUtils.RichQuantityDouble import edu.ie3.util.scala.quantities.Megavars +import org.apache.pekko.actor.testkit.typed.scaladsl.{ + ScalaTestWithActorTestKit, + TestProbe, +} +import org.apache.pekko.actor.typed.ActorRef +import org.slf4j.{Logger, LoggerFactory} import squants.electro.Kilovolts import squants.energy.Megawatts import tech.units.indriya.ComparableQuantity @@ -31,12 +35,15 @@ import javax.measure.quantity.Angle * order to comprehend the expected test results. */ class PowerFlowSupportSpec - extends UnitSpec + extends ScalaTestWithActorTestKit + with UnitSpec with BasicGridWithSwitches with PowerFlowSupport with GridResultsSupport { - override val log: LoggingAdapter = NoLogging + implicit val log: Logger = LoggerFactory.getLogger(this.getClass) + val actorRef: ActorRef[GridAgentMessage] = + TestProbe[GridAgentMessage]("mock_grid_agent").ref /** Setting voltage at slack node to 110 kV and introducing a load of 1 MW at * node 1 @@ -55,7 +62,7 @@ class PowerFlowSupportSpec ), nodeToReceivedPower = Map( node1.uuid -> Map( - ActorRef.noSender -> Some( + actorRef -> Some( ExchangePower( node1.uuid, Megawatts(1d), @@ -119,7 +126,10 @@ class PowerFlowSupportSpec ) val pfResult = - createResultModels(gridModel, sweepValueStore)(ZonedDateTime.now()) + createResultModels(gridModel, sweepValueStore)( + ZonedDateTime.now(), + log, + ) // left/top side segments should have similar currents val loadLinesLeft = @@ -220,7 +230,7 @@ class PowerFlowSupportSpec gridModel.gridComponents.nodes, gridModel.nodeUuidToIndexMap, ), - )(ZonedDateTime.now()) + )(ZonedDateTime.now(), log) // left/top side segments (lines that are adjacent to the open switch) should have no load val loadLinesLeft = @@ -305,7 +315,7 @@ class PowerFlowSupportSpec gridModel.gridComponents.nodes, gridModel.nodeUuidToIndexMap, ), - )(ZonedDateTime.now()) + )(ZonedDateTime.now(), log) // left/top side segments (lines that are adjacent to the open switch) should have load val expectedLoadLines = diff --git a/src/test/scala/edu/ie3/simona/agent/grid/ReceivedValuesStoreSpec.scala b/src/test/scala/edu/ie3/simona/agent/grid/ReceivedValuesStoreSpec.scala index 0902a476ce..378c08c1ef 100644 --- a/src/test/scala/edu/ie3/simona/agent/grid/ReceivedValuesStoreSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/grid/ReceivedValuesStoreSpec.scala @@ -6,53 +6,52 @@ package edu.ie3.simona.agent.grid -import java.util.UUID - -import org.apache.pekko.actor.{ActorRef, ActorSystem} -import org.apache.pekko.testkit.{TestKit, TestProbe} -import com.typesafe.config.ConfigFactory import edu.ie3.datamodel.graph.SubGridGate -import edu.ie3.simona.test.common.{TestKitWithShutdown, UnitSpec} +import edu.ie3.simona.agent.participant.ParticipantAgent.ParticipantMessage +import edu.ie3.simona.test.common.UnitSpec import edu.ie3.simona.test.common.model.grid.SubGridGateMokka +import org.apache.pekko.actor.testkit.typed.scaladsl.{ + ScalaTestWithActorTestKit, + TestProbe, +} +import org.apache.pekko.actor.typed.ActorRef + +import java.util.UUID class ReceivedValuesStoreSpec - extends TestKitWithShutdown( - ActorSystem( - "ReceivedValuesStoreSpec", - ConfigFactory - .parseString(""" - |pekko.loggers =["org.apache.pekko.event.slf4j.Slf4jLogger"] - |pekko.loglevel="OFF" - """.stripMargin), - ) - ) + extends ScalaTestWithActorTestKit with UnitSpec with SubGridGateMokka { // test actorRefs - val actorProbe1: TestProbe = TestProbe() - val actorProbe2: TestProbe = TestProbe() - val actorProbe3: TestProbe = TestProbe() + val participant1: TestProbe[ParticipantMessage] = + TestProbe[ParticipantMessage]() + val participant2: TestProbe[ParticipantMessage] = + TestProbe[ParticipantMessage]() + val participant3: TestProbe[ParticipantMessage] = + TestProbe[ParticipantMessage]() + val gridAgent: TestProbe[GridAgentMessage] = TestProbe[GridAgentMessage]() // test data used by almost all tests // / node to asset agents mapping - val nodeToAssetAgentsMap: Map[UUID, Set[ActorRef]] = Map( + val nodeToAssetAgentsMap: Map[UUID, Set[ActorRef[ParticipantMessage]]] = Map( UUID.fromString("dd9a5b54-94bb-4201-9108-2b1b7d689546") -> Set( - actorProbe1.ref + participant1.ref ), UUID.fromString("34e807f1-c62b-4968-b0f6-980ce500ff97") -> Set( - actorProbe2.ref + participant2.ref ), ) // / subnet gate mapping for inferior grids - val inferiorSubGridGateToActorRefMap: Map[SubGridGate, ActorRef] = Map( + val inferiorSubGridGateToActorRefMap + : Map[SubGridGate, ActorRef[GridAgentMessage]] = Map( build2wSubGridGate( UUID.fromString("5cd55ab5-a7d2-499f-a25f-6dbc3845c5e8"), 1, UUID.fromString("1676360a-c7c4-43a9-a667-90ddfe8a18e6"), 2, - ) -> actorProbe3.ref + ) -> gridAgent.ref ) // / superior grid nodeUuid vector @@ -64,8 +63,10 @@ class ReceivedValuesStoreSpec "initialize an empty store correctly when everything is empty" in { - val nodeToAssetAgentsMap = Map.empty[UUID, Set[ActorRef]] - val inferiorSubGridGateToActorRefMap = Map.empty[SubGridGate, ActorRef] + val nodeToAssetAgentsMap = + Map.empty[UUID, Set[ActorRef[ParticipantMessage]]] + val inferiorSubGridGateToActorRefMap = + Map.empty[SubGridGate, ActorRef[GridAgentMessage]] val superiorGridNodeUuids = Vector.empty[UUID] val receivedValuesStore = @@ -92,13 +93,13 @@ class ReceivedValuesStoreSpec receivedValuesStore.nodeToReceivedPower.size shouldBe 3 receivedValuesStore.nodeToReceivedPower( UUID.fromString("dd9a5b54-94bb-4201-9108-2b1b7d689546") - ) shouldBe Map(actorProbe1.ref -> None) + ) shouldBe Map(participant1.ref -> None) receivedValuesStore.nodeToReceivedPower( UUID.fromString("34e807f1-c62b-4968-b0f6-980ce500ff97") - ) shouldBe Map(actorProbe2.ref -> None) + ) shouldBe Map(participant2.ref -> None) receivedValuesStore.nodeToReceivedPower( UUID.fromString("5cd55ab5-a7d2-499f-a25f-6dbc3845c5e8") - ) shouldBe Map(actorProbe3.ref -> None) + ) shouldBe Map(gridAgent.ref -> None) receivedValuesStore.nodeToReceivedSlackVoltage.size shouldBe 1 receivedValuesStore.nodeToReceivedSlackVoltage( @@ -112,15 +113,16 @@ class ReceivedValuesStoreSpec val nodeToAssetAgentsMap = Map( UUID.fromString("dd9a5b54-94bb-4201-9108-2b1b7d689546") -> Set( - actorProbe1.ref + participant1.ref ), UUID.fromString("34e807f1-c62b-4968-b0f6-980ce500ff97") -> Set( - actorProbe2.ref, - actorProbe3.ref, + participant2.ref, + participant3.ref, ), ) - val inferiorSubGridGateToActorRefMap = Map.empty[SubGridGate, ActorRef] + val inferiorSubGridGateToActorRefMap = + Map.empty[SubGridGate, ActorRef[GridAgentMessage]] val superiorGridNodeUuids = Vector.empty[UUID] val receivedValuesStore = @@ -135,12 +137,12 @@ class ReceivedValuesStoreSpec receivedValuesStore.nodeToReceivedPower.size shouldBe 2 receivedValuesStore.nodeToReceivedPower( UUID.fromString("dd9a5b54-94bb-4201-9108-2b1b7d689546") - ) shouldBe Map(actorProbe1.ref -> None) + ) shouldBe Map(participant1.ref -> None) receivedValuesStore.nodeToReceivedPower( UUID.fromString("34e807f1-c62b-4968-b0f6-980ce500ff97") ) shouldBe Map( - actorProbe2.ref -> None, - actorProbe3.ref -> None, + participant2.ref -> None, + participant3.ref -> None, ) } @@ -161,20 +163,22 @@ class ReceivedValuesStoreSpec receivedValuesStore.nodeToReceivedPower.size shouldBe 3 receivedValuesStore.nodeToReceivedPower( UUID.fromString("dd9a5b54-94bb-4201-9108-2b1b7d689546") - ) shouldBe Map(actorProbe1.ref -> None) + ) shouldBe Map(participant1.ref -> None) receivedValuesStore.nodeToReceivedPower( UUID.fromString("34e807f1-c62b-4968-b0f6-980ce500ff97") - ) shouldBe Map(actorProbe2.ref -> None) + ) shouldBe Map(participant2.ref -> None) receivedValuesStore.nodeToReceivedPower( UUID.fromString("5cd55ab5-a7d2-499f-a25f-6dbc3845c5e8") - ) shouldBe Map(actorProbe3.ref -> None) + ) shouldBe Map(gridAgent.ref -> None) } "initialize an empty store correctly when only information on the superior grid slack nodes are provided" in { - val nodeToAssetAgentsMap = Map.empty[UUID, Set[ActorRef]] - val inferiorSubGridGateToActorRefMap = Map.empty[SubGridGate, ActorRef] + val nodeToAssetAgentsMap = + Map.empty[UUID, Set[ActorRef[ParticipantMessage]]] + val inferiorSubGridGateToActorRefMap = + Map.empty[SubGridGate, ActorRef[GridAgentMessage]] val superiorGridNodeUuids = Vector( UUID.fromString("baded8c4-b703-4316-b62f-75ffe09c9843"), @@ -202,7 +206,8 @@ class ReceivedValuesStoreSpec "initialize an empty store correctly when only an invalid mapping for asset agents with duplicates is provided" in { - val inferiorSubGridGateToActorRefMap = Map.empty[SubGridGate, ActorRef] + val inferiorSubGridGateToActorRefMap = + Map.empty[SubGridGate, ActorRef[GridAgentMessage]] val superiorGridNodeUuids = Vector.empty[UUID] val receivedValuesStore = @@ -217,10 +222,10 @@ class ReceivedValuesStoreSpec receivedValuesStore.nodeToReceivedPower.size shouldBe 2 receivedValuesStore.nodeToReceivedPower( UUID.fromString("dd9a5b54-94bb-4201-9108-2b1b7d689546") - ) shouldBe Map(actorProbe1.ref -> None) + ) shouldBe Map(participant1.ref -> None) receivedValuesStore.nodeToReceivedPower( UUID.fromString("34e807f1-c62b-4968-b0f6-980ce500ff97") - ) shouldBe Map(actorProbe2.ref -> None) + ) shouldBe Map(participant2.ref -> None) } diff --git a/src/test/scala/edu/ie3/simona/agent/participant/ParticipantAgent2ListenerSpec.scala b/src/test/scala/edu/ie3/simona/agent/participant/ParticipantAgent2ListenerSpec.scala index fc5d0f66f5..2db9a91fc0 100644 --- a/src/test/scala/edu/ie3/simona/agent/participant/ParticipantAgent2ListenerSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/participant/ParticipantAgent2ListenerSpec.scala @@ -9,7 +9,7 @@ package edu.ie3.simona.agent.participant import com.typesafe.config.ConfigFactory import edu.ie3.datamodel.models.input.system.SystemParticipantInput import edu.ie3.datamodel.models.result.system.SystemParticipantResult -import edu.ie3.simona.agent.grid.GridAgent.FinishGridSimulationTrigger +import edu.ie3.simona.agent.participant.ParticipantAgent.FinishParticipantSimulation import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ApparentPower import edu.ie3.simona.agent.participant.statedata.ParticipantStateData.ParticipantInitializeStateData import edu.ie3.simona.config.SimonaConfig @@ -241,7 +241,7 @@ class ParticipantAgent2ListenerSpec case unknownMsg => fail(s"Received unexpected message: $unknownMsg") } - scheduler.send(mockAgent, FinishGridSimulationTrigger(3000L)) + scheduler.send(mockAgent, FinishParticipantSimulation(3000L)) /* Wait for the result event (this is the event listener) */ logger.warn( @@ -303,7 +303,7 @@ class ParticipantAgent2ListenerSpec case unknownMsg => fail(s"Received unexpected message: $unknownMsg") } - scheduler.send(mockAgent, FinishGridSimulationTrigger(3000L)) + scheduler.send(mockAgent, FinishParticipantSimulation(3000L)) /* Make sure nothing else is sent */ expectNoMessage(noReceiveTimeOut.duration) diff --git a/src/test/scala/edu/ie3/simona/config/ConfigFailFastSpec.scala b/src/test/scala/edu/ie3/simona/config/ConfigFailFastSpec.scala index a9eacda69a..3b587fac30 100644 --- a/src/test/scala/edu/ie3/simona/config/ConfigFailFastSpec.scala +++ b/src/test/scala/edu/ie3/simona/config/ConfigFailFastSpec.scala @@ -12,7 +12,11 @@ import edu.ie3.simona.config.SimonaConfig.Simona.Output.Sink import edu.ie3.simona.config.SimonaConfig.Simona.Output.Sink.{Csv, InfluxDb1x} import edu.ie3.simona.config.SimonaConfig.Simona.Powerflow.Newtonraphson import edu.ie3.simona.config.SimonaConfig.Simona.{Powerflow, Time} -import edu.ie3.simona.config.SimonaConfig.{BaseCsvParams, ResultKafkaParams} +import edu.ie3.simona.config.SimonaConfig.{ + BaseCsvParams, + ResultKafkaParams, + TransformerControlGroup, +} import edu.ie3.simona.exceptions.InvalidConfigParameterException import edu.ie3.simona.test.common.{ConfigTestData, UnitSpec} import edu.ie3.simona.util.ConfigUtil.{CsvConfigUtil, NotifierIdentifier} @@ -949,7 +953,78 @@ class ConfigFailFastSpec extends UnitSpec with ConfigTestData { ) }.getMessage shouldBe "The weather data scheme 'this won't work' is not supported. Supported schemes:\n\ticon\n\tcosmo" } + } + + "checking the transformer control groups" should { + val checkTransformerControl = + PrivateMethod[Unit](Symbol("checkTransformerControl")) + + "throw an exception, if the measurements are empty" in { + val dut = TransformerControlGroup( + List.empty, + List("a16cf7ca-8bbf-46e1-a74e-ffa6513c89a8"), + 1.02, + 0.98, + ) + + intercept[InvalidConfigParameterException] { + ConfigFailFast invokePrivate checkTransformerControl(dut) + }.getMessage shouldBe s"A transformer control group (${dut.toString}) cannot have no measurements assigned." + } + + "throw an exception, if the transformers are empty" in { + val dut = TransformerControlGroup( + List("6888c53a-7629-4563-ac8e-840f80b03106"), + List.empty, + 1.02, + 0.98, + ) + intercept[InvalidConfigParameterException] { + ConfigFailFast invokePrivate checkTransformerControl(dut) + }.getMessage shouldBe s"A transformer control group (${dut.toString}) cannot have no transformers assigned." + } + + "throw an exception, if vMax is smaller than vMin" in { + val dut = TransformerControlGroup( + List("6888c53a-7629-4563-ac8e-840f80b03106"), + List("a16cf7ca-8bbf-46e1-a74e-ffa6513c89a8"), + 0.98, + 1.02, + ) + + intercept[InvalidConfigParameterException] { + ConfigFailFast invokePrivate checkTransformerControl(dut) + }.getMessage shouldBe s"The minimum permissible voltage magnitude of a transformer control group (${dut.toString}) must be smaller than the maximum permissible voltage magnitude." + } + + "throw Exception if vMin is lower than -20% of nominal Voltage" in { + val dut = TransformerControlGroup( + List("6888c53a-7629-4563-ac8e-840f80b03106"), + List("a16cf7ca-8bbf-46e1-a74e-ffa6513c89a8"), + 1.02, + 0.79, + ) + + intercept[InvalidConfigParameterException] { + ConfigFailFast invokePrivate checkTransformerControl(dut) + }.getMessage shouldBe s"A control group (${dut.toString}) which control boundaries exceed the limit of +- 20% of nominal voltage! This may be caused " + + "by invalid parametrization of one control groups where vMin is lower than the lower boundary (0.8 of nominal Voltage)!" + } + + "throw Exception if vMax is higher than +20% of nominal Voltage" in { + val dut = TransformerControlGroup( + List("6888c53a-7629-4563-ac8e-840f80b03106"), + List("a16cf7ca-8bbf-46e1-a74e-ffa6513c89a8"), + 1.21, + 0.98, + ) + + intercept[InvalidConfigParameterException] { + ConfigFailFast invokePrivate checkTransformerControl(dut) + }.getMessage shouldBe s"A control group (${dut.toString}) which control boundaries exceed the limit of +- 20% of nominal voltage! This may be caused " + + "by invalid parametrization of one control groups where vMax is higher than the upper boundary (1.2 of nominal Voltage)!" + } } } diff --git a/src/test/scala/edu/ie3/simona/event/NotifierSpec.scala b/src/test/scala/edu/ie3/simona/event/NotifierSpec.scala index 654e6ff8d4..23c9062a79 100644 --- a/src/test/scala/edu/ie3/simona/event/NotifierSpec.scala +++ b/src/test/scala/edu/ie3/simona/event/NotifierSpec.scala @@ -7,7 +7,13 @@ package edu.ie3.simona.event import java.util.{Calendar, Date} -import org.apache.pekko.actor.{ActorLogging, ActorRef, ActorSystem, Props} +import org.apache.pekko.actor.{ + Actor, + ActorLogging, + ActorRef, + ActorSystem, + Props, +} import org.apache.pekko.testkit.ImplicitSender import org.apache.pekko.util.Timeout import com.typesafe.config.ConfigFactory @@ -39,6 +45,7 @@ class NotifierSpec // test listenerActor class NotifierActor(override val listener: Iterable[ActorRef]) extends Notifier + with Actor with ActorLogging { override def preStart(): Unit = { log.debug(s"{} started!", self) diff --git a/src/test/scala/edu/ie3/simona/model/control/TransformerControlGroupModelSpec.scala b/src/test/scala/edu/ie3/simona/model/control/TransformerControlGroupModelSpec.scala new file mode 100644 index 0000000000..67d762dca7 --- /dev/null +++ b/src/test/scala/edu/ie3/simona/model/control/TransformerControlGroupModelSpec.scala @@ -0,0 +1,131 @@ +/* + * © 2021. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.model.control + +import breeze.linalg.DenseMatrix +import breeze.math.Complex +import edu.ie3.powerflow.model.NodeData.StateData +import edu.ie3.powerflow.model.PowerFlowResult.SuccessFullPowerFlowResult.ValidNewtonRaphsonPFResult +import edu.ie3.powerflow.model.enums.NodeType +import edu.ie3.simona.model.control.TransformerControlGroupModel.RegulationCriterion +import edu.ie3.simona.test.common.UnitSpec +import edu.ie3.simona.test.matchers.QuantityMatchers +import squants.{Dimensionless, Each} + +import java.util.UUID + +class TransformerControlGroupModelSpec extends UnitSpec with QuantityMatchers { + + implicit val tolerance: Dimensionless = Each(1e-10) + + "Checking the function of transformer control groups" should { + val regulationFunction = + PrivateMethod[RegulationCriterion](Symbol("regulationFunction")) + + val regulationCriterion = + TransformerControlGroupModel invokePrivate regulationFunction(1.1, 0.9) + + val dut = TransformerControlGroupModel( + Set( + UUID.fromString( + "d4d650be-87b7-4cf6-be7f-03f0bbcde3e3" + ), + UUID.fromString( + "08b8d2ca-993d-45cd-9456-f009ecb47bc0" + ), + UUID.fromString( + "324f49e5-1c35-4c49-afb1-3cf41696bf93" + ), + ), + regulationCriterion, + ) + + val uuidToIndex = Map( + UUID.fromString( + "d4d650be-87b7-4cf6-be7f-03f0bbcde3e3" + ) -> 0, + UUID.fromString( + "08b8d2ca-993d-45cd-9456-f009ecb47bc0" + ) -> 1, + UUID.fromString( + "324f49e5-1c35-4c49-afb1-3cf41696bf93" + ) -> 2, + ) + + "return no regulation need, if everything is fine" in { + val result = ValidNewtonRaphsonPFResult( + 0, + Array( + StateData(0, NodeType.SL, Complex.one, Complex.zero), + StateData(1, NodeType.PQ, Complex.one, Complex.zero), + StateData(2, NodeType.PQ, Complex.one, Complex.zero), + ), + DenseMatrix.zeros(1, 1), + ) + + val actual = dut.determineRegulationNeed(result, uuidToIndex) + + actual shouldBe None + } + + "return no regulation need, if requests are contradictory" in { + val result = ValidNewtonRaphsonPFResult( + 0, + Array( + StateData(0, NodeType.SL, Complex.one, Complex.zero), + StateData(1, NodeType.PQ, Complex.one * 0.88, Complex.zero), + StateData(2, NodeType.PQ, Complex.one * 1.11, Complex.zero), + ), + DenseMatrix.zeros(1, 1), + ) + + val actual = dut.determineRegulationNeed(result, uuidToIndex) + + actual shouldBe None + } + + "return the biggest positive regulation need" in { + val result = ValidNewtonRaphsonPFResult( + 0, + Array( + StateData(0, NodeType.SL, Complex.one, Complex.zero), + StateData(1, NodeType.PQ, Complex.one * 0.85, Complex.zero), + StateData(2, NodeType.PQ, Complex.one * 0.88, Complex.zero), + ), + DenseMatrix.zeros(1, 1), + ) + + val actual = dut.determineRegulationNeed(result, uuidToIndex) + + actual match { + case Some(regulationNeed) => + regulationNeed should approximate(Each(0.05)) + case None => fail("Did expect to receive a regulation need.") + } + } + + "return the biggest negative regulation need" in { + val result = ValidNewtonRaphsonPFResult( + 0, + Array( + StateData(0, NodeType.SL, Complex.one, Complex.zero), + StateData(1, NodeType.PQ, Complex.one * 1.15, Complex.zero), + StateData(2, NodeType.PQ, Complex.one * 1.11, Complex.zero), + ), + DenseMatrix.zeros(1, 1), + ) + + val actual = dut.determineRegulationNeed(result, uuidToIndex) + + actual match { + case Some(regulationNeed) => + regulationNeed should approximate(Each(-0.05)) + case None => fail("Did expect to receive a regulation need.") + } + } + } +} 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 7a86895f74..53165fc018 100644 --- a/src/test/scala/edu/ie3/simona/model/grid/GridSpec.scala +++ b/src/test/scala/edu/ie3/simona/model/grid/GridSpec.scala @@ -10,7 +10,10 @@ import breeze.linalg.DenseMatrix import breeze.math.Complex import breeze.numerics.abs import edu.ie3.datamodel.exceptions.InvalidGridException +import edu.ie3.datamodel.models.input.MeasurementUnitInput +import edu.ie3.datamodel.models.voltagelevels.GermanVoltageLevelUtils import edu.ie3.simona.exceptions.GridInconsistencyException +import edu.ie3.simona.model.control.{GridControls, TransformerControlGroupModel} import edu.ie3.simona.model.grid.GridModel.GridComponents import edu.ie3.simona.test.common.input.{GridInputTestData, LineInputTestData} import edu.ie3.simona.test.common.model.grid.{ @@ -18,11 +21,16 @@ import edu.ie3.simona.test.common.model.grid.{ BasicGridWithSwitches, FiveLinesWithNodes, } -import edu.ie3.simona.test.common.{DefaultTestData, UnitSpec} +import edu.ie3.simona.test.common.{ConfigTestData, DefaultTestData, UnitSpec} +import testutils.TestObjectFactory import java.util.UUID -class GridSpec extends UnitSpec with LineInputTestData with DefaultTestData { +class GridSpec + extends UnitSpec + with LineInputTestData + with DefaultTestData + with ConfigTestData { private val _printAdmittanceMatrixOnMismatch : (DenseMatrix[Complex], DenseMatrix[Complex]) => Unit = { @@ -205,6 +213,7 @@ class GridSpec extends UnitSpec with LineInputTestData with DefaultTestData { Set.empty[Transformer3wModel], switches, ), + GridControls.empty, ) // get the private method for validation val validateConnectivity: PrivateMethod[Unit] = @@ -233,6 +242,7 @@ class GridSpec extends UnitSpec with LineInputTestData with DefaultTestData { Set.empty[Transformer3wModel], Set.empty[SwitchModel], ), + GridControls.empty, ) // get the private method for validation @@ -278,6 +288,7 @@ class GridSpec extends UnitSpec with LineInputTestData with DefaultTestData { Set.empty[Transformer3wModel], switches, ), + GridControls.empty, ) // get the private method for validation @@ -382,6 +393,7 @@ class GridSpec extends UnitSpec with LineInputTestData with DefaultTestData { Set.empty[Transformer3wModel], switches, ), + GridControls.empty, ) // update the uuidToIndexMap @@ -433,6 +445,7 @@ class GridSpec extends UnitSpec with LineInputTestData with DefaultTestData { Set.empty[Transformer3wModel], Set.empty[SwitchModel], ), + GridControls.empty, ) // update the uuidToIndexMap @@ -453,7 +466,88 @@ class GridSpec extends UnitSpec with LineInputTestData with DefaultTestData { gridModel.nodeUuidToIndexMap.keySet.toVector.sorted should be( nodes.map(node => node.uuid).toVector.sorted ) + } + } + + "build correct transformer control models" should { + /* Testing of distinct transformer control group building can be found in the spec for transformer control groups */ + "determine node uuids correctly" in { + val determineNodeUuids = + PrivateMethod[Set[UUID]](Symbol("determineNodeUuids")) + + val node0 = TestObjectFactory.buildNodeInput( + false, + GermanVoltageLevelUtils.MV_10KV, + 1, + ) + val node1 = TestObjectFactory.buildNodeInput( + false, + GermanVoltageLevelUtils.MV_10KV, + 1, + ) + val node2 = TestObjectFactory.buildNodeInput( + false, + GermanVoltageLevelUtils.MV_10KV, + 1, + ) + val node3 = TestObjectFactory.buildNodeInput( + false, + GermanVoltageLevelUtils.MV_10KV, + 1, + ) + + val measurementUnits = Set( + new MeasurementUnitInput( + UUID.fromString("3ad9e076-c02b-4cf9-8720-18e2bb541ede"), + "measurement_unit_0", + node0, + true, + false, + false, + false, + ), + new MeasurementUnitInput( + UUID.fromString("ab66fbb0-ece1-44b9-9341-86a884233ec4"), + "measurement_unit_1", + node1, + true, + false, + false, + false, + ), + new MeasurementUnitInput( + UUID.fromString("93b4d0d8-cc67-41f5-9d5c-1cd6dbb2e70d"), + "measurement_unit_2", + node2, + true, + false, + false, + false, + ), + new MeasurementUnitInput( + UUID.fromString("8e84eb8a-2940-4900-b0ce-0eeb6bca8bae"), + "measurement_unit_3", + node3, + false, + false, + false, + false, + ), + ) + val selectedMeasurements = Set( + "ab66fbb0-ece1-44b9-9341-86a884233ec4", + "93b4d0d8-cc67-41f5-9d5c-1cd6dbb2e70d", + "8e84eb8a-2940-4900-b0ce-0eeb6bca8bae", + ) + val expectedUuids = Set(node1, node2).map(_.getUuid) + + val actual = + TransformerControlGroupModel invokePrivate determineNodeUuids( + measurementUnits, + selectedMeasurements, + ) + actual should contain theSameElementsAs expectedUuids } } @@ -464,6 +558,7 @@ class GridSpec extends UnitSpec with LineInputTestData with DefaultTestData { gridInputModelTestDataRefSystem, defaultSimulationStart, defaultSimulationEnd, + simonaConfig, ) } diff --git a/src/test/scala/edu/ie3/simona/model/grid/TransformerModelSpec.scala b/src/test/scala/edu/ie3/simona/model/grid/TransformerModelSpec.scala index d7fd3025ff..880ca12116 100644 --- a/src/test/scala/edu/ie3/simona/model/grid/TransformerModelSpec.scala +++ b/src/test/scala/edu/ie3/simona/model/grid/TransformerModelSpec.scala @@ -18,7 +18,7 @@ import edu.ie3.powerflow.model.NodeData.{PresetData, StateData} import edu.ie3.powerflow.model.StartData.WithForcedStartVoltages import edu.ie3.powerflow.model.enums.NodeType import edu.ie3.powerflow.model.{NodeData, PowerFlowResult} -import edu.ie3.simona.test.common.UnitSpec +import edu.ie3.simona.test.common.{ConfigTestData, UnitSpec} import edu.ie3.simona.test.common.model.grid.{ TapTestData, TransformerTestData, @@ -36,7 +36,10 @@ import java.time.ZonedDateTime import java.time.temporal.ChronoUnit import java.util.UUID -class TransformerModelSpec extends UnitSpec with TableDrivenPropertyChecks { +class TransformerModelSpec + extends UnitSpec + with TableDrivenPropertyChecks + with ConfigTestData { val quantityTolerance: Double = 1e-5 val testingTolerancePf = 1e-9 implicit val electricCurrentTolerance: squants.electro.ElectricCurrent = @@ -378,6 +381,7 @@ class TransformerModelSpec extends UnitSpec with TableDrivenPropertyChecks { refSystem, defaultSimulationStart, defaultSimulationEnd, + simonaConfig, ) gridModel.gridComponents.transformers diff --git a/src/test/scala/edu/ie3/simona/sim/SimonaSimSpec.scala b/src/test/scala/edu/ie3/simona/sim/SimonaSimSpec.scala index e6dd11b28b..bbeb1c251c 100644 --- a/src/test/scala/edu/ie3/simona/sim/SimonaSimSpec.scala +++ b/src/test/scala/edu/ie3/simona/sim/SimonaSimSpec.scala @@ -7,13 +7,14 @@ package edu.ie3.simona.sim import edu.ie3.simona.agent.EnvironmentRefs +import edu.ie3.simona.agent.grid.GridAgentMessage import edu.ie3.simona.api.ExtSimAdapter +import edu.ie3.simona.event.{ResultEvent, RuntimeEvent} import edu.ie3.simona.event.listener.{ DelayedStopHelper, ResultEventListener, RuntimeEventListener, } -import edu.ie3.simona.event.{ResultEvent, RuntimeEvent} import edu.ie3.simona.main.RunSimona.SimonaEnded import edu.ie3.simona.ontology.messages.SchedulerMessage import edu.ie3.simona.ontology.messages.SchedulerMessage.Completion @@ -23,7 +24,6 @@ import edu.ie3.simona.sim.SimonaSimSpec._ import edu.ie3.simona.sim.setup.{ExtSimSetupData, SimonaSetup} import edu.ie3.simona.test.common.UnitSpec import org.apache.pekko.actor.testkit.typed.scaladsl.{ - LogCapturing, ScalaTestWithActorTestKit, TestProbe, } @@ -444,8 +444,8 @@ object SimonaSimSpec { override def gridAgents( context: ActorContext[_], environmentRefs: EnvironmentRefs, - systemParticipantListener: Seq[ActorRef[ResultEvent]], - ): Iterable[ClassicRef] = Iterable.empty + resultEventListeners: Seq[ActorRef[ResultEvent]], + ): Iterable[ActorRef[GridAgentMessage]] = Iterable.empty override def extSimulations( context: ActorContext[_], diff --git a/src/test/scala/edu/ie3/simona/sim/setup/SetupHelperSpec.scala b/src/test/scala/edu/ie3/simona/sim/setup/SetupHelperSpec.scala index d0bbcf39b3..1a933a3bd9 100644 --- a/src/test/scala/edu/ie3/simona/sim/setup/SetupHelperSpec.scala +++ b/src/test/scala/edu/ie3/simona/sim/setup/SetupHelperSpec.scala @@ -7,8 +7,7 @@ package edu.ie3.simona.sim.setup import java.util.UUID - -import org.apache.pekko.actor.ActorRef +import org.apache.pekko.actor.typed.ActorRef import org.apache.pekko.testkit.TestException import edu.ie3.datamodel.models.input.MeasurementUnitInput import edu.ie3.datamodel.models.input.connector.{ @@ -19,16 +18,27 @@ import edu.ie3.datamodel.models.input.container.{ JointGridContainer, RawGridElements, } +import edu.ie3.simona.agent.grid.GridAgentMessage import edu.ie3.simona.test.common.UnitSpec import edu.ie3.simona.test.common.input.GridInputTestData +import org.apache.pekko.actor.testkit.typed.scaladsl.{ + ScalaTestWithActorTestKit, + TestProbe, +} import scala.jdk.CollectionConverters._ -class SetupHelperSpec extends UnitSpec with GridInputTestData { +class SetupHelperSpec + extends ScalaTestWithActorTestKit + with UnitSpec + with GridInputTestData { private final object SetupHelperInstance extends SetupHelper "A setup helper" should { + val actorRef: ActorRef[GridAgentMessage] = + TestProbe[GridAgentMessage]("mock_grid_agent").ref + "reduce multiple SubGridGates between the same superior and inferior nodes to one unique SubGridGate" in { // build dummy grid with two transformers between the same nodes based on the basic grid input test data @@ -83,7 +93,7 @@ class SetupHelperSpec extends UnitSpec with GridInputTestData { ) val subGridToActorRefMap = - Map(1 -> ActorRef.noSender, 100 -> ActorRef.noSender) + Map(1 -> actorRef, 100 -> actorRef) // subGrid gates should be the same for this case gridModel.getSubGridTopologyGraph.edgesOf( diff --git a/src/test/scala/edu/ie3/simona/sim/setup/SimonaSetupSpec.scala b/src/test/scala/edu/ie3/simona/sim/setup/SimonaSetupSpec.scala index 552ce6ac46..67a7a9f6b8 100644 --- a/src/test/scala/edu/ie3/simona/sim/setup/SimonaSetupSpec.scala +++ b/src/test/scala/edu/ie3/simona/sim/setup/SimonaSetupSpec.scala @@ -12,6 +12,7 @@ import edu.ie3.datamodel.models.input.connector.{ Transformer3WInput, } import edu.ie3.simona.agent.EnvironmentRefs +import edu.ie3.simona.agent.grid.GridAgentMessage import edu.ie3.simona.event.listener.{ResultEventListener, RuntimeEventListener} import edu.ie3.simona.event.{ResultEvent, RuntimeEvent} import edu.ie3.simona.ontology.messages.SchedulerMessage @@ -19,8 +20,9 @@ import edu.ie3.simona.scheduler.TimeAdvancer import edu.ie3.simona.sim.SimonaSim import edu.ie3.simona.test.common.UnitSpec import edu.ie3.simona.test.common.model.grid.SubGridGateMokka -import org.apache.pekko.actor.typed.scaladsl -import org.apache.pekko.actor.{ActorRef, typed} +import org.apache.pekko.actor.typed.ActorRef +import org.apache.pekko.actor.typed.scaladsl.ActorContext +import org.apache.pekko.actor.{ActorRef => ClassicRef} import java.util.UUID @@ -29,58 +31,57 @@ class SimonaSetupSpec extends UnitSpec with SimonaSetup with SubGridGateMokka { override val args: Array[String] = Array.empty[String] override def runtimeEventListener( - context: scaladsl.ActorContext[_] - ): typed.ActorRef[RuntimeEventListener.Request] = // todo typed + context: ActorContext[_] + ): ActorRef[RuntimeEventListener.Request] = throw new NotImplementedException( "This is a dummy setup" ) override def resultEventListener( - context: scaladsl.ActorContext[_], + context: ActorContext[_], extSimulationData: ExtSimSetupData, - ): Seq[typed.ActorRef[ResultEventListener.Request]] = + ): Seq[ActorRef[ResultEventListener.Request]] = throw new NotImplementedException("This is a dummy setup") override def primaryServiceProxy( - context: scaladsl.ActorContext[_], - scheduler: typed.ActorRef[SchedulerMessage], + context: ActorContext[_], + scheduler: ActorRef[SchedulerMessage], extSimSetupData: ExtSimSetupData, - ): ActorRef = throw new NotImplementedException("This is a dummy setup") + ): ClassicRef = throw new NotImplementedException("This is a dummy setup") override def weatherService( - context: scaladsl.ActorContext[_], - scheduler: typed.ActorRef[SchedulerMessage], - ): ActorRef = throw new NotImplementedException("This is a dummy setup") + context: ActorContext[_], + scheduler: ActorRef[SchedulerMessage], + ): ClassicRef = throw new NotImplementedException("This is a dummy setup") override def extSimulations( - context: scaladsl.ActorContext[_], - scheduler: typed.ActorRef[SchedulerMessage], + context: ActorContext[_], + scheduler: ActorRef[SchedulerMessage], ): ExtSimSetupData = throw new NotImplementedException( "This is a dummy setup" ) override def timeAdvancer( - context: scaladsl.ActorContext[_], - simulation: typed.ActorRef[SimonaSim.SimulationEnded.type], - runtimeEventListener: typed.ActorRef[RuntimeEvent], - ): typed.ActorRef[TimeAdvancer.Request] = throw new NotImplementedException( + context: ActorContext[_], + simulation: ActorRef[SimonaSim.SimulationEnded.type], + runtimeEventListener: ActorRef[RuntimeEvent], + ): ActorRef[TimeAdvancer.Request] = throw new NotImplementedException( "This is a dummy setup" ) override def scheduler( - context: scaladsl.ActorContext[_], - timeAdvancer: typed.ActorRef[TimeAdvancer.Request], - ): typed.ActorRef[SchedulerMessage] = throw new NotImplementedException( + context: ActorContext[_], + timeAdvancer: ActorRef[TimeAdvancer.Request], + ): ActorRef[SchedulerMessage] = throw new NotImplementedException( "This is a dummy setup" ) override def gridAgents( - context: scaladsl.ActorContext[_], + context: ActorContext[_], environmentRefs: EnvironmentRefs, - resultEventListeners: Seq[typed.ActorRef[ResultEvent]], - ): Iterable[ActorRef] = throw new NotImplementedException( - "This is a dummy setup" - ) + resultEventListeners: Seq[ActorRef[ResultEvent]], + ): Iterable[ActorRef[GridAgentMessage]] = + throw new NotImplementedException("This is a dummy setup") "Attempting to modify a sub grid gate" should { val nodeAUuid = UUID.randomUUID() 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 f8e5bb2a03..bd709daa01 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 @@ -6,6 +6,7 @@ package edu.ie3.simona.test.common.model.grid +import edu.ie3.simona.model.control.GridControls import edu.ie3.simona.model.grid.GridModel.GridComponents import edu.ie3.simona.model.grid.{ GridModel, @@ -230,6 +231,7 @@ trait BasicGridWithSwitches extends BasicGrid { Set.empty[Transformer3wModel], gridSwitches, ), + GridControls.empty, ) }