diff --git a/CHANGELOG.md b/CHANGELOG.md index da2d0b3dec..0584926516 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Introducing new ParticipantAgent and ParticipantModel [#1134](https://github.com/ie3-institute/simona/issues/1134) - Using new `ParticipantAgent.Request` messages everywhere [#1195](https://github.com/ie3-institute/simona/issues/1195) - Implementing consideration of `NotifierConfig` into new participant [#1200](https://github.com/ie3-institute/simona/issues/1200) +- Replace `LoadModel` with its new implementation [#1150](https://github.com/ie3-institute/simona/issues/1150) +- Replace `FixedFeedInModel` with its new implementation [#1152](https://github.com/ie3-institute/simona/issues/1152) - Added congestion detection [#1186](https://github.com/ie3-institute/simona/issues/1186) ### Changed @@ -137,6 +139,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Refactor `RuntimeConfig` [#1172](https://github.com/ie3-institute/simona/issues/1172) - Renamed some methods and variables within `ThermalGrid` and `ThermalHouse` [#1193](https://github.com/ie3-institute/simona/issues/1193) - Replaced Java Durations with Scala Durations [#1068](https://github.com/ie3-institute/simona/issues/1068) +- Typo and format of `ThermalGrid` and `ThermalHouse` ScalaDocs [#1196](https://github.com/ie3-institute/simona/issues/1196) +- Refactor `EmRuntimeConfig` [#1181](https://github.com/ie3-institute/simona/issues/1181) +- Based `PvModel` calculations on irradiance (power per area) instead of irradiation (energy per area) [#1212](https://github.com/ie3-institute/simona/issues/1212) ### Fixed - Fix rendering of references in documentation [#505](https://github.com/ie3-institute/simona/issues/505) @@ -186,6 +191,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Move `ScheduleServiceActivation` out of `RegistrationResponseMessage` [#1143](https://github.com/ie3-institute/simona/issues/1143) - Check for runningHp when handling infeed to thermalGrid [#1167](https://github.com/ie3-institute/simona/issues/1167) - Send `FlexResult` to EM [#1202](https://github.com/ie3-institute/simona/issues/1202) +- Fix test timeouts for all tests [#1222](https://github.com/ie3-institute/simona/issues/1222) + +### Removed +- Removed `SimonaListerner` and related code [#1205](https://github.com/ie3-institute/simona/issues/1205) ## [3.0.0] - 2023-08-07 diff --git a/build.gradle b/build.gradle index cb48595a42..651e98888a 100644 --- a/build.gradle +++ b/build.gradle @@ -28,6 +28,7 @@ ext { confluentKafkaVersion = '7.4.0' scapegoatVersion = '3.1.4' + junitVersion = '1.12.0' testContainerVersion = '0.41.8' scriptsLocation = 'gradle' + File.separator + 'scripts' + File.separator // location of script plugins @@ -98,8 +99,8 @@ dependencies { /* testing */ // scalatest & junit testImplementation "org.scalatest:scalatest_${scalaVersion}:3.2.19" - testImplementation "org.junit.platform:junit-platform-launcher:1.11.4" - testRuntimeOnly "org.junit.platform:junit-platform-engine:1.11.4" + testImplementation "org.junit.platform:junit-platform-launcher:${junitVersion}" + testRuntimeOnly "org.junit.platform:junit-platform-engine:${junitVersion}" testRuntimeOnly "org.scalatestplus:junit-5-11_${scalaVersion}:3.2.19.0" // mocking framework diff --git a/docs/readthedocs/config.md b/docs/readthedocs/config.md index 925c040239..8b6a329a2d 100644 --- a/docs/readthedocs/config.md +++ b/docs/readthedocs/config.md @@ -237,12 +237,6 @@ The initial SOC defaults to 0%, while the target SOC is optional. When no target Individual configuration can be assigned accordingly. -## Event configuration - -Tba: - - `simona.event.listener = []` - ## Grid configuration ### Reference System diff --git a/docs/readthedocs/models/pv_model.md b/docs/readthedocs/models/pv_model.md index da64e2067a..9a9e3c5802 100644 --- a/docs/readthedocs/models/pv_model.md +++ b/docs/readthedocs/models/pv_model.md @@ -4,7 +4,8 @@ This page documents the functionality of the PV model available in SIMONA. -The initial parts of the model are presented in the paper [Agent based approach to model photovoltaic feed-in in distribution network planning](https://ieeexplore.ieee.org/abstract/document/7038345). Since then several adaptions has been made that are documented as follows. +The initial parts of the model are presented in the paper _Agent based approach to model photovoltaic feed-in in distribution network planning_ by {cite:cts}`Seack.2014`. +Since then several adaptions has been made that are documented as follows. The PV Model is part of the SIMONA Simulation framework and represented by an agent. @@ -20,21 +21,29 @@ Please refer to {doc}`PowerSystemDataModel - PV Model ClassicRef} import org.slf4j.Logger +import squants.Each import java.time.ZonedDateTime import java.util.UUID @@ -74,6 +83,7 @@ class GridAgentController( environmentRefs: EnvironmentRefs, simulationStartDate: ZonedDateTime, simulationEndDate: ZonedDateTime, + emConfigs: AssetConfigs[EmRuntimeConfig], participantsConfig: Participant, outputConfig: SimonaConfig.Simona.Output.Participant, resolution: Long, @@ -88,10 +98,24 @@ class GridAgentController( val systemParticipants = filterSysParts(subGridContainer, environmentRefs) + val outputConfigUtil = ConfigUtil.OutputConfigUtil(outputConfig) + + // ems that control at least one participant directly + val firstLevelEms = systemParticipants.flatMap { + _.getControllingEm.toScala.map(em => em.getUuid -> em) + }.toMap + + val allEms = buildEmsRecursively( + EmConfigUtil(emConfigs), + outputConfigUtil, + firstLevelEms, + ) + /* Browse through all system participants, build actors and map their node's UUID to the actor references */ buildParticipantToActorRef( participantsConfig, - outputConfig, + allEms, + outputConfigUtil, systemParticipants, thermalIslandGridsByBusId, environmentRefs, @@ -174,7 +198,8 @@ class GridAgentController( */ private def buildParticipantToActorRef( participantsConfig: Participant, - outputConfig: SimonaConfig.Simona.Output.Participant, + emAgents: Map[UUID, ActorRef[FlexResponse]], + outputConfigUtil: OutputConfigUtil, participants: Vector[SystemParticipantInput], thermalIslandGridsByBusId: Map[UUID, ThermalGrid], environmentRefs: EnvironmentRefs, @@ -183,18 +208,6 @@ class GridAgentController( * phase */ val participantConfigUtil = ConfigUtil.ParticipantConfigUtil(participantsConfig) - val outputConfigUtil = ConfigUtil.OutputConfigUtil(outputConfig) - - // ems that control at least one participant directly - val firstLevelEms = participants.flatMap { - _.getControllingEm.toScala.map(em => em.getUuid -> em) - }.toMap - - val allEms = buildEmsRecursively( - participantConfigUtil, - outputConfigUtil, - firstLevelEms, - ) participants .map { participant => @@ -204,7 +217,7 @@ class GridAgentController( participant.getControllingEm.toScala .map(_.getUuid) .map(uuid => - allEms.getOrElse( + emAgents.getOrElse( uuid, throw new CriticalFailureException( s"EM actor with UUID $uuid not found." @@ -221,7 +234,6 @@ class GridAgentController( environmentRefs, controllingEm, ) - introduceAgentToEnvironment(actorRef) // return uuid to actorRef node.getUuid -> actorRef } @@ -234,7 +246,7 @@ class GridAgentController( * its way up to EMs at root level, which are not EM-controlled themselves. * The first level can also be root level. * - * @param participantConfigUtil + * @param emConfigUtil * Configuration util for participant models * @param outputConfigUtil * Configuration util for output behaviour @@ -247,7 +259,7 @@ class GridAgentController( * Map from model UUID to EmAgent ActorRef */ private def buildEmsRecursively( - participantConfigUtil: ConfigUtil.ParticipantConfigUtil, + emConfigUtil: ConfigUtil.EmConfigUtil, outputConfigUtil: OutputConfigUtil, emInputs: Map[UUID, EmInput], previousLevelEms: Map[UUID, ActorRef[FlexResponse]] = Map.empty, @@ -261,7 +273,7 @@ class GridAgentController( else { val actor = buildEm( emInput, - participantConfigUtil.getOrDefault[EmRuntimeConfig](uuid), + emConfigUtil.getOrDefault(uuid), outputConfigUtil.getOrDefault(NotifierIdentifier.Em), maybeControllingEm = None, ) @@ -282,7 +294,7 @@ class GridAgentController( // Return value includes previous level and uncontrolled EMs of this level val recursiveEms = buildEmsRecursively( - participantConfigUtil, + emConfigUtil, outputConfigUtil, controllingEms, previousLevelAndUncontrolledEms, @@ -302,7 +314,7 @@ class GridAgentController( uuid -> buildEm( emInput, - participantConfigUtil.getOrDefault[EmRuntimeConfig](uuid), + emConfigUtil.getOrDefault(uuid), outputConfigUtil.getOrDefault(NotifierIdentifier.Em), maybeControllingEm = controllingEm, ) @@ -322,241 +334,176 @@ class GridAgentController( thermalIslandGridsByBusId: Map[UUID, ThermalGrid], environmentRefs: EnvironmentRefs, maybeControllingEm: Option[ActorRef[FlexResponse]], - ): ActorRef[ParticipantAgent.Request] = participantInputModel match { - case input: FixedFeedInInput => - buildFixedFeedIn( - input, - participantConfigUtil.getOrDefault[FixedFeedInRuntimeConfig]( - input.getUuid - ), - environmentRefs.primaryServiceProxy, - simulationStartDate, - simulationEndDate, - resolution, - requestVoltageDeviationThreshold, - outputConfigUtil.getOrDefault(NotifierIdentifier.FixedFeedIn), - maybeControllingEm, - ) - case input: LoadInput => - buildLoad( - input, - participantConfigUtil.getOrDefault[LoadRuntimeConfig]( - input.getUuid - ), - environmentRefs.primaryServiceProxy, - simulationStartDate, - simulationEndDate, - resolution, - requestVoltageDeviationThreshold, - outputConfigUtil.getOrDefault(NotifierIdentifier.Load), - maybeControllingEm, - ) - case input: PvInput => - buildPv( - input, - participantConfigUtil.getOrDefault[PvRuntimeConfig]( - input.getUuid - ), - environmentRefs.primaryServiceProxy, - environmentRefs.weather, - simulationStartDate, - simulationEndDate, - resolution, - requestVoltageDeviationThreshold, - outputConfigUtil.getOrDefault(NotifierIdentifier.PvPlant), - maybeControllingEm, - ) - case input: WecInput => - buildWec( - input, - participantConfigUtil.getOrDefault[WecRuntimeConfig]( - input.getUuid - ), - environmentRefs.primaryServiceProxy, - environmentRefs.weather, - simulationStartDate, - simulationEndDate, - resolution, - requestVoltageDeviationThreshold, - outputConfigUtil.getOrDefault(NotifierIdentifier.Wec), - maybeControllingEm, - ) - case input: EvcsInput => - buildEvcs( - input, - participantConfigUtil.getOrDefault[EvcsRuntimeConfig]( - input.getUuid - ), - environmentRefs.primaryServiceProxy, - environmentRefs.evDataService.getOrElse( - throw new GridAgentInitializationException( - "EvMovementsService required for setting up evcs." - ) - ), - simulationStartDate, - simulationEndDate, - resolution, - requestVoltageDeviationThreshold, - outputConfigUtil.getOrDefault(NotifierIdentifier.Evcs), - maybeControllingEm, - ) - case hpInput: HpInput => - thermalIslandGridsByBusId.get(hpInput.getThermalBus.getUuid) match { - case Some(thermalGrid) => - buildHp( - hpInput, - thermalGrid, - participantConfigUtil.getOrDefault[HpRuntimeConfig]( - hpInput.getUuid - ), - environmentRefs.primaryServiceProxy, - environmentRefs.weather, - requestVoltageDeviationThreshold, - outputConfigUtil.getOrDefault(NotifierIdentifier.Hp), - maybeControllingEm, - ) - case None => - throw new GridAgentInitializationException( - s"Unable to find thermal island grid for heat pump '${hpInput.getUuid}' with thermal bus '${hpInput.getThermalBus.getUuid}'." - ) - } - case input: StorageInput => - buildStorage( - input, - participantConfigUtil.getOrDefault[StorageRuntimeConfig]( - input.getUuid + ): ActorRef[ParticipantAgent.Request] = { + + val serviceMap: Map[ServiceType, ClassicRef] = + Seq( + Some(ServiceType.WeatherService -> environmentRefs.weather), + environmentRefs.evDataService.map(ref => + ServiceType.EvMovementService -> ref ), - environmentRefs.primaryServiceProxy, - simulationStartDate, - simulationEndDate, - resolution, - requestVoltageDeviationThreshold, - outputConfigUtil.getOrDefault(NotifierIdentifier.Storage), - maybeControllingEm, - ) - case input: SystemParticipantInput => - throw new NotImplementedError( - s"Building ${input.getClass.getSimpleName} is not implemented, yet." - ) - case unknown => - throw new GridAgentInitializationException( - "Received unknown input model type " + unknown.toString + "." - ) - } + ).flatten.toMap - /** Creates a fixed feed in agent and determines the needed additional - * information for later initialization of the agent. - * - * @param fixedFeedInInput - * Fixed Feed In input model to derive information from - * @param modelConfiguration - * User-provided configuration for this specific fixed feed in model - * @param primaryServiceProxy - * Reference to the primary data service proxy - * @param simulationStartDate - * The simulation time at which the simulation starts - * @param simulationEndDate - * The simulation time at which the simulation ends - * @param resolution - * Frequency of power flow calculations - * @param requestVoltageDeviationThreshold - * Maximum deviation in p.u. of request voltages to be considered equal - * @param outputConfig - * Configuration of the output behavior - * @param maybeControllingEm - * The parent EmAgent, if applicable - * @return - * The [[FixedFeedInAgent]] 's [[ActorRef]] - */ - private def buildFixedFeedIn( - fixedFeedInInput: FixedFeedInInput, - modelConfiguration: FixedFeedInRuntimeConfig, - primaryServiceProxy: ClassicRef, - simulationStartDate: ZonedDateTime, - simulationEndDate: ZonedDateTime, - resolution: Long, - requestVoltageDeviationThreshold: Double, - outputConfig: NotifierConfig, - maybeControllingEm: Option[ActorRef[FlexResponse]], - ): ActorRef[ParticipantAgent.Request] = - gridAgentContext.toClassic - .simonaActorOf( - FixedFeedInAgent.props( - environmentRefs.scheduler.toClassic, - ParticipantInitializeStateData( - fixedFeedInInput, - modelConfiguration, - primaryServiceProxy, - Iterable.empty, - simulationStartDate, - simulationEndDate, - resolution, - requestVoltageDeviationThreshold, - outputConfig, - maybeControllingEm, + val participantRefs = ParticipantRefs( + gridAgentContext.self, + environmentRefs.primaryServiceProxy, + serviceMap, + listener, + ) + + val simParams = SimulationParameters( + resolution, + Each(requestVoltageDeviationThreshold), + simulationStartDate, + simulationEndDate, + ) + + participantInputModel match { + case input: FixedFeedInInput => + buildParticipant( + input, + participantConfigUtil.getOrDefault[FixedFeedInRuntimeConfig]( + input.getUuid ), - listener.map(_.toClassic), - ), - fixedFeedInInput.getId, - ) - .toTyped + outputConfigUtil.getOrDefault(NotifierIdentifier.FixedFeedIn), + participantRefs, + simParams, + environmentRefs.scheduler, + maybeControllingEm, + ) + case input: LoadInput => + buildParticipant( + input, + participantConfigUtil.getOrDefault[LoadRuntimeConfig]( + input.getUuid + ), + outputConfigUtil.getOrDefault(NotifierIdentifier.Load), + participantRefs, + simParams, + environmentRefs.scheduler, + maybeControllingEm, + ) + case input: PvInput => + buildPv( + input, + participantConfigUtil.getOrDefault[PvRuntimeConfig]( + input.getUuid + ), + environmentRefs.primaryServiceProxy, + environmentRefs.weather, + simulationStartDate, + simulationEndDate, + resolution, + requestVoltageDeviationThreshold, + outputConfigUtil.getOrDefault(NotifierIdentifier.PvPlant), + maybeControllingEm, + ) + case input: WecInput => + buildWec( + input, + participantConfigUtil.getOrDefault[WecRuntimeConfig]( + input.getUuid + ), + environmentRefs.primaryServiceProxy, + environmentRefs.weather, + simulationStartDate, + simulationEndDate, + resolution, + requestVoltageDeviationThreshold, + outputConfigUtil.getOrDefault(NotifierIdentifier.Wec), + maybeControllingEm, + ) + case input: EvcsInput => + buildEvcs( + input, + participantConfigUtil.getOrDefault[EvcsRuntimeConfig]( + input.getUuid + ), + environmentRefs.primaryServiceProxy, + environmentRefs.evDataService.getOrElse( + throw new GridAgentInitializationException( + "EvMovementsService required for setting up evcs." + ) + ), + simulationStartDate, + simulationEndDate, + resolution, + requestVoltageDeviationThreshold, + outputConfigUtil.getOrDefault(NotifierIdentifier.Evcs), + maybeControllingEm, + ) + case hpInput: HpInput => + thermalIslandGridsByBusId.get(hpInput.getThermalBus.getUuid) match { + case Some(thermalGrid) => + buildHp( + hpInput, + thermalGrid, + participantConfigUtil.getOrDefault[HpRuntimeConfig]( + hpInput.getUuid + ), + environmentRefs.primaryServiceProxy, + environmentRefs.weather, + requestVoltageDeviationThreshold, + outputConfigUtil.getOrDefault(NotifierIdentifier.Hp), + maybeControllingEm, + ) + case None => + throw new GridAgentInitializationException( + s"Unable to find thermal island grid for heat pump '${hpInput.getUuid}' with thermal bus '${hpInput.getThermalBus.getUuid}'." + ) + } + case input: StorageInput => + buildStorage( + input, + participantConfigUtil.getOrDefault[StorageRuntimeConfig]( + input.getUuid + ), + environmentRefs.primaryServiceProxy, + simulationStartDate, + simulationEndDate, + resolution, + requestVoltageDeviationThreshold, + outputConfigUtil.getOrDefault(NotifierIdentifier.Storage), + maybeControllingEm, + ) + case input: SystemParticipantInput => + throw new NotImplementedError( + s"Building ${input.getClass.getSimpleName} is not implemented, yet." + ) + case unknown => + throw new GridAgentInitializationException( + "Received unknown input model type " + unknown.toString + "." + ) + } + } - /** Creates a load agent and determines the needed additional information for - * later initialization of the agent. - * - * @param loadInput - * Load input model to derive information from - * @param modelConfiguration - * User-provided configuration for this specific load model - * @param primaryServiceProxy - * Reference to the primary data service proxy - * @param simulationStartDate - * The simulation time at which the simulation starts - * @param simulationEndDate - * The simulation time at which the simulation ends - * @param resolution - * Frequency of power flow calculations - * @param requestVoltageDeviationThreshold - * Maximum deviation in p.u. of request voltages to be considered equal - * @param outputConfig - * Configuration of the output behavior - * @param maybeControllingEm - * The parent EmAgent, if applicable - * @return - * The [[LoadAgent]] 's [[ActorRef]] - */ - private def buildLoad( - loadInput: LoadInput, - modelConfiguration: LoadRuntimeConfig, - primaryServiceProxy: ClassicRef, - simulationStartDate: ZonedDateTime, - simulationEndDate: ZonedDateTime, - resolution: Long, - requestVoltageDeviationThreshold: Double, - outputConfig: NotifierConfig, + private def buildParticipant( + participantInput: SystemParticipantInput, + runtimeConfig: BaseRuntimeConfig, + notifierConfig: NotifierConfig, + participantRefs: ParticipantRefs, + simParams: SimulationParameters, + scheduler: ActorRef[SchedulerMessage], maybeControllingEm: Option[ActorRef[FlexResponse]], - ): ActorRef[ParticipantAgent.Request] = - gridAgentContext.toClassic - .simonaActorOf( - LoadAgent.props( - environmentRefs.scheduler.toClassic, - ParticipantInitializeStateData( - loadInput, - modelConfiguration, - primaryServiceProxy, - Iterable.empty, - simulationStartDate, - simulationEndDate, - resolution, - requestVoltageDeviationThreshold, - outputConfig, - maybeControllingEm, - ), - listener.map(_.toClassic), - ), - loadInput.getId, - ) - .toTyped + ): ActorRef[ParticipantAgent.Request] = { + val participant = gridAgentContext.spawn( + ParticipantAgentInit( + participantInput, + runtimeConfig, + notifierConfig, + participantRefs, + simParams, + maybeControllingEm.toRight(scheduler), + ), + name = actorName( + participantInput.getClass.getSimpleName.replace("Input", ""), + participantInput.getId, + ), + ) + gridAgentContext.watch(participant) + + participant + } /** Creates a pv agent and determines the needed additional information for * later initialization of the agent. @@ -595,8 +542,8 @@ class GridAgentController( requestVoltageDeviationThreshold: Double, outputConfig: NotifierConfig, maybeControllingEm: Option[ActorRef[FlexResponse]], - ): ActorRef[ParticipantAgent.Request] = - gridAgentContext.toClassic + ): ActorRef[ParticipantAgent.Request] = { + val participant = gridAgentContext.toClassic .simonaActorOf( PvAgent.props( environmentRefs.scheduler.toClassic, @@ -617,6 +564,10 @@ class GridAgentController( pvInput.getId, ) .toTyped + introduceAgentToEnvironment(participant) + + participant + } /** Creates an Evcs agent and determines the needed additional information for * later initialization of the agent. @@ -655,8 +606,8 @@ class GridAgentController( requestVoltageDeviationThreshold: Double, outputConfig: NotifierConfig, maybeControllingEm: Option[ActorRef[FlexResponse]], - ): ActorRef[ParticipantAgent.Request] = - gridAgentContext.toClassic + ): ActorRef[ParticipantAgent.Request] = { + val participant = gridAgentContext.toClassic .simonaActorOf( EvcsAgent.props( environmentRefs.scheduler.toClassic, @@ -681,6 +632,10 @@ class GridAgentController( evcsInput.getId, ) .toTyped + introduceAgentToEnvironment(participant) + + participant + } /** Builds an [[HpAgent]] from given input * @@ -712,8 +667,8 @@ class GridAgentController( requestVoltageDeviationThreshold: Double, outputConfig: NotifierConfig, maybeControllingEm: Option[ActorRef[FlexResponse]], - ): ActorRef[ParticipantAgent.Request] = - gridAgentContext.toClassic + ): ActorRef[ParticipantAgent.Request] = { + val participant = gridAgentContext.toClassic .simonaActorOf( HpAgent.props( environmentRefs.scheduler.toClassic, @@ -735,6 +690,10 @@ class GridAgentController( hpInput.getId, ) .toTyped + introduceAgentToEnvironment(participant) + + participant + } /** Creates a wec agent and determines the needed additional information for * later initialization of the agent. @@ -773,8 +732,8 @@ class GridAgentController( requestVoltageDeviationThreshold: Double, outputConfig: NotifierConfig, maybeControllingEm: Option[ActorRef[FlexResponse]], - ): ActorRef[ParticipantAgent.Request] = - gridAgentContext.toClassic + ): ActorRef[ParticipantAgent.Request] = { + val participant = gridAgentContext.toClassic .simonaActorOf( WecAgent.props( environmentRefs.scheduler.toClassic, @@ -795,6 +754,10 @@ class GridAgentController( wecInput.getId, ) .toTyped + introduceAgentToEnvironment(participant) + + participant + } /** Creates a storage agent and determines the needed additional information * for later initialization of the agent. @@ -830,8 +793,8 @@ class GridAgentController( requestVoltageDeviationThreshold: Double, outputConfig: NotifierConfig, maybeControllingEm: Option[ActorRef[FlexResponse]] = None, - ): ActorRef[ParticipantAgent.Request] = - gridAgentContext.toClassic + ): ActorRef[ParticipantAgent.Request] = { + val participant = gridAgentContext.toClassic .simonaActorOf( StorageAgent.props( environmentRefs.scheduler.toClassic, @@ -852,6 +815,10 @@ class GridAgentController( storageInput.getId, ) .toTyped + introduceAgentToEnvironment(participant) + + participant + } /** Builds an [[EmAgent]] from given input * diff --git a/src/main/scala/edu/ie3/simona/agent/participant/fixedfeedin/FixedFeedInAgent.scala b/src/main/scala/edu/ie3/simona/agent/participant/fixedfeedin/FixedFeedInAgent.scala deleted file mode 100644 index 8ee881025e..0000000000 --- a/src/main/scala/edu/ie3/simona/agent/participant/fixedfeedin/FixedFeedInAgent.scala +++ /dev/null @@ -1,68 +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.participant.fixedfeedin - -import edu.ie3.datamodel.models.input.system.FixedFeedInInput -import edu.ie3.simona.agent.participant.ParticipantAgent -import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ComplexPower -import edu.ie3.simona.agent.participant.statedata.ParticipantStateData -import edu.ie3.simona.agent.participant.statedata.ParticipantStateData.ParticipantInitializeStateData -import edu.ie3.simona.config.RuntimeConfig.FixedFeedInRuntimeConfig -import edu.ie3.simona.model.participant.CalcRelevantData.FixedRelevantData -import edu.ie3.simona.model.participant.FixedFeedInModel -import edu.ie3.simona.model.participant.ModelState.ConstantState -import org.apache.pekko.actor.{ActorRef, Props} - -object FixedFeedInAgent { - def props( - scheduler: ActorRef, - initStateData: ParticipantInitializeStateData[ - FixedFeedInInput, - FixedFeedInRuntimeConfig, - ComplexPower, - ], - listener: Iterable[ActorRef], - ): Props = - Props(new FixedFeedInAgent(scheduler, initStateData, listener)) -} - -/** Creating a fixed feed in agent - * - * @param scheduler - * Actor reference of the scheduler - * @param listener - * List of listeners interested in results - */ -class FixedFeedInAgent( - scheduler: ActorRef, - initStateData: ParticipantInitializeStateData[ - FixedFeedInInput, - FixedFeedInRuntimeConfig, - ComplexPower, - ], - override val listener: Iterable[ActorRef], -) extends ParticipantAgent[ - ComplexPower, - FixedRelevantData.type, - ConstantState.type, - ParticipantStateData[ComplexPower], - FixedFeedInInput, - FixedFeedInRuntimeConfig, - FixedFeedInModel, - ](scheduler, initStateData) - with FixedFeedInAgentFundamentals { - - /* - * "Hey, SIMONA! What is handled in ParticipantAgent?" - * "Hey, dude! The following things are handled in ParticipantAgent: - * 1) Initialization of Agent - * 2) Event reactions in Idle state - * 3) Handling of incoming information - * 4) Performing model calculations - * " - */ -} diff --git a/src/main/scala/edu/ie3/simona/agent/participant/fixedfeedin/FixedFeedInAgentFundamentals.scala b/src/main/scala/edu/ie3/simona/agent/participant/fixedfeedin/FixedFeedInAgentFundamentals.scala deleted file mode 100644 index d7303135b7..0000000000 --- a/src/main/scala/edu/ie3/simona/agent/participant/fixedfeedin/FixedFeedInAgentFundamentals.scala +++ /dev/null @@ -1,405 +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.participant.fixedfeedin - -import edu.ie3.datamodel.models.input.system.FixedFeedInInput -import edu.ie3.datamodel.models.result.ResultEntity -import edu.ie3.datamodel.models.result.system.{ - FixedFeedInResult, - SystemParticipantResult, -} -import edu.ie3.simona.agent.ValueStore -import edu.ie3.simona.agent.participant.ParticipantAgent.getAndCheckNodalVoltage -import edu.ie3.simona.agent.participant.ParticipantAgentFundamentals -import edu.ie3.simona.agent.participant.data.Data.PrimaryData.{ - ComplexPower, - ZERO_POWER, -} -import edu.ie3.simona.agent.participant.data.Data.SecondaryData -import edu.ie3.simona.agent.participant.data.secondary.SecondaryDataService -import edu.ie3.simona.agent.participant.statedata.BaseStateData.{ - FlexControlledData, - ParticipantModelBaseStateData, -} -import edu.ie3.simona.agent.participant.statedata.ParticipantStateData -import edu.ie3.simona.agent.participant.statedata.ParticipantStateData.InputModelContainer -import edu.ie3.simona.agent.state.AgentState -import edu.ie3.simona.agent.state.AgentState.Idle -import edu.ie3.simona.config.RuntimeConfig.FixedFeedInRuntimeConfig -import edu.ie3.simona.event.notifier.NotifierConfig -import edu.ie3.simona.exceptions.agent.{ - InconsistentStateException, - InvalidRequestException, -} -import edu.ie3.simona.io.result.AccompaniedSimulationResult -import edu.ie3.simona.model.participant.CalcRelevantData.FixedRelevantData -import edu.ie3.simona.model.participant.ModelState.ConstantState -import edu.ie3.simona.model.participant.{ - CalcRelevantData, - FixedFeedInModel, - FlexChangeIndicator, - ModelState, -} -import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage.{ - FlexRequest, - FlexResponse, -} -import edu.ie3.simona.util.SimonaConstants -import edu.ie3.simona.util.TickUtil.RichZonedDateTime -import edu.ie3.util.quantities.PowerSystemUnits.PU -import edu.ie3.util.quantities.QuantityUtils.RichQuantityDouble -import edu.ie3.util.scala.quantities.ReactivePower -import org.apache.pekko.actor.typed.scaladsl.adapter.ClassicActorRefOps -import org.apache.pekko.actor.typed.{ActorRef => TypedActorRef} -import org.apache.pekko.actor.{ActorRef, FSM} -import squants.{Dimensionless, Each, Power} - -import java.time.ZonedDateTime -import java.util.UUID -import scala.collection.SortedSet -import scala.reflect.{ClassTag, classTag} - -protected trait FixedFeedInAgentFundamentals - extends ParticipantAgentFundamentals[ - ComplexPower, - FixedRelevantData.type, - ConstantState.type, - ParticipantStateData[ComplexPower], - FixedFeedInInput, - FixedFeedInRuntimeConfig, - FixedFeedInModel, - ] { - this: FixedFeedInAgent => - override protected val pdClassTag: ClassTag[ComplexPower] = - classTag[ComplexPower] - override val alternativeResult: ComplexPower = ZERO_POWER - - /** Determines the needed base state data in dependence of the foreseen - * simulation mode of the agent. - * - * @param inputModel - * Input model definition - * @param modelConfig - * Configuration of the model - * @param services - * Collection of services to register with - * @param simulationStartDate - * Real world time date time, when the simulation starts - * @param simulationEndDate - * Real world time date time, when the simulation ends - * @param resolution - * Agents regular time bin it wants to be triggered e.g. one hour - * @param requestVoltageDeviationThreshold - * Threshold, after which two nodal voltage magnitudes from participant - * power requests for the same tick are considered to be different - * @param outputConfig - * Config for the output behaviour of simulation results - * @return - * A child of [[ParticipantModelBaseStateData]] that reflects the behaviour - * based on the data source definition - */ - override def determineModelBaseStateData( - inputModel: InputModelContainer[FixedFeedInInput], - modelConfig: FixedFeedInRuntimeConfig, - services: Iterable[SecondaryDataService[_ <: SecondaryData]], - simulationStartDate: ZonedDateTime, - simulationEndDate: ZonedDateTime, - resolution: Long, - requestVoltageDeviationThreshold: Double, - outputConfig: NotifierConfig, - maybeEmAgent: Option[TypedActorRef[FlexResponse]], - ): ParticipantModelBaseStateData[ - ComplexPower, - FixedRelevantData.type, - ConstantState.type, - FixedFeedInModel, - ] = { - /* Build the calculation model */ - val model = - buildModel( - inputModel, - modelConfig, - simulationStartDate, - simulationEndDate, - ) - - /* Go and collect all ticks, in which new data will be available. Also register for - * services, where needed. */ - val lastTickInSimulation = simulationEndDate.toTick(simulationStartDate) - val dataTicks = - /* As participant agents always return their last known operation point on request, it is sufficient - * to let a FixedFeedIn-Model determine its operation point on: - * 1) The first tick of the simulation - * 2) The tick, it turns on (in time-dependent operation) - * 3) The tick, it turns off (in time-dependent operation) - * Coinciding ticks are summarized and the last tick is removed, as the change in operation status - * doesn't affect anything then */ - SortedSet[Long]( - SimonaConstants.FIRST_TICK_IN_SIMULATION, - model.operationInterval.start, - model.operationInterval.end, - ).filterNot(_ == lastTickInSimulation) - - ParticipantModelBaseStateData[ - ComplexPower, - FixedRelevantData.type, - ConstantState.type, - FixedFeedInModel, - ]( - simulationStartDate, - simulationEndDate, - model, - services, - outputConfig, - dataTicks, - Map.empty, - requestVoltageDeviationThreshold, - ValueStore.forVoltage( - resolution, - Each( - inputModel.electricalInputModel.getNode - .getvTarget() - .to(PU) - .getValue - .doubleValue - ), - ), - ValueStore(resolution), - ValueStore(resolution), - ValueStore(resolution), - ValueStore(resolution), - maybeEmAgent.map(FlexControlledData(_, self.toTyped[FlexRequest])), - ) - } - - override def buildModel( - inputModel: InputModelContainer[FixedFeedInInput], - modelConfig: FixedFeedInRuntimeConfig, - simulationStartDate: ZonedDateTime, - simulationEndDate: ZonedDateTime, - ): FixedFeedInModel = FixedFeedInModel( - inputModel.electricalInputModel, - modelConfig, - simulationStartDate, - simulationEndDate, - ) - - override protected def createInitialState( - baseStateData: ParticipantModelBaseStateData[ - ComplexPower, - FixedRelevantData.type, - ConstantState.type, - FixedFeedInModel, - ] - ): ModelState.ConstantState.type = ConstantState - - override protected def createCalcRelevantData( - baseStateData: ParticipantModelBaseStateData[ - ComplexPower, - FixedRelevantData.type, - ConstantState.type, - FixedFeedInModel, - ], - tick: Long, - ): FixedRelevantData.type = - FixedRelevantData - - /** Handle an active power change by flex control. - * @param tick - * Tick, in which control is issued - * @param baseStateData - * Base state data of the agent - * @param data - * Calculation relevant data - * @param lastState - * Last known model state - * @param setPower - * Setpoint active power - * @return - * Updated model state, a result model and a [[FlexChangeIndicator]] - */ - def handleControlledPowerChange( - tick: Long, - baseStateData: ParticipantModelBaseStateData[ - ComplexPower, - FixedRelevantData.type, - ConstantState.type, - FixedFeedInModel, - ], - data: FixedRelevantData.type, - lastState: ConstantState.type, - setPower: squants.Power, - ): ( - ConstantState.type, - AccompaniedSimulationResult[ComplexPower], - FlexChangeIndicator, - ) = { - /* Calculate result */ - val voltage = getAndCheckNodalVoltage(baseStateData, tick) - - val reactivePower = baseStateData.model.calculateReactivePower( - setPower, - voltage, - ) - val result = AccompaniedSimulationResult( - ComplexPower(setPower, reactivePower), - Seq.empty[ResultEntity], - ) - - /* Handle the request within the model */ - val (updatedState, flexChangeIndicator) = - baseStateData.model.handleControlledPowerChange(data, lastState, setPower) - (updatedState, result, flexChangeIndicator) - } - - override val calculateModelPowerFunc: ( - Long, - ParticipantModelBaseStateData[ - ComplexPower, - FixedRelevantData.type, - ConstantState.type, - FixedFeedInModel, - ], - ConstantState.type, - Dimensionless, - ) => ComplexPower = ( - currentTick: Long, - baseStateData: ParticipantModelBaseStateData[ - ComplexPower, - FixedRelevantData.type, - ConstantState.type, - FixedFeedInModel, - ], - state: ConstantState.type, - voltage: Dimensionless, - ) => - baseStateData.model match { - case fixedModel: FixedFeedInModel => - fixedModel.calculatePower( - currentTick, - voltage, - state, - FixedRelevantData, - ) - case unsupportedModel => - throw new InconsistentStateException( - s"The model $unsupportedModel is not supported!" - ) - } - - /** Calculate the power output of the participant utilising secondary data. - * However, it might appear, that not the complete set of secondary data is - * available for the given tick. This might especially be true, if the actor - * has been additionally activated. This method thereby has to try and fill - * up missing data with the last known data, as this is still supposed to be - * valid. The secondary data therefore is put to the calculation relevant - * data store.

The next state is [[Idle]], sending a - * [[edu.ie3.simona.ontology.messages.SchedulerMessage.Completion]] to - * scheduler and using update result values.

- * - * @param baseStateData - * The base state data with collected secondary data - * @param lastModelState - * Optional last model state - * @param currentTick - * Tick, the trigger belongs to - * @param scheduler - * [[ActorRef]] to the scheduler in the simulation - * @return - * [[Idle]] with updated result values - */ - override def calculatePowerWithSecondaryDataAndGoToIdle( - baseStateData: ParticipantModelBaseStateData[ - ComplexPower, - FixedRelevantData.type, - ConstantState.type, - FixedFeedInModel, - ], - lastModelState: ConstantState.type, - currentTick: Long, - scheduler: ActorRef, - ): FSM.State[AgentState, ParticipantStateData[ComplexPower]] = - throw new InvalidRequestException( - "Request to calculate power with secondary data cannot be processed in a fixed feed in agent." - ) - - /** Determine the average result within the given tick window - * - * @param tickToResults - * Mapping from data tick to actual data - * @param windowStart - * First, included tick of the time window - * @param windowEnd - * Last, included tick of the time window - * @param activeToReactivePowerFuncOpt - * An Option on a function, that transfers the active into reactive power - * @return - * The averaged result - */ - override def averageResults( - tickToResults: Map[Long, ComplexPower], - windowStart: Long, - windowEnd: Long, - activeToReactivePowerFuncOpt: Option[ - Power => ReactivePower - ] = None, - ): ComplexPower = - ParticipantAgentFundamentals.averageApparentPower( - tickToResults, - windowStart, - windowEnd, - activeToReactivePowerFuncOpt, - log, - ) - - /** Determines the correct result. - * - * @param uuid - * Unique identifier of the physical model - * @param dateTime - * Real world date of the result - * @param result - * The primary data to build a result model for - * @return - * The equivalent event - */ - override protected def buildResult( - uuid: UUID, - dateTime: ZonedDateTime, - result: ComplexPower, - ): SystemParticipantResult = - new FixedFeedInResult( - dateTime, - uuid, - result.p.toMegawatts.asMegaWatt, - result.q.toMegavars.asMegaVar, - ) - - /** Update the last known model state with the given external, relevant data - * - * @param tick - * Tick to update state for - * @param modelState - * Last known model state - * @param calcRelevantData - * Data, relevant for calculation - * @param nodalVoltage - * Current nodal voltage of the agent - * @param model - * Model for calculation - * @return - * The updated state at given tick under consideration of calculation - * relevant data - */ - override protected def updateState( - tick: Long, - modelState: ModelState.ConstantState.type, - calcRelevantData: CalcRelevantData.FixedRelevantData.type, - nodalVoltage: squants.Dimensionless, - model: FixedFeedInModel, - ): ModelState.ConstantState.type = modelState -} diff --git a/src/main/scala/edu/ie3/simona/agent/participant/load/LoadAgent.scala b/src/main/scala/edu/ie3/simona/agent/participant/load/LoadAgent.scala deleted file mode 100644 index 5354eaf2a8..0000000000 --- a/src/main/scala/edu/ie3/simona/agent/participant/load/LoadAgent.scala +++ /dev/null @@ -1,133 +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.participant.load - -import edu.ie3.datamodel.models.input.system.LoadInput -import edu.ie3.simona.agent.participant.ParticipantAgent -import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ComplexPower -import edu.ie3.simona.agent.participant.load.LoadAgentFundamentals.{ - FixedLoadAgentFundamentals, - ProfileLoadAgentFundamentals, - RandomLoadAgentFundamentals, -} -import edu.ie3.simona.agent.participant.statedata.ParticipantStateData -import edu.ie3.simona.agent.participant.statedata.ParticipantStateData.ParticipantInitializeStateData -import edu.ie3.simona.config.RuntimeConfig.LoadRuntimeConfig -import edu.ie3.simona.model.participant.CalcRelevantData.LoadRelevantData -import edu.ie3.simona.model.participant.ModelState.ConstantState -import edu.ie3.simona.model.participant.load.profile.ProfileLoadModel -import edu.ie3.simona.model.participant.load.profile.ProfileLoadModel.ProfileRelevantData -import edu.ie3.simona.model.participant.load.random.RandomLoadModel -import edu.ie3.simona.model.participant.load.random.RandomLoadModel.RandomRelevantData -import edu.ie3.simona.model.participant.load.{ - FixedLoadModel, - LoadModel, - LoadModelBehaviour, -} -import org.apache.pekko.actor.{ActorRef, Props} - -object LoadAgent { - def props( - scheduler: ActorRef, - initStateData: ParticipantInitializeStateData[ - LoadInput, - LoadRuntimeConfig, - ComplexPower, - ], - listener: Iterable[ActorRef], - ): Props = - LoadModelBehaviour(initStateData.modelConfig.modelBehaviour) match { - case LoadModelBehaviour.FIX => - Props(new FixedLoadAgent(scheduler, initStateData, listener)) - case LoadModelBehaviour.PROFILE => - Props(new ProfileLoadAgent(scheduler, initStateData, listener)) - case LoadModelBehaviour.RANDOM => - Props(new RandomLoadAgent(scheduler, initStateData, listener)) - case unsupported => - throw new IllegalArgumentException( - s"The load agent behaviour '$unsupported' is currently not supported." - ) - } - - final class FixedLoadAgent( - scheduler: ActorRef, - initStateData: ParticipantInitializeStateData[ - LoadInput, - LoadRuntimeConfig, - ComplexPower, - ], - override val listener: Iterable[ActorRef], - ) extends LoadAgent[ - FixedLoadModel.FixedLoadRelevantData.type, - FixedLoadModel, - ](scheduler, initStateData, listener) - with FixedLoadAgentFundamentals - - final class ProfileLoadAgent( - scheduler: ActorRef, - initStateData: ParticipantInitializeStateData[ - LoadInput, - LoadRuntimeConfig, - ComplexPower, - ], - override val listener: Iterable[ActorRef], - ) extends LoadAgent[ - ProfileRelevantData, - ProfileLoadModel, - ](scheduler, initStateData, listener) - with ProfileLoadAgentFundamentals - - final class RandomLoadAgent( - scheduler: ActorRef, - initStateData: ParticipantInitializeStateData[ - LoadInput, - LoadRuntimeConfig, - ComplexPower, - ], - override val listener: Iterable[ActorRef], - ) extends LoadAgent[ - RandomRelevantData, - RandomLoadModel, - ](scheduler, initStateData, listener) - with RandomLoadAgentFundamentals -} - -/** Creating a load agent - * - * @param scheduler - * Actor reference of the scheduler - * @param listener - * List of listeners interested in results - */ -abstract class LoadAgent[LD <: LoadRelevantData, LM <: LoadModel[LD]]( - scheduler: ActorRef, - initStateData: ParticipantInitializeStateData[ - LoadInput, - LoadRuntimeConfig, - ComplexPower, - ], - override val listener: Iterable[ActorRef], -) extends ParticipantAgent[ - ComplexPower, - LD, - ConstantState.type, - ParticipantStateData[ComplexPower], - LoadInput, - LoadRuntimeConfig, - LM, - ](scheduler, initStateData) - with LoadAgentFundamentals[LD, LM] { - /* - * "Hey, SIMONA! What is handled in ParticipantAgent?" - * "Hey, dude! The following things are handled in ParticipantAgent: - * 1) Initialization of Agent - * 2) Event reactions in Idle state - * 3) Handling of incoming information - * 4) Performing model calculations - * " - */ -} diff --git a/src/main/scala/edu/ie3/simona/agent/participant/load/LoadAgentFundamentals.scala b/src/main/scala/edu/ie3/simona/agent/participant/load/LoadAgentFundamentals.scala deleted file mode 100644 index 1df2fe1a82..0000000000 --- a/src/main/scala/edu/ie3/simona/agent/participant/load/LoadAgentFundamentals.scala +++ /dev/null @@ -1,570 +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.participant.load - -import edu.ie3.datamodel.models.input.system.LoadInput -import edu.ie3.datamodel.models.result.ResultEntity -import edu.ie3.datamodel.models.result.system.{ - LoadResult, - SystemParticipantResult, -} -import edu.ie3.simona.agent.ValueStore -import edu.ie3.simona.agent.participant.ParticipantAgent.getAndCheckNodalVoltage -import edu.ie3.simona.agent.participant.ParticipantAgentFundamentals -import edu.ie3.simona.agent.participant.data.Data.PrimaryData.{ - ComplexPower, - ZERO_POWER, -} -import edu.ie3.simona.agent.participant.data.Data.SecondaryData -import edu.ie3.simona.agent.participant.data.secondary.SecondaryDataService -import edu.ie3.simona.agent.participant.statedata.BaseStateData.{ - FlexControlledData, - ParticipantModelBaseStateData, -} -import edu.ie3.simona.agent.participant.statedata.ParticipantStateData -import edu.ie3.simona.agent.participant.statedata.ParticipantStateData.InputModelContainer -import edu.ie3.simona.agent.state.AgentState -import edu.ie3.simona.agent.state.AgentState.Idle -import edu.ie3.simona.config.RuntimeConfig.LoadRuntimeConfig -import edu.ie3.simona.event.notifier.NotifierConfig -import edu.ie3.simona.exceptions.agent.InconsistentStateException -import edu.ie3.simona.io.result.AccompaniedSimulationResult -import edu.ie3.simona.model.SystemComponent -import edu.ie3.simona.model.participant.CalcRelevantData.LoadRelevantData -import edu.ie3.simona.model.participant.ModelState.ConstantState -import edu.ie3.simona.model.participant.load.FixedLoadModel.FixedLoadRelevantData -import edu.ie3.simona.model.participant.load.profile.ProfileLoadModel.ProfileRelevantData -import edu.ie3.simona.model.participant.load.profile.{ - LoadProfileStore, - ProfileLoadModel, -} -import edu.ie3.simona.model.participant.load.random.RandomLoadModel.RandomRelevantData -import edu.ie3.simona.model.participant.load.random.{ - RandomLoadModel, - RandomLoadParamStore, -} -import edu.ie3.simona.model.participant.load.{ - FixedLoadModel, - LoadModel, - LoadReference, -} -import edu.ie3.simona.model.participant.{FlexChangeIndicator, ModelState} -import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage.{ - FlexRequest, - FlexResponse, -} -import edu.ie3.simona.util.SimonaConstants -import edu.ie3.simona.util.TickUtil._ -import edu.ie3.util.quantities.PowerSystemUnits.PU -import edu.ie3.util.quantities.QuantityUtils.RichQuantityDouble -import edu.ie3.util.scala.OperationInterval -import edu.ie3.util.scala.quantities.ReactivePower -import org.apache.pekko.actor.typed.scaladsl.adapter.ClassicActorRefOps -import org.apache.pekko.actor.typed.{ActorRef => TypedActorRef} -import org.apache.pekko.actor.{ActorRef, FSM} -import squants.{Dimensionless, Each, Power} - -import java.time.ZonedDateTime -import java.util.UUID -import scala.collection.SortedSet -import scala.reflect.{ClassTag, classTag} - -protected trait LoadAgentFundamentals[LD <: LoadRelevantData, LM <: LoadModel[ - LD -]] extends ParticipantAgentFundamentals[ - ComplexPower, - LD, - ConstantState.type, - ParticipantStateData[ComplexPower], - LoadInput, - LoadRuntimeConfig, - LM, - ] { - this: LoadAgent[LD, LM] => - override protected val pdClassTag: ClassTag[ComplexPower] = - classTag[ComplexPower] - override val alternativeResult: ComplexPower = ZERO_POWER - - /** Determines the needed base state data in dependence of the foreseen - * simulation mode of the agent. - * - * @param inputModel - * Input model definition - * @param modelConfig - * Configuration of the model - * @param services - * Collection of services to register with - * @param simulationStartDate - * Real world time date time, when the simulation starts - * @param simulationEndDate - * Real world time date time, when the simulation ends - * @param resolution - * Agents regular time bin it wants to be triggered e.g. one hour - * @param requestVoltageDeviationThreshold - * Threshold, after which two nodal voltage magnitudes from participant - * power requests for the same tick are considered to be different - * @param outputConfig - * Config of the output behaviour for simulation results - * @return - * A child of [[ParticipantModelBaseStateData]] that reflects the behaviour - * based on the data source definition - */ - override def determineModelBaseStateData( - inputModel: InputModelContainer[LoadInput], - modelConfig: LoadRuntimeConfig, - services: Iterable[SecondaryDataService[_ <: SecondaryData]], - simulationStartDate: ZonedDateTime, - simulationEndDate: ZonedDateTime, - resolution: Long, - requestVoltageDeviationThreshold: Double, - outputConfig: NotifierConfig, - maybeEmAgent: Option[TypedActorRef[FlexResponse]], - ): ParticipantModelBaseStateData[ - ComplexPower, - LD, - ConstantState.type, - LM, - ] = { - /* Build the calculation model */ - val model = - buildModel( - inputModel, - modelConfig, - simulationStartDate, - simulationEndDate, - ) - - /* Go and collect all ticks, in which activation is needed in addition to the activations made by incoming data. - * Also register for services, where needed. */ - val lastTickInSimulation = simulationEndDate.toTick(simulationStartDate) - val additionalActivationTicks = model match { - /* If no secondary data is needed (implicitly by fixed load model), add activation ticks for the simple model */ - case fixedLoadModel: FixedLoadModel => - /* As participant agents always return their last known operation point on request, it is sufficient - * to let a fixed load model determine its operation point on: - * 1) The first tick of the simulation - * 2) The tick, it turns on (in time-dependent operation) - * 3) The tick, it turns off (in time-dependent operation) - * Coinciding ticks are summarized and the last tick is removed, as the change in operation status - * doesn't affect anything then */ - SortedSet[Long]( - SimonaConstants.FIRST_TICK_IN_SIMULATION, - fixedLoadModel.operationInterval.start, - fixedLoadModel.operationInterval.end, - ).filterNot(_ == lastTickInSimulation) - case profileLoadModel: ProfileLoadModel => - activationTicksInOperationTime( - simulationStartDate, - LoadProfileStore.resolution.getSeconds, - profileLoadModel.operationInterval.start, - profileLoadModel.operationInterval.end, - ) - case randomLoadModel: RandomLoadModel => - activationTicksInOperationTime( - simulationStartDate, - RandomLoadParamStore.resolution.getSeconds, - randomLoadModel.operationInterval.start, - randomLoadModel.operationInterval.end, - ) - case _ => - SortedSet.empty[Long] - } - - ParticipantModelBaseStateData[ComplexPower, LD, ConstantState.type, LM]( - simulationStartDate, - simulationEndDate, - model, - services, - outputConfig, - additionalActivationTicks, - Map.empty, - requestVoltageDeviationThreshold, - ValueStore.forVoltage( - resolution, - Each( - inputModel.electricalInputModel.getNode - .getvTarget() - .to(PU) - .getValue - .doubleValue - ), - ), - ValueStore(resolution), - ValueStore(resolution), - ValueStore(resolution), - ValueStore(resolution), - maybeEmAgent.map(FlexControlledData(_, self.toTyped[FlexRequest])), - ) - } - - override def buildModel( - inputModel: InputModelContainer[LoadInput], - modelConfig: LoadRuntimeConfig, - simulationStartDate: ZonedDateTime, - simulationEndDate: ZonedDateTime, - ): LM = { - val operationInterval: OperationInterval = - SystemComponent.determineOperationInterval( - simulationStartDate, - simulationEndDate, - inputModel.electricalInputModel.getOperationTime, - ) - val reference = LoadReference(inputModel.electricalInputModel, modelConfig) - buildModel( - inputModel.electricalInputModel, - operationInterval, - modelConfig, - reference, - ) - } - - protected def buildModel( - inputModel: LoadInput, - operationInterval: OperationInterval, - modelConfig: LoadRuntimeConfig, - reference: LoadReference, - ): LM - - override protected def createInitialState( - baseStateData: ParticipantModelBaseStateData[ - ComplexPower, - LD, - ConstantState.type, - LM, - ] - ): ModelState.ConstantState.type = ConstantState - - /** Handle an active power change by flex control. - * @param tick - * Tick, in which control is issued - * @param baseStateData - * Base state data of the agent - * @param data - * Calculation relevant data - * @param lastState - * Last known model state - * @param setPower - * Setpoint active power - * @return - * Updated model state, a result model and a [[FlexChangeIndicator]] - */ - def handleControlledPowerChange( - tick: Long, - baseStateData: ParticipantModelBaseStateData[ - ComplexPower, - LD, - ConstantState.type, - LM, - ], - data: LD, - lastState: ConstantState.type, - setPower: squants.Power, - ): ( - ConstantState.type, - AccompaniedSimulationResult[ComplexPower], - FlexChangeIndicator, - ) = { - /* Calculate result */ - val voltage = getAndCheckNodalVoltage(baseStateData, tick) - - val reactivePower = baseStateData.model.calculateReactivePower( - setPower, - voltage, - ) - val result = AccompaniedSimulationResult( - ComplexPower(setPower, reactivePower), - Seq.empty[ResultEntity], - ) - - /* Handle the request within the model */ - val (updatedState, flexChangeIndicator) = - baseStateData.model.handleControlledPowerChange(data, lastState, setPower) - (updatedState, result, flexChangeIndicator) - } - - /** Calculate the power output of the participant utilising secondary data. - * However, it might appear, that not the complete set of secondary data is - * available for the given tick. This might especially be true, if the actor - * has been additionally activated. This method thereby has to try and fill - * up missing data with the last known data, as this is still supposed to be - * valid. The secondary data therefore is put to the calculation relevant - * data store.

The next state is [[Idle]], sending a - * [[edu.ie3.simona.ontology.messages.SchedulerMessage.Completion]] to - * scheduler and using update result values.

- * - * @param baseStateData - * The base state data with collected secondary data - * @param lastModelState - * Optional last model state - * @param currentTick - * Tick, the trigger belongs to - * @param scheduler - * [[ActorRef]] to the scheduler in the simulation - * @return - * [[Idle]] with updated result values - */ - override def calculatePowerWithSecondaryDataAndGoToIdle( - baseStateData: ParticipantModelBaseStateData[ - ComplexPower, - LD, - ConstantState.type, - LM, - ], - lastModelState: ConstantState.type, - currentTick: Long, - scheduler: ActorRef, - ): FSM.State[AgentState, ParticipantStateData[ComplexPower]] = - throw new InconsistentStateException( - s"Load model is not able to calculate power with secondary data." - ) - - /** Determine the average result within the given tick window - * - * @param tickToResults - * Mapping from data tick to actual data - * @param windowStart - * First, included tick of the time window - * @param windowEnd - * Last, included tick of the time window - * @param activeToReactivePowerFuncOpt - * An Option on a function, that transfers the active into reactive power - * @return - * The averaged result - */ - override def averageResults( - tickToResults: Map[Long, ComplexPower], - windowStart: Long, - windowEnd: Long, - activeToReactivePowerFuncOpt: Option[ - Power => ReactivePower - ] = None, - ): ComplexPower = - ParticipantAgentFundamentals.averageApparentPower( - tickToResults, - windowStart, - windowEnd, - activeToReactivePowerFuncOpt, - log, - ) - - /** Determines the correct result. - * - * @param uuid - * Unique identifier of the physical model - * @param dateTime - * Real world date of the result - * @param result - * The primary data to build a result model for - * @return - * The equivalent event - */ - override protected def buildResult( - uuid: UUID, - dateTime: ZonedDateTime, - result: ComplexPower, - ): SystemParticipantResult = - new LoadResult( - dateTime, - uuid, - result.p.toMegawatts.asMegaWatt, - result.q.toMegavars.asMegaVar, - ) - - override protected def updateState( - tick: Long, - modelState: ModelState.ConstantState.type, - calcRelevantData: LD, - nodalVoltage: squants.Dimensionless, - model: LM, - ): ModelState.ConstantState.type = modelState -} - -object LoadAgentFundamentals { - trait FixedLoadAgentFundamentals - extends LoadAgentFundamentals[ - FixedLoadModel.FixedLoadRelevantData.type, - FixedLoadModel, - ] { - this: LoadAgent.FixedLoadAgent => - - override def buildModel( - inputModel: LoadInput, - operationInterval: OperationInterval, - modelConfig: LoadRuntimeConfig, - reference: LoadReference, - ): FixedLoadModel = - FixedLoadModel( - inputModel, - modelConfig.scaling, - operationInterval, - reference, - ) - - override protected def createCalcRelevantData( - baseStateData: ParticipantModelBaseStateData[ - ComplexPower, - FixedLoadRelevantData.type, - ConstantState.type, - FixedLoadModel, - ], - tick: Long, - ): FixedLoadRelevantData.type = - FixedLoadRelevantData - - /** Partial function, that is able to transfer - * [[ParticipantModelBaseStateData]] (holding the actual calculation model) - * into a pair of active and reactive power - */ - override val calculateModelPowerFunc: ( - Long, - ParticipantModelBaseStateData[ - ComplexPower, - FixedLoadRelevantData.type, - ConstantState.type, - FixedLoadModel, - ], - ConstantState.type, - Dimensionless, - ) => ComplexPower = ( - tick: Long, - baseStateData: ParticipantModelBaseStateData[ - ComplexPower, - FixedLoadRelevantData.type, - ConstantState.type, - FixedLoadModel, - ], - state: ConstantState.type, - voltage: Dimensionless, - ) => - baseStateData.model.calculatePower( - tick, - voltage, - state, - FixedLoadRelevantData, - ) - } - - trait ProfileLoadAgentFundamentals - extends LoadAgentFundamentals[ - ProfileRelevantData, - ProfileLoadModel, - ] { - this: LoadAgent.ProfileLoadAgent => - - override def buildModel( - inputModel: LoadInput, - operationInterval: OperationInterval, - modelConfig: LoadRuntimeConfig, - reference: LoadReference, - ): ProfileLoadModel = - ProfileLoadModel( - inputModel, - operationInterval, - modelConfig.scaling, - reference, - ) - - override protected def createCalcRelevantData( - baseStateData: ParticipantModelBaseStateData[ - ComplexPower, - ProfileRelevantData, - ConstantState.type, - ProfileLoadModel, - ], - currentTick: Long, - ): ProfileRelevantData = - ProfileRelevantData( - currentTick.toDateTime(baseStateData.startDate) - ) - - /** Partial function, that is able to transfer - * [[ParticipantModelBaseStateData]] (holding the actual calculation model) - * into a pair of active and reactive power - */ - override val calculateModelPowerFunc: ( - Long, - ParticipantModelBaseStateData[ - ComplexPower, - ProfileRelevantData, - ConstantState.type, - ProfileLoadModel, - ], - ConstantState.type, - Dimensionless, - ) => ComplexPower = (tick, baseStateData, _, voltage) => { - val profileRelevantData = - createCalcRelevantData(baseStateData, tick) - - baseStateData.model.calculatePower( - currentTick, - voltage, - ConstantState, - profileRelevantData, - ) - } - } - - trait RandomLoadAgentFundamentals - extends LoadAgentFundamentals[ - RandomRelevantData, - RandomLoadModel, - ] { - this: LoadAgent.RandomLoadAgent => - - override def buildModel( - inputModel: LoadInput, - operationInterval: OperationInterval, - modelConfig: LoadRuntimeConfig, - reference: LoadReference, - ): RandomLoadModel = - RandomLoadModel( - inputModel, - operationInterval, - modelConfig.scaling, - reference, - ) - - override protected def createCalcRelevantData( - baseStateData: ParticipantModelBaseStateData[ - ComplexPower, - RandomRelevantData, - ConstantState.type, - RandomLoadModel, - ], - tick: Long, - ): RandomRelevantData = - RandomRelevantData( - tick.toDateTime(baseStateData.startDate) - ) - - /** Partial function, that is able to transfer - * [[ParticipantModelBaseStateData]] (holding the actual calculation model) - * into a pair of active and reactive power - */ - override val calculateModelPowerFunc: ( - Long, - ParticipantModelBaseStateData[ - ComplexPower, - RandomRelevantData, - ConstantState.type, - RandomLoadModel, - ], - ConstantState.type, - Dimensionless, - ) => ComplexPower = (tick, baseStateData, _, voltage) => { - val profileRelevantData = - createCalcRelevantData(baseStateData, tick) - - baseStateData.model.calculatePower( - currentTick, - voltage, - ConstantState, - profileRelevantData, - ) - } - } -} diff --git a/src/main/scala/edu/ie3/simona/agent/participant/pv/PvAgentFundamentals.scala b/src/main/scala/edu/ie3/simona/agent/participant/pv/PvAgentFundamentals.scala index 4d908d43d1..907fd3c9d0 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant/pv/PvAgentFundamentals.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant/pv/PvAgentFundamentals.scala @@ -50,7 +50,6 @@ import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage.{ FlexResponse, } import edu.ie3.simona.ontology.messages.services.WeatherMessage.WeatherData -import edu.ie3.simona.service.weather.WeatherService.FALLBACK_WEATHER_STEM_DISTANCE import edu.ie3.simona.util.TickUtil.TickLong import edu.ie3.util.quantities.PowerSystemUnits.PU import edu.ie3.util.quantities.QuantityUtils.RichQuantityDouble @@ -203,17 +202,6 @@ protected trait PvAgentFundamentals baseStateData.startDate val dateTime = tick.toDateTime - val tickInterval = - baseStateData.receivedSecondaryDataStore - .lastKnownTick(tick - 1) match { - case Some(dataTick) => - tick - dataTick - case _ => - /* At the first tick, we are not able to determine the tick interval from last tick - * (since there is none). Then we use a fallback pv stem distance. */ - FALLBACK_WEATHER_STEM_DISTANCE - } - // take the last weather data, not necessarily the one for the current tick: // we might receive flex control messages for irregular ticks val (_, secondaryData) = baseStateData.receivedSecondaryDataStore @@ -240,7 +228,6 @@ protected trait PvAgentFundamentals PvRelevantData( dateTime, - tickInterval, weatherData.diffIrr, weatherData.dirIrr, ) diff --git a/src/main/scala/edu/ie3/simona/config/ArgsParser.scala b/src/main/scala/edu/ie3/simona/config/ArgsParser.scala index c6c52fb80e..22891d8ace 100644 --- a/src/main/scala/edu/ie3/simona/config/ArgsParser.scala +++ b/src/main/scala/edu/ie3/simona/config/ArgsParser.scala @@ -8,8 +8,6 @@ package edu.ie3.simona.config import com.typesafe.config.{ConfigFactory, Config => TypesafeConfig} import com.typesafe.scalalogging.LazyLogging -import edu.ie3.simona.event.listener.SimonaListenerCompanion -import edu.ie3.util.scala.ReflectionTools import scopt.{OptionParser => scoptOptionParser} import java.io.File @@ -156,43 +154,6 @@ object ArgsParser extends LazyLogging { override def toString = "worker" } - /** Parses the given listener configuration tino a map of - * [[SimonaListenerCompanion]] to an optional list of it's regarding events - * to process - * - * @param listenerConfigOption - * Option to a list of listener definitions from config - * @return - * A mapping from [[SimonaListenerCompanion]] to an Option to a list of - * events to process - */ - def parseListenerConfigOption( - listenerConfigOption: Option[List[SimonaConfig.Simona.Event.Listener$Elm]] - ): Map[SimonaListenerCompanion, Option[List[String]]] = { - listenerConfigOption match { - case Some(listenerElems) => - listenerElems.foldLeft( - Map.empty[SimonaListenerCompanion, Option[List[String]]] - )((listenerMap, listenerElem) => - ReflectionTools - .resolveClassNameToCompanion(listenerElem.fullClassPath) match { - case Some(listener: SimonaListenerCompanion) => - listenerMap + (listener -> listenerElem.eventsToProcess) - case nonListenerCompanion => - logger.warn( - s"Invalid value ${nonListenerCompanion.getClass} for 'event.listener' config parameter!" - ) - listenerMap - } - ) - case None => - logger.info( - "No listener assigned in configuration value 'event.listener'. No event are going to be processed!" - ) - Map.empty[SimonaListenerCompanion, Option[List[String]]] - } - } - /** Prepare the config by parsing the provided program arguments * * @param args diff --git a/src/main/scala/edu/ie3/simona/config/ConfigFailFast.scala b/src/main/scala/edu/ie3/simona/config/ConfigFailFast.scala index ce26616f18..e83ac6198d 100644 --- a/src/main/scala/edu/ie3/simona/config/ConfigFailFast.scala +++ b/src/main/scala/edu/ie3/simona/config/ConfigFailFast.scala @@ -24,7 +24,10 @@ import edu.ie3.simona.config.SimonaConfig.Simona.Output.Sink.InfluxDb1x import edu.ie3.simona.config.SimonaConfig._ import edu.ie3.simona.exceptions.InvalidConfigParameterException import edu.ie3.simona.io.result.ResultSinkType -import edu.ie3.simona.model.participant.load.{LoadModelBehaviour, LoadReference} +import edu.ie3.simona.model.participant2.load.{ + LoadModelBehaviour, + LoadReferenceType, +} import edu.ie3.simona.service.primary.PrimaryServiceProxy import edu.ie3.simona.service.weather.WeatherSource.WeatherScheme import edu.ie3.simona.util.CollectionUtils @@ -36,7 +39,6 @@ import edu.ie3.simona.util.ConfigUtil.DatabaseConfigUtil.{ checkSqlParams, } import edu.ie3.simona.util.ConfigUtil.{CsvConfigUtil, NotifierIdentifier} -import edu.ie3.util.scala.ReflectionTools import edu.ie3.util.{StringUtils, TimeUtil} import tech.units.indriya.quantity.Quantities import tech.units.indriya.unit.Units @@ -200,7 +202,7 @@ object ConfigFailFast extends LazyLogging { ) // failure if all sinks are not-configured - val sinkConfigs = ReflectionTools.classFieldToVal(sink).values.map { + val sinkConfigs = sink.productIterator.toSeq.map { case o: Option[_] => o case _ => throw new InvalidConfigParameterException( @@ -445,7 +447,7 @@ object ConfigFailFast extends LazyLogging { ) if ( - !LoadReference.isEligibleKey( + !LoadReferenceType.isEligibleInput( loadModelConfig.reference ) ) @@ -796,8 +798,7 @@ object ConfigFailFast extends LazyLogging { * * 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]] + * influenced by the transformer? * * @param transformerControlGroup * Transformer control group definition diff --git a/src/main/scala/edu/ie3/simona/config/RuntimeConfig.scala b/src/main/scala/edu/ie3/simona/config/RuntimeConfig.scala index 6e70a48302..0749eda0a5 100644 --- a/src/main/scala/edu/ie3/simona/config/RuntimeConfig.scala +++ b/src/main/scala/edu/ie3/simona/config/RuntimeConfig.scala @@ -7,13 +7,19 @@ package edu.ie3.simona.config import edu.ie3.simona.config.RuntimeConfig._ -import edu.ie3.simona.config.SimonaConfig.{RuntimeKafkaParams, VoltLvlConfig} +import edu.ie3.simona.config.SimonaConfig.{ + AssetConfigs, + RuntimeKafkaParams, + VoltLvlConfig, +} import pureconfig.generic.ProductHint import pureconfig.{CamelCase, ConfigFieldMapping} import scala.language.implicitConversions /** Runtime configurations for simona. + * @param em + * runtime configs for energy management systems * @param listener * runtime listener configuration * @param participant @@ -24,6 +30,7 @@ import scala.language.implicitConversions * option for selected voltage levels (default: None) */ final case class RuntimeConfig( + em: AssetConfigs[EmRuntimeConfig] = AssetConfigs(EmRuntimeConfig()), listener: Listener = Listener(), participant: Participant = Participant.empty(), selectedSubgrids: Option[List[Int]] = None, @@ -54,8 +61,6 @@ object RuntimeConfig { ) /** Runtime configurations for participants. - * @param em - * runtime configs for energy management systems * @param evcs * runtime configs for electrical vehicle charging stations * @param fixedFeedIn @@ -74,7 +79,6 @@ object RuntimeConfig { * runtime configs for wind energy converters */ final case class Participant( - em: ParticipantRuntimeConfigs[EmRuntimeConfig], evcs: ParticipantRuntimeConfigs[EvcsRuntimeConfig], fixedFeedIn: ParticipantRuntimeConfigs[FixedFeedInRuntimeConfig], hp: ParticipantRuntimeConfigs[HpRuntimeConfig], @@ -90,7 +94,6 @@ object RuntimeConfig { /** Returns a [[Participant]] object with default values. */ def empty(): Participant = Participant( - em = EmRuntimeConfig(), evcs = EvcsRuntimeConfig(), fixedFeedIn = FixedFeedInRuntimeConfig(), hp = HpRuntimeConfig(), diff --git a/src/main/scala/edu/ie3/simona/config/SimonaConfig.scala b/src/main/scala/edu/ie3/simona/config/SimonaConfig.scala index 1730484d30..9fc075ddc7 100644 --- a/src/main/scala/edu/ie3/simona/config/SimonaConfig.scala +++ b/src/main/scala/edu/ie3/simona/config/SimonaConfig.scala @@ -74,6 +74,19 @@ object SimonaConfig { // pure config end + /** Case class contains default and individual configs for assets. + * @param defaultConfig + * to use + * @param individualConfigs + * specific configs, that are used instead of the [[defaultConfig]] + * @tparam T + * type of asset config + */ + final case class AssetConfigs[T]( + defaultConfig: T, + individualConfigs: List[T] = List.empty, + ) + final case class BaseCsvParams( override val csvSep: String, override val directoryPath: String, @@ -177,7 +190,6 @@ object SimonaConfig { congestionManagement: Simona.CongestionManagement = Simona.CongestionManagement(), control: Option[Simona.Control] = None, - event: Simona.Event = Simona.Event(), gridConfig: Simona.GridConfig = Simona.GridConfig(), input: Simona.Input, output: Simona.Output, @@ -196,16 +208,6 @@ object SimonaConfig { transformer: List[TransformerControlGroup] = List.empty ) - final case class Event( - listener: Option[List[Event.Listener$Elm]] = None - ) - object Event { - final case class Listener$Elm( - eventsToProcess: Option[List[String]] = None, - fullClassPath: String, - ) - } - final case class GridConfig( refSystems: Option[List[RefSystemConfig]] = None, voltageLimits: Option[List[VoltageLimitsConfig]] = None, diff --git a/src/main/scala/edu/ie3/simona/event/listener/SimonaListener.scala b/src/main/scala/edu/ie3/simona/event/listener/SimonaListener.scala deleted file mode 100644 index c76057cb7a..0000000000 --- a/src/main/scala/edu/ie3/simona/event/listener/SimonaListener.scala +++ /dev/null @@ -1,11 +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.event.listener - -import org.apache.pekko.actor.Actor - -trait SimonaListener extends Actor diff --git a/src/main/scala/edu/ie3/simona/event/listener/SimonaListenerCompanion.scala b/src/main/scala/edu/ie3/simona/event/listener/SimonaListenerCompanion.scala deleted file mode 100644 index 5a93bf2a16..0000000000 --- a/src/main/scala/edu/ie3/simona/event/listener/SimonaListenerCompanion.scala +++ /dev/null @@ -1,19 +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.event.listener - -import org.apache.pekko.actor.Props - -import scala.reflect.ClassTag - -/** Companion trait to [[[[edu.ie3.simona.event.listener.SimonaListener]] - */ -trait SimonaListenerCompanion { - def props[A <: SimonaListenerWithFilter: ClassTag]( - eventsToProcess: Option[List[String]] = None - ): Props -} diff --git a/src/main/scala/edu/ie3/simona/event/listener/SimonaListenerWithFilter.scala b/src/main/scala/edu/ie3/simona/event/listener/SimonaListenerWithFilter.scala deleted file mode 100644 index 666120e80e..0000000000 --- a/src/main/scala/edu/ie3/simona/event/listener/SimonaListenerWithFilter.scala +++ /dev/null @@ -1,59 +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.event.listener - -import org.apache.pekko.actor.ActorRef -import edu.ie3.simona.event.Event -import edu.ie3.simona.logging.SimonaActorLogging - -/** Implementations can be registered at simulation setup and will then receive - * and process [[Event]] s from other Actors - * - * @param eventsToProcess - * Option of the events to process in this simulation. To make processing - * possible, events have to be implemented in processEvent

None -> - * All implemented events will be processed
Some(List.empty) -> No - * events will be processed
Some(List(...)) -> All implemented events of - * this List will be processed - */ -abstract class SimonaListenerWithFilter(eventsToProcess: Option[List[String]]) - extends SimonaListener - with SimonaActorLogging { - - /** Introduce handling for [[Event]] s via - * [[edu.ie3.simona.event.listener.SimonaListenerWithFilter.receiveEvent]] DO - * NOT OVERRIDE THIS METHOD IN IMPLEMENTATION! Use [[processEvent()]] - * instead! - * - * @return - */ - override def receive: Receive = { - case e: Event => receiveEvent(e) - case unknownMessage => - log.warning(s"Received unknown message: $unknownMessage") - } - - /** Filters received event and processes it only if it is in - * [[edu.ie3.simona.event.listener.SimonaListenerWithFilter.eventsToProcess]] - * (or eventsToProcess is None) - */ - private def receiveEvent: Receive = { case event: Event => - eventsToProcess match { - case None => processEvent(event, sender()) - case Some(events) if events.contains(event.id) => - processEvent(event, sender()) - case _ => - log.debug( - "Skipping event {} as it is not in the list of events to process.", - event.id, - ) - } - } - - protected def processEvent(event: Event, ref: ActorRef): Unit - -} diff --git a/src/main/scala/edu/ie3/simona/model/participant/FixedFeedInModel.scala b/src/main/scala/edu/ie3/simona/model/participant/FixedFeedInModel.scala deleted file mode 100644 index 1deb1c138b..0000000000 --- a/src/main/scala/edu/ie3/simona/model/participant/FixedFeedInModel.scala +++ /dev/null @@ -1,128 +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.model.participant - -import com.typesafe.scalalogging.LazyLogging -import edu.ie3.datamodel.models.input.system.FixedFeedInInput -import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ComplexPower -import edu.ie3.simona.config.RuntimeConfig.FixedFeedInRuntimeConfig -import edu.ie3.simona.model.SystemComponent -import edu.ie3.simona.model.participant.CalcRelevantData.FixedRelevantData -import edu.ie3.simona.model.participant.ModelState.ConstantState -import edu.ie3.simona.model.participant.control.QControl -import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage.ProvideFlexOptions -import edu.ie3.simona.ontology.messages.flex.MinMaxFlexibilityMessage.ProvideMinMaxFlexOptions -import edu.ie3.util.quantities.PowerSystemUnits -import edu.ie3.util.scala.OperationInterval -import edu.ie3.util.scala.quantities.{ApparentPower, Kilovoltamperes} -import squants.Power - -import java.time.ZonedDateTime -import java.util.UUID - -/** Fixed feed generation model delivering constant power - * - * @param uuid - * the element's uuid - * @param id - * the element's human-readable id - * @param operationInterval - * Interval, in which the system is in operation - * @param qControl - * Type of reactive power control - * @param sRated - * Rated apparent power - * @param cosPhiRated - * Rated power factor - */ -final case class FixedFeedInModel( - uuid: UUID, - id: String, - operationInterval: OperationInterval, - qControl: QControl, - sRated: ApparentPower, - cosPhiRated: Double, -) extends SystemParticipant[ - FixedRelevantData.type, - ComplexPower, - ConstantState.type, - ]( - uuid, - id, - operationInterval, - qControl, - sRated, - cosPhiRated, - ) - with ApparentPowerParticipant[FixedRelevantData.type, ConstantState.type] { - - /** Calculate the active power behaviour of the model - * - * @param data - * Further needed, secondary data. Due to the nature of a fixed feed model, - * no further data is required. - * @return - * Active power - */ - override def calculateActivePower( - modelState: ConstantState.type, - data: FixedRelevantData.type = FixedRelevantData, - ): Power = sRated.toActivePower(cosPhiRated) * -1 - - override def determineFlexOptions( - data: FixedRelevantData.type, - lastState: ConstantState.type, - ): ProvideFlexOptions = - ProvideMinMaxFlexOptions.noFlexOption( - uuid, - calculateActivePower(lastState, data), - ) - - override def handleControlledPowerChange( - data: FixedRelevantData.type, - lastState: ConstantState.type, - setPower: Power, - ): (ConstantState.type, FlexChangeIndicator) = - (lastState, FlexChangeIndicator()) -} - -object FixedFeedInModel extends LazyLogging { - def apply( - inputModel: FixedFeedInInput, - modelConfiguration: FixedFeedInRuntimeConfig, - simulationStartDate: ZonedDateTime, - simulationEndDate: ZonedDateTime, - ): FixedFeedInModel = { - val scaledInput = - inputModel.copy().scale(modelConfiguration.scaling).build() - - /* Determine the operation interval */ - val operationInterval: OperationInterval = - SystemComponent.determineOperationInterval( - simulationStartDate, - simulationEndDate, - scaledInput.getOperationTime, - ) - - // build the fixed feed in model - val model = FixedFeedInModel( - scaledInput.getUuid, - scaledInput.getId, - operationInterval, - QControl.apply(scaledInput.getqCharacteristics), - Kilovoltamperes( - scaledInput.getsRated - .to(PowerSystemUnits.KILOVOLTAMPERE) - .getValue - .doubleValue - ), - scaledInput.getCosPhiRated, - ) - model.enable() - model - } -} diff --git a/src/main/scala/edu/ie3/simona/model/participant/PvModel.scala b/src/main/scala/edu/ie3/simona/model/participant/PvModel.scala index 98f2162ed2..d399892cfa 100644 --- a/src/main/scala/edu/ie3/simona/model/participant/PvModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant/PvModel.scala @@ -51,45 +51,35 @@ final case class PvModel private ( with ApparentPowerParticipant[PvRelevantData, ConstantState.type] { /** Override sMax as the power output of a pv unit could become easily up to - * 10% higher than the sRated value found in the technical sheets + * 10% higher than the sRated value found in the technical sheets. */ override val sMax: ApparentPower = sRated * 1.1 - /** Permissible maximum active power feed in (therefore negative) */ + /** Permissible maximum active power feed in (therefore negative). */ protected val pMax: Power = sMax.toActivePower(cosPhiRated) * -1d - /** Reference yield at standard testing conditions (STC) */ + /** Reference yield at standard testing conditions (STC). */ private val yieldSTC = WattsPerSquareMeter(1000d) private val activationThreshold = sRated.toActivePower(cosPhiRated) * 0.001 * -1d - /** Calculate the active power behaviour of the model + /** Calculate the active power behaviour of the model. * * @param data - * Further needed, secondary data + * Further needed, secondary data. * @return - * Active power + * THe Active power. */ override protected def calculateActivePower( modelState: ConstantState.type, data: PvRelevantData, ): Power = { - // === Weather Base Data === // - /* The pv model calculates the power in-feed based on the solar irradiance that is received over a specific - * time frame (which actually is the solar irradiation). Hence, a multiplication with the time frame within - * this irradiance is received is required. */ - val duration: Time = Seconds(data.weatherDataFrameLength) - - // eBeamH and eDifH needs to be extract to their double values in some places - // hence a conversion to watt-hour per square meter is required, to avoid - // invalid double value extraction! - val eBeamH = - data.dirIrradiance * duration - val eDifH = - data.diffIrradiance * duration - - // === Beam Radiation Parameters === // + // Irradiance on a horizontal surface + val gBeamH = data.dirIrradiance + val gDifH = data.diffIrradiance + + // === Beam irradiance parameters === // val angleJ = calcAngleJ(data.dateTime) val delta = calcSunDeclinationDelta(angleJ) @@ -104,9 +94,9 @@ final case class PvModel private ( val omegas = calculateBeamOmegas(thetaG, omega, omegaSS, omegaSR) - // === Beam Radiation ===// - val eBeamS = calcBeamRadiationOnSlopedSurface( - eBeamH, + // === Beam irradiance ===// + val gBeamS = calcBeamIrradianceOnSlopedSurface( + gBeamH, omegas, delta, lat, @@ -114,44 +104,43 @@ final case class PvModel private ( alphaE, ) - // === Diffuse Radiation Parameters ===// + // === Diffuse irradiance parameters ===// val thetaZ = calcZenithAngleThetaZ(alphaS) val airMass = calcAirMass(thetaZ) - val extraterrestrialRadiationI0 = calcExtraterrestrialRadiationI0(angleJ) + val g0 = calcExtraterrestrialRadianceG0(angleJ) - // === Diffuse Radiation ===// - val eDifS = calcDiffuseRadiationOnSlopedSurfacePerez( - eDifH, - eBeamH, + // === Diffuse irradiance ===// + val gDifS = calcDiffuseIrradianceOnSlopedSurfacePerez( + gDifH, + gBeamH, airMass, - extraterrestrialRadiationI0, + g0, thetaZ, thetaG, gammaE, ) - // === Reflected Radiation ===// - val eRefS = - calcReflectedRadiationOnSlopedSurface(eBeamH, eDifH, gammaE, albedo) + // === Reflected irradiance ===// + val gRefS = + calcReflectedIrradianceOnSlopedSurface(gBeamH, gDifH, gammaE, albedo) - // === Total Radiation ===// - val eTotal = eDifS + eBeamS + eRefS + // === Total irradiance ===// + val gTotal = gDifS + gBeamS + gRefS - val irraditionSTC = yieldSTC * duration calcOutput( - eTotal, + gTotal, data.dateTime, - irraditionSTC, + yieldSTC, ) } /** Calculates the position of the earth in relation to the sun (day angle) - * for the provided time + * for the provided time. * * @param time - * the time + * The time. * @return - * day angle J + * Day angle J. */ def calcAngleJ(time: ZonedDateTime): Angle = { val day = time.getDayOfYear // day of the year @@ -165,9 +154,9 @@ final case class PvModel private ( * of the position of the sun". Appl. Opt. 1971, 10, 2569–2571 * * @param angleJ - * day angle J + * The day angle J. * @return - * declination angle + * The declination angle. */ def calcSunDeclinationDelta( angleJ: Angle @@ -189,13 +178,13 @@ final case class PvModel private ( * on its axis at 15◦ per hour; morning negative, afternoon positive. * * @param time - * the requested time (which is transformed to solar time) + * The requested time (which is transformed to solar time). * @param angleJ - * day angle J + * The day angle J. * @param longitude - * longitude of the position + * The longitude of the position. * @return - * hour angle omega + * The hour angle omega. */ def calcHourAngleOmega( time: ZonedDateTime, @@ -221,11 +210,11 @@ final case class PvModel private ( * omegaSS. * * @param latitude - * latitude of the position + * The latitude of the position. * @param delta - * sun declination angle + * The sun declination angle. * @return - * sunset angle omegaSS + * The sunset angle omegaSS. */ def calcSunsetAngleOmegaSS( latitude: Angle, @@ -247,13 +236,13 @@ final case class PvModel private ( * the zenith angle. * * @param omega - * hour angle + * The hour angle. * @param delta - * sun declination angle + * The sun declination angle. * @param latitude - * latitude of the position + * The latitude of the position. * @return - * solar altitude angle alphaS + * The solar altitude angle alphaS. */ def calcSolarAltitudeAngleAlphaS( omega: Angle, @@ -277,7 +266,7 @@ final case class PvModel private ( /** Calculates the zenith angle thetaG which represents the angle between the * vertical and the line to the sun, that is, the angle of incidence of beam - * radiation on a horizontal surface. + * irradiance on a horizontal surface. * * @param alphaS * sun altitude angle @@ -294,13 +283,13 @@ final case class PvModel private ( } /** Calculates the ratio of the mass of atmosphere through which beam - * radiation passes to the mass it would pass through if the sun were at the + * irradiance passes to the mass it would pass through if the sun were at the * zenith (i.e., directly overhead). * * @param thetaZ - * zenith angle + * The zenith angle. * @return - * air mass + * The air mass. */ def calcAirMass(thetaZ: Angle): Double = { val thetaZInRad = thetaZ.toRadians @@ -317,17 +306,17 @@ final case class PvModel private ( ) - airMassRatio * cos(thetaZInRad) } - /** Calculates the extraterrestrial radiation, that is, the radiation that + /** Calculates the extraterrestrial irradiance, that is, the irradiance that * would be received in the absence of the atmosphere. * * @param angleJ - * day angle J + * The day angle J. * @return - * extraterrestrial radiation I0 + * The extraterrestrial irradiance G0. */ - def calcExtraterrestrialRadiationI0( + def calcExtraterrestrialRadianceG0( angleJ: Angle - ): Irradiation = { + ): Irradiance = { val jInRad = angleJ.toRadians // eccentricity correction factor @@ -338,27 +327,27 @@ final case class PvModel private ( 0.000077 * sin(2d * jInRad) // solar constant in W/m2 - val Gsc = WattHoursPerSquareMeter(1367) // solar constant - Gsc * e0 + val gSc = WattsPerSquareMeter(1367) // solar constant + gSc * e0 } - /** Calculates the angle of incidence thetaG of beam radiation on a surface + /** Calculates the angle of incidence thetaG of beam irradiance on a surface. * * @param delta - * sun declination angle + * The sun declination angle. * @param latitude - * latitude of the position + * The latitude of the position. * @param gammaE - * slope angle (the angle between the plane of the surface in question and - * the horizontal) + * The slope angle (the angle between the plane of the surface in question + * and the horizontal). * @param alphaE - * surface azimuth angle (the deviation of the projection on a horizontal - * plane of the normal to the surface from the local meridian, with zero - * due south, east negative, and west positive) + * The surface azimuth angle (the deviation of the projection on a + * horizontal plane of the normal to the surface from the local meridian, + * with zero due south, east negative, and west positive). * @param omega - * hour angle + * The hour angle. * @return - * angle of incidence thetaG + * The angle of incidence thetaG. */ def calcAngleOfIncidenceThetaG( delta: Angle, @@ -386,19 +375,19 @@ final case class PvModel private ( } /** Calculates omega1 and omega2, which are parameters for - * calcBeamRadiationOnSlopedSurface + * calcBeamIrradianceOnSlopedSurface * * @param thetaG - * angle of incidence + * The angle of incidence. * @param omega - * hour angle + * The hour angle. * @param omegaSS - * sunset angle + * The sunset angle. * @param omegaSR - * sunrise angle + * The sunrise angle. * @return - * omega1 and omega encapsulated in an Option, if applicable. None - * otherwise + * The omega1 and omega encapsulated in an Option, if applicable. None + * otherwise. */ def calculateBeamOmegas( thetaG: Angle, @@ -417,7 +406,7 @@ final case class PvModel private ( val omega2InRad = omega1InRad + omegaOneHour // requested hour plus 1 hour // (thetaG < 90°): sun is visible - // (thetaG > 90°), otherwise: sun is behind the surface -> no direct radiation + // (thetaG > 90°), otherwise: sun is behind the surface -> no direct irradiance if ( thetaGInRad < toRadians(90) // omega1 and omega2: sun has risen and has not set yet @@ -441,34 +430,34 @@ final case class PvModel private ( None } - /** Calculates the beam radiation on a sloped surface + /** Calculates the beam irradiance on a sloped surface. * - * @param eBeamH - * beam radiation on a horizontal surface + * @param gBeamH + * The beam irradiance on a horizontal surface. * @param omegas - * omega1 and omega2 + * Omega1 and omega2. * @param delta - * sun declination angle + * The sun declination angle. * @param latitude - * latitude of the position + * The latitude of the position. * @param gammaE - * slope angle (the angle between the plane of the surface in question and - * the horizontal) + * The slope angle (the angle between the plane of the surface in question + * and the horizontal). * @param alphaE - * surface azimuth angle (the deviation of the projection on a horizontal - * plane of the normal to the surface from the local meridian, with zero - * due south, east negative, and west positive) + * The surface azimuth angle (the deviation of the projection on a + * horizontal plane of the normal to the surface from the local meridian, + * with zero due south, east negative, and west positive). * @return - * the beam radiation on the sloped surface + * The beam irradiance on the sloped surface. */ - def calcBeamRadiationOnSlopedSurface( - eBeamH: Irradiation, + def calcBeamIrradianceOnSlopedSurface( + gBeamH: Irradiance, omegas: Option[(Angle, Angle)], delta: Angle, latitude: Angle, gammaE: Angle, alphaE: Angle, - ): Irradiation = { + ): Irradiance = { omegas match { case Some((omega1, omega2)) => @@ -500,60 +489,60 @@ final case class PvModel private ( // in rare cases (close to sunrise) r can become negative (although very small) val r = max(a / b, 0d) - eBeamH * r - case None => WattHoursPerSquareMeter(0d) + gBeamH * r + case None => WattsPerSquareMeter(0d) } } - /** Calculates the diffuse radiation on a sloped surface based on the model of - * Perez et al. + /** Calculates the diffuse irradiance on a sloped surface based on the model + * of Perez et al. * *

Formula taken from Perez, R., P. Ineichen, R. Seals, J. Michalsky, and * R. Stewart, "Modeling Daylight Availability and Irradiance Components from * Direct and Global Irradiance". Solar Energy, 44, 271 (1990). * - * @param eDifH - * diffuse radiation on a horizontal surface - * @param eBeamH - * beam radiation on a horizontal surface + * @param gDifH + * The diffuse irradiance on a horizontal surface. + * @param gBeamH + * The beam irradiance on a horizontal surface. * @param airMass - * the air mass - * @param extraterrestrialRadiationI0 - * extraterrestrial radiation + * The air mass. + * @param extraterrestrialRadianceG0 + * The extraterrestrial irradiance. * @param thetaZ - * zenith angle + * The zenith angle. * @param thetaG - * angle of incidence + * The angle of incidence. * @param gammaE - * slope angle (the angle between the plane of the surface in question and - * the horizontal) + * The slope angle (the angle between the plane of the surface in question + * and the horizontal). * @return - * the diffuse radiation on the sloped surface + * The diffuse irradiance on the sloped surface. */ - def calcDiffuseRadiationOnSlopedSurfacePerez( - eDifH: Irradiation, - eBeamH: Irradiation, + def calcDiffuseIrradianceOnSlopedSurfacePerez( + gDifH: Irradiance, + gBeamH: Irradiance, airMass: Double, - extraterrestrialRadiationI0: Irradiation, + extraterrestrialRadianceG0: Irradiance, thetaZ: Angle, thetaG: Angle, gammaE: Angle, - ): Irradiation = { + ): Irradiance = { val thetaZInRad = thetaZ.toRadians val thetaGInRad = thetaG.toRadians val gammaEInRad = gammaE.toRadians // == brightness index beta ==// - val delta = eDifH * airMass / extraterrestrialRadiationI0 + val delta = gDifH * airMass / extraterrestrialRadianceG0 // == cloud index epsilon ==// - val x = if (eDifH.value.doubleValue > 0) { - // if we have diffuse radiation on horizontal surface we have to consider + val x = if (gDifH.value.doubleValue > 0) { + // if we have diffuse irradiance on horizontal surface we have to consider // the clearness parameter epsilon, which then gives us an epsilon bin x - // Beam radiation is required on a plane normal to the beam direction (normal incidence), + // Beam irradiance is required on a plane normal to the beam direction (normal incidence), // thus dividing by cos theta_z - var epsilon = ((eDifH + eBeamH / cos(thetaZInRad)) / eDifH + + var epsilon = ((gDifH + gBeamH / cos(thetaZInRad)) / gDifH + (5.535d * 1.0e-6) * pow( thetaZ.toDegrees, 3, @@ -615,36 +604,36 @@ final case class PvModel private ( val aPerez = max(0, cos(thetaGInRad)) val bPerez = max(cos(1.4835298641951802), cos(thetaZInRad)) - // finally calculate the diffuse radiation on an inclined surface - eDifH * ( + // finally calculate the diffuse irradiance on an inclined surface + gDifH * ( ((1 + cos(gammaEInRad)) / 2) * (1 - f1) + (f1 * (aPerez / bPerez)) + (f2 * sin(gammaEInRad)) ) } - /** Calculates the reflected radiation on a sloped surface + /** Calculates the reflected irradiance on a sloped surface. * - * @param eBeamH - * beam radiation on a horizontal surface - * @param eDifH - * diffuse radiation on a horizontal surface + * @param gBeamH + * The beam irradiance on a horizontal surface. + * @param gDifH + * The diffuse irradiance on a horizontal surface. * @param gammaE - * slope angle (the angle between the plane of the surface in question and - * the horizontal) + * The slope angle (the angle between the plane of the surface in question + * and the horizontal). * @param albedo - * albedo / "composite" ground reflection + * The albedo / "composite" ground reflection. * @return - * the reflected radiation on the sloped surface eRefS + * The reflected irradiance on the sloped surface eRefS. */ - def calcReflectedRadiationOnSlopedSurface( - eBeamH: Irradiation, - eDifH: Irradiation, + def calcReflectedIrradianceOnSlopedSurface( + gBeamH: Irradiance, + gDifH: Irradiance, gammaE: Angle, albedo: Double, - ): Irradiation = { + ): Irradiance = { val gammaEInRad = gammaE.toRadians - (eBeamH + eDifH) * (albedo * 0.5 * (1 - cos(gammaEInRad))) + (gBeamH + gDifH) * albedo * 0.5 * (1 - cos(gammaEInRad)) } private def generatorCorrectionFactor( @@ -681,9 +670,9 @@ final case class PvModel private ( } private def calcOutput( - eTotalInWhPerSM: Irradiation, + gTotal: Irradiance, time: ZonedDateTime, - irradiationSTC: Irradiation, + irradianceSTC: Irradiance, ): Power = { val genCorr = generatorCorrectionFactor(time, gammaE) val tempCorr = temperatureCorrectionFactor(time) @@ -691,11 +680,11 @@ final case class PvModel private ( * area. The yield also takes care of generator and temperature correction factors as well as the converter's * efficiency */ val actYield = - eTotalInWhPerSM * moduleSurface.toSquareMeters * etaConv.toEach * (genCorr * tempCorr) + gTotal * moduleSurface.toSquareMeters * etaConv.toEach * (genCorr * tempCorr) /* Calculate the foreseen active power output without boundary condition adaptions */ val proposal = - sRated.toActivePower(cosPhiRated) * -1 * (actYield / irradiationSTC) + sRated.toActivePower(cosPhiRated) * -1 * (actYield / irradianceSTC) /* Do sanity check, if the proposed feed in is above the estimated maximum to be apparent active power of the plant */ if (proposal < pMax) @@ -731,21 +720,17 @@ final case class PvModel private ( object PvModel { - /** Class that holds all relevant data for a pv model calculation + /** Class that holds all relevant data for a pv model calculation. * * @param dateTime - * date and time of the ending of time frame to calculate - * @param weatherDataFrameLength - * the duration in ticks (= seconds) the provided irradiance is received by - * the pv panel + * Date and time of the ending of time frame to calculate. * @param diffIrradiance - * diffuse solar irradiance + * The diffuse solar irradiance on a horizontal surface. * @param dirIrradiance - * direct solar irradiance + * The direct solar irradiance on a horizontal surface. */ final case class PvRelevantData( dateTime: ZonedDateTime, - weatherDataFrameLength: Long, diffIrradiance: Irradiance, dirIrradiance: Irradiance, ) extends CalcRelevantData diff --git a/src/main/scala/edu/ie3/simona/model/participant/load/FixedLoadModel.scala b/src/main/scala/edu/ie3/simona/model/participant/load/FixedLoadModel.scala deleted file mode 100644 index 48ef0d4eb7..0000000000 --- a/src/main/scala/edu/ie3/simona/model/participant/load/FixedLoadModel.scala +++ /dev/null @@ -1,110 +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.model.participant.load - -import edu.ie3.datamodel.models.input.system.LoadInput -import edu.ie3.simona.model.participant.CalcRelevantData.LoadRelevantData -import edu.ie3.simona.model.participant.ModelState.ConstantState -import edu.ie3.simona.model.participant.control.QControl -import edu.ie3.simona.model.participant.load.FixedLoadModel.FixedLoadRelevantData -import edu.ie3.simona.model.participant.load.LoadReference.{ - ActivePower, - EnergyConsumption, -} -import edu.ie3.util.quantities.PowerSystemUnits -import edu.ie3.util.scala.OperationInterval -import edu.ie3.util.scala.quantities.{ApparentPower, Kilovoltamperes} -import squants.Power -import squants.time.Days - -import java.util.UUID - -/** Load model always consuming the same, constant power - * - * @param uuid - * unique identifier - * @param id - * human-readable id - * @param operationInterval - * Interval, in which the system is in operation - * @param qControl - * Type of reactive power control - * @param sRated - * Rated apparent power - * @param cosPhiRated - * Rated power factor - * @param reference - * Scale the fixed output to this reference - */ -final case class FixedLoadModel( - uuid: UUID, - id: String, - operationInterval: OperationInterval, - qControl: QControl, - sRated: ApparentPower, - cosPhiRated: Double, - reference: LoadReference, -) extends LoadModel[FixedLoadRelevantData.type]( - uuid, - id, - operationInterval, - qControl, - sRated, - cosPhiRated, - ) { - - val activePower: Power = reference match { - case ActivePower(power) => power - case EnergyConsumption(energyConsumption) => - val duration = Days(365d) - energyConsumption / duration - } - - /** Calculate the active power behaviour of the model - * - * @param data - * Further needed, secondary data. Due to the nature of a fixed load model, - * no further data is needed. - * @return - * Active power - */ - override def calculateActivePower( - modelState: ConstantState.type, - data: FixedLoadRelevantData.type = FixedLoadRelevantData, - ): Power = activePower -} - -object FixedLoadModel { - case object FixedLoadRelevantData extends LoadRelevantData - - def apply( - input: LoadInput, - scalingFactor: Double, - operationInterval: OperationInterval, - reference: LoadReference, - ): FixedLoadModel = { - - val scaledInput = input.copy().scale(scalingFactor).build() - - val model = FixedLoadModel( - scaledInput.getUuid, - scaledInput.getId, - operationInterval, - QControl(scaledInput.getqCharacteristics()), - Kilovoltamperes( - scaledInput.getsRated - .to(PowerSystemUnits.KILOVOLTAMPERE) - .getValue - .doubleValue - ), - scaledInput.getCosPhiRated, - reference, - ) - model.enable() - model - } -} diff --git a/src/main/scala/edu/ie3/simona/model/participant/load/LoadModel.scala b/src/main/scala/edu/ie3/simona/model/participant/load/LoadModel.scala deleted file mode 100644 index 6eb5a6fd0d..0000000000 --- a/src/main/scala/edu/ie3/simona/model/participant/load/LoadModel.scala +++ /dev/null @@ -1,143 +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.model.participant.load - -import com.typesafe.scalalogging.LazyLogging -import edu.ie3.datamodel.models.input.system.LoadInput -import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ComplexPower -import edu.ie3.simona.model.participant.CalcRelevantData.LoadRelevantData -import edu.ie3.simona.model.participant.ModelState.ConstantState -import edu.ie3.simona.model.participant.control.QControl -import edu.ie3.simona.model.participant.{ - ApparentPowerParticipant, - FlexChangeIndicator, - SystemParticipant, -} -import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage.ProvideFlexOptions -import edu.ie3.simona.ontology.messages.flex.MinMaxFlexibilityMessage.ProvideMinMaxFlexOptions -import edu.ie3.util.quantities.PowerSystemUnits -import edu.ie3.util.scala.OperationInterval -import edu.ie3.util.scala.quantities.{ApparentPower, Kilovoltamperes} -import squants.{Energy, Power} - -import java.util.UUID - -/** Abstract super class of a load model. - * - * @tparam D - * Type of data, that is needed for model calculation - */ -abstract class LoadModel[D <: LoadRelevantData]( - uuid: UUID, - id: String, - operationInterval: OperationInterval, - qControl: QControl, - sRated: ApparentPower, - cosPhiRated: Double, -) extends SystemParticipant[D, ComplexPower, ConstantState.type]( - uuid, - id, - operationInterval, - qControl, - sRated, - cosPhiRated, - ) - with ApparentPowerParticipant[D, ConstantState.type] { - - override def determineFlexOptions( - data: D, - lastState: ConstantState.type, - ): ProvideFlexOptions = - ProvideMinMaxFlexOptions.noFlexOption( - uuid, - calculateActivePower(lastState, data), - ) - - override def handleControlledPowerChange( - data: D, - lastState: ConstantState.type, - setPower: Power, - ): (ConstantState.type, FlexChangeIndicator) = - (lastState, FlexChangeIndicator()) -} - -object LoadModel extends LazyLogging { - - /** Scale profile based load models' sRated based on a provided active power - * value - * - * When the load is scaled to the active power value, the models' sRated is - * multiplied by the ratio of the provided active power value and the active - * power value of the model (activePowerVal / (input.sRated*input.cosPhi) - * - * @param inputModel - * the input model instance - * @param activePower - * the active power value sRated should be scaled to - * @param safetyFactor - * a safety factor to address potential higher sRated values than the - * original scaling would provide (e.g. when using unrestricted probability - * functions) - * @return - * the inputs model sRated scaled to the provided active power - */ - def scaleSRatedActivePower( - inputModel: LoadInput, - activePower: Power, - safetyFactor: Double = 1d, - ): ApparentPower = { - val sRated = Kilovoltamperes( - inputModel.getsRated - .to(PowerSystemUnits.KILOVOLTAMPERE) - .getValue - .doubleValue - ) - val pRated = sRated.toActivePower(inputModel.getCosPhiRated) - val referenceScalingFactor = activePower / pRated - sRated * referenceScalingFactor * safetyFactor - } - - /** Scale profile based load model's sRated based on the provided yearly - * energy consumption - * - * When the load is scaled based on the consumed energy per year, the - * installed sRated capacity is not usable anymore instead, the load's rated - * apparent power is scaled on the maximum power occurring in the specified - * load profile multiplied by the ratio of the annual consumption and the - * standard load profile scale - * - * @param inputModel - * the input model instance - * @param energyConsumption - * the yearly energy consumption the models' sRated should be scaled to - * @param profileMaxPower - * the maximum power value of the profile - * @param profileEnergyScaling - * the energy scaling factor of the profile (= amount of yearly energy the - * profile is scaled to) - * @param safetyFactor - * a safety factor to address potential higher sRated values than the - * original scaling would provide (e.g. when using unrestricted probability - * functions) - * @return - * he inputs model sRated scaled to the provided energy consumption - */ - def scaleSRatedEnergy( - inputModel: LoadInput, - energyConsumption: Energy, - profileMaxPower: Power, - profileEnergyScaling: Energy, - safetyFactor: Double = 1d, - ): ApparentPower = { - val power = (profileMaxPower / inputModel.getCosPhiRated) * ( - energyConsumption / profileEnergyScaling - ) * safetyFactor - - Kilovoltamperes(power.toKilowatts) - } - -} diff --git a/src/main/scala/edu/ie3/simona/model/participant/load/LoadReference.scala b/src/main/scala/edu/ie3/simona/model/participant/load/LoadReference.scala deleted file mode 100644 index df97792e69..0000000000 --- a/src/main/scala/edu/ie3/simona/model/participant/load/LoadReference.scala +++ /dev/null @@ -1,101 +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.model.participant.load - -import edu.ie3.datamodel.models.input.system.LoadInput -import edu.ie3.simona.config.RuntimeConfig.LoadRuntimeConfig -import edu.ie3.util.StringUtils -import edu.ie3.util.quantities.PowerSystemUnits.{MEGAWATT, MEGAWATTHOUR} -import squants.energy.{MegawattHours, Megawatts} -import squants.{Energy, Power} - -/** Denoting difference referencing scenarios for scaling load model output - */ -sealed trait LoadReference { - val key: String - - def getKey: String = key - - def scale(factor: Double): LoadReference -} -object LoadReference { - - /** Scale the load model behaviour to reach the given active power in max - * - * @param power - * Foreseen active power - */ - final case class ActivePower(power: Power) extends LoadReference { - override val key: String = "power" - - override def scale(factor: Double): ActivePower = - copy(power = power * factor) - } - - /** Scale the load model behaviour to reach the given annual energy - * consumption - * - * @param energyConsumption - * Annual energy consumption to reach - */ - final case class EnergyConsumption( - energyConsumption: Energy - ) extends LoadReference { - override val key: String = "energy" - - override def scale(factor: Double): LoadReference = - copy(energyConsumption = energyConsumption * factor) - } - - def isEligibleKey(key: String): Boolean = { - Set("power", "energy").contains(key) - } - - /** Build a reference object, that denotes, to which reference a load model - * behaviour might be scaled. If the behaviour is meant to be scaled to - * energy consumption and no annual energy consumption is given, an - * [[IllegalArgumentException]] is thrown - * - * @param inputModel - * [[LoadInput]] to derive energy information from - * @param modelConfig - * Configuration of model behaviour - * @return - * A [[LoadReference]] for use in [[LoadModel]] - */ - def apply( - inputModel: LoadInput, - modelConfig: LoadRuntimeConfig, - ): LoadReference = - StringUtils.cleanString(modelConfig.reference).toLowerCase match { - case "power" => - val activePower = Megawatts( - inputModel - .getsRated() - .to(MEGAWATT) - .getValue - .doubleValue - ) * - inputModel.getCosPhiRated - LoadReference.ActivePower(activePower) - case "energy" => - Option(inputModel.geteConsAnnual()) match { - case Some(consumption) => - LoadReference.EnergyConsumption( - MegawattHours(consumption.to(MEGAWATTHOUR).getValue.doubleValue) - ) - case None => - throw new IllegalArgumentException( - s"Load model with uuid ${inputModel.getUuid} is meant to be scaled to annual energy consumption, but the energy is not provided." - ) - } - case unsupported => - throw new IllegalArgumentException( - s"Load model with uuid ${inputModel.getUuid} is meant to be scaled to unsupported reference '$unsupported'." - ) - } -} diff --git a/src/main/scala/edu/ie3/simona/model/participant/load/profile/ProfileLoadModel.scala b/src/main/scala/edu/ie3/simona/model/participant/load/profile/ProfileLoadModel.scala deleted file mode 100644 index e30390bb74..0000000000 --- a/src/main/scala/edu/ie3/simona/model/participant/load/profile/ProfileLoadModel.scala +++ /dev/null @@ -1,150 +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.model.participant.load.profile - -import edu.ie3.datamodel.models.input.system.LoadInput -import edu.ie3.datamodel.models.profile.StandardLoadProfile -import edu.ie3.simona.model.participant.CalcRelevantData.LoadRelevantData -import edu.ie3.simona.model.participant.ModelState.ConstantState -import edu.ie3.simona.model.participant.control.QControl -import edu.ie3.simona.model.participant.load.LoadReference._ -import edu.ie3.simona.model.participant.load.profile.ProfileLoadModel.ProfileRelevantData -import edu.ie3.simona.model.participant.load.{LoadModel, LoadReference} -import edu.ie3.util.scala.OperationInterval -import edu.ie3.util.scala.quantities.ApparentPower -import squants.Power - -import java.time.ZonedDateTime -import java.util.UUID - -/** Power model consuming power according to standard load profiles - * - * @param uuid - * unique identifier - * @param id - * human-readable id - * @param operationInterval - * Interval, in which the system is in operation - * @param qControl - * Type of reactive power control - * @param sRated - * Rated apparent power - * @param cosPhiRated - * Rated power factor - * @param loadProfile - * The load profile to take - * @param reference - * Scale the profiles to this reference - */ -final case class ProfileLoadModel( - uuid: UUID, - id: String, - operationInterval: OperationInterval, - qControl: QControl, - sRated: ApparentPower, - cosPhiRated: Double, - loadProfile: StandardLoadProfile, - reference: LoadReference, -) extends LoadModel[ProfileRelevantData]( - uuid, - id, - operationInterval, - qControl, - sRated, - cosPhiRated, - ) { - - private val loadProfileStore: LoadProfileStore = LoadProfileStore() - - /* maximum energy throughout the year of the selected load profile*/ - private val profileMaxPower = loadProfileStore.maxPower(loadProfile) - - /* energy reference is always models yearly energy consumption divided by the energy the profile is scaled to */ - private lazy val energyReferenceScalingFactor = - reference match { - case EnergyConsumption(energyConsumption) => - energyConsumption / LoadProfileStore.defaultLoadProfileEnergyScaling - case _ => - throw new IllegalArgumentException( - s"Applying energy reference scaling factor for reference mode '$reference' is not supported!" - ) - } - - /** Calculate the active power behaviour of the model - * - * @param data - * Further needed, secondary data - * @return - * Active power - */ - override protected def calculateActivePower( - modelState: ConstantState.type, - data: ProfileRelevantData, - ): Power = { - /* The power comes in W and is delivered all 15 minutes */ - val averagePower: Power = loadProfileStore - .entry(data.date, loadProfile) - - reference match { - case ActivePower(activePower) => - /* scale the reference active power based on the profiles averagePower/maxPower ratio */ - val referenceScalingFactor = averagePower / profileMaxPower - activePower * referenceScalingFactor - case _: EnergyConsumption => - /* scale the profiles average power based on the energyConsumption/profileEnergyScaling(=1000kWh/year) ratio */ - averagePower * energyReferenceScalingFactor - } - } -} - -object ProfileLoadModel { - - final case class ProfileRelevantData(date: ZonedDateTime) - extends LoadRelevantData - - def apply( - input: LoadInput, - operationInterval: OperationInterval, - scalingFactor: Double, - reference: LoadReference, - ): ProfileLoadModel = { - - val scaledReference = reference.scale(scalingFactor) - val scaledInput = input.copy().scale(scalingFactor).build() - - val scaledSRated = scaledReference match { - case LoadReference.ActivePower(power) => - LoadModel.scaleSRatedActivePower(scaledInput, power) - - case LoadReference.EnergyConsumption(energyConsumption) => - val loadProfileMax = - LoadProfileStore().maxPower( - scaledInput.getLoadProfile.asInstanceOf[StandardLoadProfile] - ) - LoadModel.scaleSRatedEnergy( - scaledInput, - energyConsumption, - loadProfileMax, - LoadProfileStore.defaultLoadProfileEnergyScaling, - ) - } - - val model = ProfileLoadModel( - scaledInput.getUuid, - scaledInput.getId, - operationInterval, - QControl.apply(scaledInput.getqCharacteristics()), - scaledSRated, - scaledInput.getCosPhiRated, - scaledInput.getLoadProfile.asInstanceOf[StandardLoadProfile], - scaledReference, - ) - - model.enable() - model - } -} diff --git a/src/main/scala/edu/ie3/simona/model/participant/load/random/RandomLoadModel.scala b/src/main/scala/edu/ie3/simona/model/participant/load/random/RandomLoadModel.scala deleted file mode 100644 index bb0d95a9c8..0000000000 --- a/src/main/scala/edu/ie3/simona/model/participant/load/random/RandomLoadModel.scala +++ /dev/null @@ -1,213 +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.model.participant.load.random - -import de.lmu.ifi.dbs.elki.math.statistics.distribution.GeneralizedExtremeValueDistribution -import de.lmu.ifi.dbs.elki.utilities.random.RandomFactory -import edu.ie3.datamodel.models.input.system.LoadInput -import edu.ie3.simona.model.participant.CalcRelevantData.LoadRelevantData -import edu.ie3.simona.model.participant.ModelState.ConstantState -import edu.ie3.simona.model.participant.control.QControl -import edu.ie3.simona.model.participant.load.LoadReference._ -import edu.ie3.simona.model.participant.load.random.RandomLoadModel.RandomRelevantData -import edu.ie3.simona.model.participant.load.{DayType, LoadModel, LoadReference} -import edu.ie3.util.TimeUtil -import edu.ie3.util.scala.OperationInterval -import edu.ie3.util.scala.quantities.ApparentPower -import squants.Power -import squants.energy.{KilowattHours, Kilowatts, Watts} - -import java.time.ZonedDateTime -import java.util.UUID -import scala.annotation.tailrec -import scala.collection.mutable -import scala.util.Random - -/** A load model consuming energy followed by time resolved probability. The - * referencing to rated active power maps the output's 95 % quantile to this - * value - * - * @param uuid - * unique identifier - * @param id - * human-readable id - * @param operationInterval - * Interval, in which the system is in operation - * @param qControl - * Type of reactive power control - * @param sRated - * Rated apparent power - * @param cosPhiRated - * Rated power factor - * @param reference - * Scale the random consumption to this reference - */ -final case class RandomLoadModel( - uuid: UUID, - id: String, - operationInterval: OperationInterval, - qControl: QControl, - sRated: ApparentPower, - cosPhiRated: Double, - reference: LoadReference, -) extends LoadModel[RandomRelevantData]( - uuid, - id, - operationInterval, - qControl, - sRated, - cosPhiRated, - ) { - - private lazy val energyReferenceScalingFactor = reference match { - case EnergyConsumption(energyConsumption) => - energyConsumption / RandomLoadModel.randomProfileEnergyScaling - case _ => - throw new IllegalArgumentException( - s"Applying energy reference scaling factor for reference mode '$reference' is not supported!" - ) - } - - private val randomLoadParamStore = RandomLoadParamStore() - - private type GevKey = (DayType.Value, Int) - private val gevStorage = - mutable.Map.empty[GevKey, GeneralizedExtremeValueDistribution] - - /** Calculate the active power behaviour of the model - * - * @param data - * Further needed, secondary data - * @return - * Active power - */ - @tailrec - override protected def calculateActivePower( - modelState: ConstantState.type, - data: RandomRelevantData, - ): Power = { - val gev = getGevDistribution(data.date) - - /* Get a next random power (in kW) */ - val randomPower = gev.nextRandom() - if (randomPower < 0) - calculateActivePower(modelState, data) - else { - val profilePower = Kilowatts(randomPower) - val activePower = reference match { - case ActivePower(activePower) => - /* scale the reference active power based on the random profiles averagePower/maxPower ratio */ - val referenceScalingFactor = - profilePower / RandomLoadModel.randomMaxPower - activePower * referenceScalingFactor - case _: EnergyConsumption => - /* scale the profiles random power based on the energyConsumption/profileEnergyScaling(=1000kWh/year) ratio */ - profilePower * energyReferenceScalingFactor - } - activePower - } - } - - /** Get the needed generalized extreme value distribution from the store or - * instantiate a new one and put it to the store. - * - * @param dateTime - * Questioned date time - * @return - * The needed generalized extreme value distribution - */ - private def getGevDistribution( - dateTime: ZonedDateTime - ): GeneralizedExtremeValueDistribution = { - /* Determine identifying key for a distinct generalized extreme value distribution and look it up. If it is not - * available, yet, instantiate one. */ - val key: GevKey = ( - DayType(dateTime.getDayOfWeek), - TimeUtil.withDefaults.getQuarterHourOfDay(dateTime), - ) - gevStorage.get(key) match { - case Some(foundIt) => foundIt - case None => - /* Instantiate new gev distribution, put it to storage and return it */ - val randomFactory = RandomFactory.get(Random.nextLong()) - val gevParameters = randomLoadParamStore.parameters(dateTime) - val newGev = new GeneralizedExtremeValueDistribution( - gevParameters.my, - gevParameters.sigma, - gevParameters.k, - randomFactory, - ) - gevStorage += (key -> newGev) - newGev - } - } -} - -object RandomLoadModel { - - final case class RandomRelevantData(date: ZonedDateTime) - extends LoadRelevantData - - /** The profile energy scaling factor, the random profile is scaled to. - * - * It is said in 'Kays - Agent-based simulation environment for improving the - * planning of distribution grids', that the Generalized Extreme Value - * distribution's parameters are sampled from input data, that is normalized - * to 1,000 kWh annual energy consumption. However, due to inaccuracies in - * random data reproduction, the sampled values will lead to an average - * annual energy consumption of approx. this value. It has been found by - * 1,000 evaluations of the year 2019. - */ - private val randomProfileEnergyScaling = KilowattHours(716.5416966513656) - - /** This is the 95 % quantile resulting from 10,000 evaluations of the year - * 2019. It is only needed, when the load is meant to be scaled to rated - * active power. - * - * @return - * Reference active power to use for later model calculations - */ - private val randomMaxPower: Power = Watts(159d) - - def apply( - input: LoadInput, - operationInterval: OperationInterval, - scalingFactor: Double, - reference: LoadReference, - ): RandomLoadModel = { - - val scaledReference = reference.scale(scalingFactor) - val scaledInput = input.copy().scale(scalingFactor).build() - - val scaledSRated = scaledReference match { - case ActivePower(power) => - LoadModel.scaleSRatedActivePower(scaledInput, power, 1.1) - - case EnergyConsumption(energyConsumption) => - LoadModel.scaleSRatedEnergy( - scaledInput, - energyConsumption, - randomMaxPower, - randomProfileEnergyScaling, - 1.1, - ) - } - - val model = RandomLoadModel( - scaledInput.getUuid, - scaledInput.getId, - operationInterval, - QControl.apply(scaledInput.getqCharacteristics()), - scaledSRated, - scaledInput.getCosPhiRated, - scaledReference, - ) - - model.enable() - model - } -} diff --git a/src/main/scala/edu/ie3/simona/model/participant2/FixedFeedInModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/FixedFeedInModel.scala new file mode 100644 index 0000000000..3e1ddbabde --- /dev/null +++ b/src/main/scala/edu/ie3/simona/model/participant2/FixedFeedInModel.scala @@ -0,0 +1,106 @@ +/* + * © 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.participant2 + +import edu.ie3.datamodel.models.input.system.FixedFeedInInput +import edu.ie3.datamodel.models.result.system.{ + FixedFeedInResult, + SystemParticipantResult, +} +import edu.ie3.simona.agent.participant.data.Data.PrimaryData.{ + ComplexPower, + PrimaryDataWithComplexPower, +} +import edu.ie3.simona.model.participant.control.QControl +import edu.ie3.simona.model.participant2.ParticipantFlexibility.ParticipantSimpleFlexibility +import edu.ie3.simona.model.participant2.ParticipantModel.{ + ActivePowerOperatingPoint, + FixedState, + ParticipantFixedState, +} +import edu.ie3.simona.service.ServiceType +import edu.ie3.util.quantities.PowerSystemUnits +import edu.ie3.util.quantities.QuantityUtils.RichQuantityDouble +import edu.ie3.util.scala.quantities.{ApparentPower, Kilovoltamperes} + +import java.time.ZonedDateTime +import java.util.UUID + +class FixedFeedInModel( + override val uuid: UUID, + override val id: String, + override val sRated: ApparentPower, + override val cosPhiRated: Double, + override val qControl: QControl, +) extends ParticipantModel[ + ActivePowerOperatingPoint, + FixedState, + ] + with ParticipantFixedState[ActivePowerOperatingPoint] + with ParticipantSimpleFlexibility[FixedState] { + + override def determineOperatingPoint( + state: ParticipantModel.FixedState + ): (ActivePowerOperatingPoint, Option[Long]) = { + val power = pRated * -1 + + (ActivePowerOperatingPoint(power), None) + } + + override def zeroPowerOperatingPoint: ActivePowerOperatingPoint = + ActivePowerOperatingPoint.zero + + override def createResults( + state: ParticipantModel.FixedState, + lastOperatingPoint: Option[ActivePowerOperatingPoint], + currentOperatingPoint: ActivePowerOperatingPoint, + complexPower: ComplexPower, + dateTime: ZonedDateTime, + ): Iterable[SystemParticipantResult] = + Iterable( + new FixedFeedInResult( + dateTime, + uuid, + complexPower.p.toMegawatts.asMegaWatt, + complexPower.q.toMegavars.asMegaVar, + ) + ) + + override def createPrimaryDataResult( + data: PrimaryDataWithComplexPower[_], + dateTime: ZonedDateTime, + ): SystemParticipantResult = + new FixedFeedInResult( + dateTime, + uuid, + data.p.toMegawatts.asMegaWatt, + data.q.toMegavars.asMegaVar, + ) + + override def getRequiredSecondaryServices: Iterable[ServiceType] = + Iterable.empty + +} + +object FixedFeedInModel { + def apply( + input: FixedFeedInInput + ): FixedFeedInModel = { + new FixedFeedInModel( + input.getUuid, + input.getId, + Kilovoltamperes( + input.getsRated + .to(PowerSystemUnits.KILOVOLTAMPERE) + .getValue + .doubleValue + ), + input.getCosPhiRated, + QControl.apply(input.getqCharacteristics), + ) + } +} diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala index f71b8d9c84..17e7507aa4 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala @@ -13,7 +13,10 @@ import edu.ie3.simona.agent.participant.data.Data.{ PrimaryData, PrimaryDataExtra, } -import edu.ie3.simona.config.RuntimeConfig.BaseRuntimeConfig +import edu.ie3.simona.config.RuntimeConfig.{ + BaseRuntimeConfig, + LoadRuntimeConfig, +} import edu.ie3.simona.exceptions.CriticalFailureException import edu.ie3.simona.model.participant2.ParticipantModel.{ ModelState, @@ -56,15 +59,16 @@ object ParticipantModelInit { }).build() (scaledParticipantInput, modelConfig) match { - case (input: LoadInput, _) => - LoadModel(input) + case (input: FixedFeedInInput, _) => + FixedFeedInModel(input) + case (input: LoadInput, config: LoadRuntimeConfig) => + LoadModel(input, config) case (input: PvInput, _) => PvModel(input) case (input, config) => throw new CriticalFailureException( - s"Handling the input model ${input.getClass.getSimpleName} or " + - "the combination of the input model with model config " + - s"${config.getClass.getSimpleName} is not implemented." + s"Handling the input model ${input.getClass.getSimpleName} and " + + s"model config ${config.getClass.getSimpleName} is not implemented." ) } } diff --git a/src/main/scala/edu/ie3/simona/model/participant/load/DayType.scala b/src/main/scala/edu/ie3/simona/model/participant2/load/DayType.scala similarity index 96% rename from src/main/scala/edu/ie3/simona/model/participant/load/DayType.scala rename to src/main/scala/edu/ie3/simona/model/participant2/load/DayType.scala index 462bb03d1d..7715b31665 100644 --- a/src/main/scala/edu/ie3/simona/model/participant/load/DayType.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/load/DayType.scala @@ -4,7 +4,7 @@ * Research group Distribution grid planning and operation */ -package edu.ie3.simona.model.participant.load +package edu.ie3.simona.model.participant2.load import java.time.DayOfWeek import java.time.DayOfWeek.{SATURDAY, SUNDAY} diff --git a/src/main/scala/edu/ie3/simona/model/participant2/load/FixedLoadModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/load/FixedLoadModel.scala new file mode 100644 index 0000000000..475fa2c6a2 --- /dev/null +++ b/src/main/scala/edu/ie3/simona/model/participant2/load/FixedLoadModel.scala @@ -0,0 +1,76 @@ +/* + * © 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.participant2.load + +import edu.ie3.datamodel.models.input.system.LoadInput +import edu.ie3.simona.config.RuntimeConfig.LoadRuntimeConfig +import edu.ie3.simona.model.participant.control.QControl +import edu.ie3.simona.model.participant2.ParticipantModel.{ + ActivePowerOperatingPoint, + FixedState, + ParticipantFixedState, +} +import edu.ie3.util.quantities.PowerSystemUnits +import edu.ie3.util.quantities.PowerSystemUnits.KILOWATTHOUR +import edu.ie3.util.scala.quantities.{ApparentPower, Kilovoltamperes} +import squants.time.Days +import squants.Power +import squants.energy.KilowattHours + +import java.util.UUID + +class FixedLoadModel( + override val uuid: UUID, + override val id: String, + override val sRated: ApparentPower, + override val cosPhiRated: Double, + override val qControl: QControl, + private val activePower: Power, +) extends LoadModel[FixedState] + with ParticipantFixedState[ActivePowerOperatingPoint] { + + override def determineOperatingPoint( + state: FixedState + ): (ActivePowerOperatingPoint, Option[Long]) = + (ActivePowerOperatingPoint(activePower), None) + +} + +object FixedLoadModel { + def apply( + input: LoadInput, + config: LoadRuntimeConfig, + ): FixedLoadModel = { + val referenceType = LoadReferenceType(config.reference) + + val sRated = Kilovoltamperes( + input.getsRated + .to(PowerSystemUnits.KILOVOLTAMPERE) + .getValue + .doubleValue + ) + + val activePower: Power = referenceType match { + case LoadReferenceType.ACTIVE_POWER => + sRated.toActivePower(input.getCosPhiRated) + case LoadReferenceType.ENERGY_CONSUMPTION => + val eConsAnnual = KilowattHours( + input.geteConsAnnual().to(KILOWATTHOUR).getValue.doubleValue + ) + eConsAnnual / Days(365d) + } + + new FixedLoadModel( + input.getUuid, + input.getId, + sRated, + input.getCosPhiRated, + QControl.apply(input.getqCharacteristics), + activePower, + ) + } +} diff --git a/src/main/scala/edu/ie3/simona/model/participant2/load/LoadModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/load/LoadModel.scala index 730592c146..65c662b2cd 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/load/LoadModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/load/LoadModel.scala @@ -7,89 +7,139 @@ package edu.ie3.simona.model.participant2.load import edu.ie3.datamodel.models.input.system.LoadInput -import edu.ie3.datamodel.models.result.system.SystemParticipantResult +import edu.ie3.datamodel.models.result.system.{ + LoadResult, + SystemParticipantResult, +} import edu.ie3.simona.agent.participant.data.Data.PrimaryData.{ ComplexPower, PrimaryDataWithComplexPower, } -import edu.ie3.simona.model.participant.control.QControl +import edu.ie3.simona.config.RuntimeConfig.LoadRuntimeConfig import edu.ie3.simona.model.participant2.ParticipantFlexibility.ParticipantSimpleFlexibility import edu.ie3.simona.model.participant2.ParticipantModel import edu.ie3.simona.model.participant2.ParticipantModel.{ ActivePowerOperatingPoint, - FixedState, + ModelState, } +import edu.ie3.simona.model.participant2.load.profile.ProfileLoadModel +import edu.ie3.simona.model.participant2.load.random.RandomLoadModel import edu.ie3.simona.service.ServiceType -import edu.ie3.util.quantities.PowerSystemUnits +import edu.ie3.util.quantities.PowerSystemUnits.{KILOVOLTAMPERE, KILOWATTHOUR} +import edu.ie3.util.quantities.QuantityUtils.RichQuantityDouble import edu.ie3.util.scala.quantities.{ApparentPower, Kilovoltamperes} +import squants.energy.KilowattHours +import squants.{Energy, Power} import java.time.ZonedDateTime -import java.util.UUID -class LoadModel private ( - override val uuid: UUID, - override val id: String, - override val sRated: ApparentPower, - override val cosPhiRated: Double, - override val qControl: QControl, -) extends ParticipantModel[ +abstract class LoadModel[S <: ModelState] + extends ParticipantModel[ ActivePowerOperatingPoint, - FixedState, + S, ] - with ParticipantSimpleFlexibility[FixedState] { + with ParticipantSimpleFlexibility[S] { override def zeroPowerOperatingPoint: ActivePowerOperatingPoint = ActivePowerOperatingPoint.zero override def createResults( - state: FixedState, + state: S, lastOperatingPoint: Option[ActivePowerOperatingPoint], currentOperatingPoint: ActivePowerOperatingPoint, complexPower: ComplexPower, dateTime: ZonedDateTime, ): Iterable[SystemParticipantResult] = - throw new NotImplementedError("Dummy implementation") + Iterable( + new LoadResult( + dateTime, + uuid, + complexPower.p.toMegawatts.asMegaWatt, + complexPower.q.toMegavars.asMegaVar, + ) + ) override def createPrimaryDataResult( data: PrimaryDataWithComplexPower[_], dateTime: ZonedDateTime, ): SystemParticipantResult = - throw new NotImplementedError("Dummy implementation") + new LoadResult( + dateTime, + uuid, + data.p.toMegawatts.asMegaWatt, + data.q.toMegavars.asMegaVar, + ) override def getRequiredSecondaryServices: Iterable[ServiceType] = Iterable.empty - override val initialState: ParticipantModel.ModelInput => FixedState = - _ => FixedState(-1) - - override def determineState( - lastState: FixedState, - operatingPoint: ActivePowerOperatingPoint, - input: ParticipantModel.ModelInput, - ): FixedState = throw new NotImplementedError("Dummy implementation") - - override def determineOperatingPoint( - state: FixedState - ): (ActivePowerOperatingPoint, Option[Long]) = throw new NotImplementedError( - "Dummy implementation" - ) } object LoadModel { - def apply( - input: LoadInput - ): LoadModel = - new LoadModel( - input.getUuid, - input.getId, - Kilovoltamperes( - input.getsRated - .to(PowerSystemUnits.KILOVOLTAMPERE) - .getValue - .doubleValue - ), - input.getCosPhiRated, - QControl(input.getqCharacteristics), + /** Calculates the scaling factor and scaled rated apparent power according to + * the reference type + * + * @param referenceType + * The type of reference according to which scaling is calculated + * @param input + * The [[LoadInput]] of the model + * @param maxPower + * The maximum power consumption possible for the model + * @param referenceEnergy + * The (annual) reference energy relevant to the load model + * @return + * the reference scaling factor used for calculation of specific power + * consumption values and the scaled rated apparent power + */ + def scaleToReference( + referenceType: LoadReferenceType.Value, + input: LoadInput, + maxPower: Power, + referenceEnergy: Energy, + ): (Double, ApparentPower) = { + val sRated = Kilovoltamperes( + input.getsRated + .to(KILOVOLTAMPERE) + .getValue + .doubleValue ) + val eConsAnnual = KilowattHours( + input.geteConsAnnual().to(KILOWATTHOUR).getValue.doubleValue + ) + + val referenceScalingFactor = referenceType match { + case LoadReferenceType.ACTIVE_POWER => + val pRated = sRated.toActivePower(input.getCosPhiRated) + pRated / maxPower + case LoadReferenceType.ENERGY_CONSUMPTION => + eConsAnnual / referenceEnergy + } + + val scaledSRated = referenceType match { + case LoadReferenceType.ACTIVE_POWER => + sRated + case LoadReferenceType.ENERGY_CONSUMPTION => + val maxApparentPower = Kilovoltamperes( + maxPower.toKilowatts / input.getCosPhiRated + ) + maxApparentPower * referenceScalingFactor + } + + (referenceScalingFactor, scaledSRated) + } + + def apply( + input: LoadInput, + config: LoadRuntimeConfig, + ): LoadModel[_ <: ModelState] = { + LoadModelBehaviour(config.modelBehaviour) match { + case LoadModelBehaviour.FIX => + FixedLoadModel(input, config) + case LoadModelBehaviour.PROFILE => + ProfileLoadModel(input, config) + case LoadModelBehaviour.RANDOM => + RandomLoadModel(input, config) + } + } } diff --git a/src/main/scala/edu/ie3/simona/model/participant/load/LoadModelBehaviour.scala b/src/main/scala/edu/ie3/simona/model/participant2/load/LoadModelBehaviour.scala similarity index 79% rename from src/main/scala/edu/ie3/simona/model/participant/load/LoadModelBehaviour.scala rename to src/main/scala/edu/ie3/simona/model/participant2/load/LoadModelBehaviour.scala index 0a9a057808..baaee28e3f 100644 --- a/src/main/scala/edu/ie3/simona/model/participant/load/LoadModelBehaviour.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/load/LoadModelBehaviour.scala @@ -4,13 +4,13 @@ * Research group Distribution grid planning and operation */ -package edu.ie3.simona.model.participant.load +package edu.ie3.simona.model.participant2.load import edu.ie3.simona.util.ParsableEnumeration /** Enumeration to describe all eligible load model behaviours */ -case object LoadModelBehaviour extends ParsableEnumeration { +object LoadModelBehaviour extends ParsableEnumeration { val FIX: Value = Value("fix") val PROFILE: Value = Value("profile") val RANDOM: Value = Value("random") diff --git a/src/main/scala/edu/ie3/simona/model/participant2/load/LoadReferenceType.scala b/src/main/scala/edu/ie3/simona/model/participant2/load/LoadReferenceType.scala new file mode 100644 index 0000000000..d241972253 --- /dev/null +++ b/src/main/scala/edu/ie3/simona/model/participant2/load/LoadReferenceType.scala @@ -0,0 +1,25 @@ +/* + * © 2020. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.model.participant2.load + +import edu.ie3.simona.util.ParsableEnumeration + +/** Denoting difference referencing scenarios for scaling load model output + */ +object LoadReferenceType extends ParsableEnumeration { + + /** Scale the load model behaviour so that the rated power of the load model + * serves as the maximum power consumption + */ + val ACTIVE_POWER: Value = Value("power") + + /** Scale the load model behaviour so that the aggregate annual energy + * consumption corresponds to the energy set by the model input + */ + val ENERGY_CONSUMPTION: Value = Value("energy") + +} diff --git a/src/main/scala/edu/ie3/simona/model/participant/load/profile/LoadProfileKey.scala b/src/main/scala/edu/ie3/simona/model/participant2/load/profile/LoadProfileKey.scala similarity index 90% rename from src/main/scala/edu/ie3/simona/model/participant/load/profile/LoadProfileKey.scala rename to src/main/scala/edu/ie3/simona/model/participant2/load/profile/LoadProfileKey.scala index af77c3fa55..ce571e9049 100644 --- a/src/main/scala/edu/ie3/simona/model/participant/load/profile/LoadProfileKey.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/load/profile/LoadProfileKey.scala @@ -4,13 +4,13 @@ * Research group Distribution grid planning and operation */ -package edu.ie3.simona.model.participant.load.profile +package edu.ie3.simona.model.participant2.load.profile -import java.time.ZonedDateTime import edu.ie3.datamodel.exceptions.ParsingException import edu.ie3.datamodel.models.profile.StandardLoadProfile -import edu.ie3.simona.model.participant.load -import edu.ie3.simona.model.participant.load.{DayType, profile} +import edu.ie3.simona.model.participant2.load.DayType + +import java.time.ZonedDateTime /** A key describing a load profile, consisting of consumer type, a season and a * day type. Is used to store load profile values for a single type. @@ -28,7 +28,7 @@ final case class LoadProfileKey( dayType: DayType.Value, ) -case object LoadProfileKey { +object LoadProfileKey { /** Creates a load profile key from given csv header, i.e. "g0SSo" * @@ -97,8 +97,8 @@ case object LoadProfileKey { ): LoadProfileKey = { new LoadProfileKey( loadProfile, - profile.Season(time), - load.DayType(time.getDayOfWeek), + Season(time), + DayType(time.getDayOfWeek), ) } } diff --git a/src/main/scala/edu/ie3/simona/model/participant/load/profile/LoadProfileStore.scala b/src/main/scala/edu/ie3/simona/model/participant2/load/profile/LoadProfileStore.scala similarity index 81% rename from src/main/scala/edu/ie3/simona/model/participant/load/profile/LoadProfileStore.scala rename to src/main/scala/edu/ie3/simona/model/participant2/load/profile/LoadProfileStore.scala index 209d9349c8..a53fb53bb5 100644 --- a/src/main/scala/edu/ie3/simona/model/participant/load/profile/LoadProfileStore.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/load/profile/LoadProfileStore.scala @@ -4,7 +4,7 @@ * Research group Distribution grid planning and operation */ -package edu.ie3.simona.model.participant.load.profile +package edu.ie3.simona.model.participant2.load.profile import breeze.numerics.round import com.typesafe.scalalogging.LazyLogging @@ -12,14 +12,13 @@ import edu.ie3.datamodel.models.profile.{ BdewStandardLoadProfile, StandardLoadProfile, } -import edu.ie3.simona.model.participant.load.profile.LoadProfileStore.{ +import edu.ie3.simona.model.participant2.load.DayType +import edu.ie3.simona.model.participant2.load.profile.LoadProfileStore.{ initializeMaxConsumptionPerProfile, initializeTypeDayValues, } -import edu.ie3.simona.model.participant.load.{DayType, profile} import org.apache.commons.csv.CSVFormat -import squants.Power -import squants.energy.{KilowattHours, Watts} +import squants.energy.{Energy, KilowattHours, Power, Watts} import java.io.{InputStreamReader, Reader} import java.time.{Duration, ZonedDateTime} @@ -58,7 +57,7 @@ class LoadProfileStore private (val reader: Reader) { def entry( time: ZonedDateTime, loadProfile: StandardLoadProfile, - ): squants.Power = { + ): Power = { val key = LoadProfileKey(loadProfile, time) profileMap.get(key) match { case Some(typeDayValues) => @@ -112,7 +111,7 @@ object LoadProfileStore extends LazyLogging { /** Default standard load profile energy scaling */ - val defaultLoadProfileEnergyScaling: squants.Energy = KilowattHours(1000d) + val profileReferenceEnergy: Energy = KilowattHours(1000d) /** Default entry point to get the default implementation with the provided * default standard load profiles @@ -200,35 +199,31 @@ object LoadProfileStore extends LazyLogging { val knownLoadProfiles: Set[StandardLoadProfile] = profileMap.keySet.map(key => key.standardLoadProfile) - knownLoadProfiles - .flatMap(loadProfile => { - (loadProfile match { - case BdewStandardLoadProfile.H0 => - // max load for h0 is expected to be exclusively found in winter, - // thus we only search there. - DayType.values - .map(dayType => { - val key = - profile.LoadProfileKey(loadProfile, Season.winter, dayType) - // maximum dynamization factor is on day 366 (leap year) or day 365 (regular year). - // The difference between day 365 and day 366 is negligible, thus pick 366 - profileMap - .get(key) - .map(typeDay => dynamization(typeDay.getMaxValue, 366)) - .getOrElse(0d) - }) - .maxOption - case _ => - (for (season <- Season.values; dayType <- DayType.values) yield { - val key = profile.LoadProfileKey(loadProfile, season, dayType) - profileMap.get(key) match { - case Some(value) => Option(value.getMaxValue) - case None => None - } - }).flatten.maxOption - }).map(maxConsumption => loadProfile -> maxConsumption) - }) - .toMap + knownLoadProfiles.flatMap { loadProfile => + val dyn = loadProfile match { + case BdewStandardLoadProfile.H0 => + // Pick the maximum dynamization factor per season, including leap years and non-leap years + (season: Season.Value, value: Double) => { + val day = season match { + case Season.winter => 366 // leap year + case Season.transition => 80 // non-leap year + case Season.summer => 135 // non-leap year + } + dynamization(value, day) + } + + case _ => + (_: Season.Value, value: Double) => value + } + + (for (season <- Season.values; dayType <- DayType.values) yield { + val key = + LoadProfileKey(loadProfile, season, dayType) + profileMap + .get(key) + .map(typeDay => dyn(season, typeDay.getMaxValue)) + }).flatten.maxOption.map(maxConsumption => loadProfile -> maxConsumption) + }.toMap } /** @return diff --git a/src/main/scala/edu/ie3/simona/model/participant2/load/profile/ProfileLoadModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/load/profile/ProfileLoadModel.scala new file mode 100644 index 0000000000..956b597e19 --- /dev/null +++ b/src/main/scala/edu/ie3/simona/model/participant2/load/profile/ProfileLoadModel.scala @@ -0,0 +1,97 @@ +/* + * © 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.participant2.load.profile + +import edu.ie3.datamodel.models.input.system.LoadInput +import edu.ie3.datamodel.models.profile.StandardLoadProfile +import edu.ie3.simona.config.RuntimeConfig.LoadRuntimeConfig +import edu.ie3.simona.exceptions.CriticalFailureException +import edu.ie3.simona.model.participant.control.QControl +import edu.ie3.simona.model.participant2.ParticipantModel +import edu.ie3.simona.model.participant2.ParticipantModel.{ + ActivePowerOperatingPoint, + DateTimeState, + ParticipantDateTimeState, +} +import edu.ie3.simona.model.participant2.load.{LoadModel, LoadReferenceType} +import edu.ie3.simona.util.TickUtil +import edu.ie3.util.scala.quantities.ApparentPower + +import java.util.UUID + +class ProfileLoadModel( + override val uuid: UUID, + override val id: String, + override val sRated: ApparentPower, + override val cosPhiRated: Double, + override val qControl: QControl, + private val loadProfileStore: LoadProfileStore, + private val loadProfile: StandardLoadProfile, + val referenceScalingFactor: Double, +) extends LoadModel[DateTimeState] + with ParticipantDateTimeState[ActivePowerOperatingPoint] { + + override def determineOperatingPoint( + state: DateTimeState + ): (ParticipantModel.ActivePowerOperatingPoint, Option[Long]) = { + val resolution = LoadProfileStore.resolution.getSeconds + + val (modelTick, modelDateTime) = TickUtil.roundToResolution( + state.tick, + state.dateTime, + resolution.toInt, + ) + + val averagePower = loadProfileStore.entry(modelDateTime, loadProfile) + val nextTick = modelTick + resolution + + ( + ActivePowerOperatingPoint(averagePower * referenceScalingFactor), + Some(nextTick), + ) + } + +} + +object ProfileLoadModel { + + def apply(input: LoadInput, config: LoadRuntimeConfig): ProfileLoadModel = { + + val loadProfileStore = LoadProfileStore() + + val loadProfile = input.getLoadProfile match { + case slp: StandardLoadProfile => + slp + case other => + throw new CriticalFailureException( + s"Expected a standard load profile type, got ${other.getClass}" + ) + } + + val referenceType = LoadReferenceType(config.reference) + + val (referenceScalingFactor, scaledSRated) = + LoadModel.scaleToReference( + referenceType, + input, + loadProfileStore.maxPower(loadProfile), + LoadProfileStore.profileReferenceEnergy, + ) + + new ProfileLoadModel( + input.getUuid, + input.getId, + scaledSRated, + input.getCosPhiRated, + QControl.apply(input.getqCharacteristics()), + loadProfileStore, + loadProfile, + referenceScalingFactor, + ) + } + +} diff --git a/src/main/scala/edu/ie3/simona/model/participant/load/profile/Season.scala b/src/main/scala/edu/ie3/simona/model/participant2/load/profile/Season.scala similarity index 96% rename from src/main/scala/edu/ie3/simona/model/participant/load/profile/Season.scala rename to src/main/scala/edu/ie3/simona/model/participant2/load/profile/Season.scala index 0d9bb6541c..dd44f7fb06 100644 --- a/src/main/scala/edu/ie3/simona/model/participant/load/profile/Season.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/load/profile/Season.scala @@ -4,7 +4,7 @@ * Research group Distribution grid planning and operation */ -package edu.ie3.simona.model.participant.load.profile +package edu.ie3.simona.model.participant2.load.profile import java.time.Month._ import java.time.ZonedDateTime diff --git a/src/main/scala/edu/ie3/simona/model/participant/load/profile/TypeDayProfile.scala b/src/main/scala/edu/ie3/simona/model/participant2/load/profile/TypeDayProfile.scala similarity index 95% rename from src/main/scala/edu/ie3/simona/model/participant/load/profile/TypeDayProfile.scala rename to src/main/scala/edu/ie3/simona/model/participant2/load/profile/TypeDayProfile.scala index 22358772b1..f149ef5b6e 100644 --- a/src/main/scala/edu/ie3/simona/model/participant/load/profile/TypeDayProfile.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/load/profile/TypeDayProfile.scala @@ -4,12 +4,12 @@ * Research group Distribution grid planning and operation */ -package edu.ie3.simona.model.participant.load.profile - -import java.time.ZonedDateTime +package edu.ie3.simona.model.participant2.load.profile import edu.ie3.util.TimeUtil +import java.time.ZonedDateTime + // needs to be imported for max function import scala.math.Ordering.Double.IeeeOrdering diff --git a/src/main/scala/edu/ie3/simona/model/participant2/load/random/RandomLoadModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/load/random/RandomLoadModel.scala new file mode 100644 index 0000000000..a3f40f084c --- /dev/null +++ b/src/main/scala/edu/ie3/simona/model/participant2/load/random/RandomLoadModel.scala @@ -0,0 +1,163 @@ +/* + * © 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.participant2.load.random + +import de.lmu.ifi.dbs.elki.math.statistics.distribution.GeneralizedExtremeValueDistribution +import de.lmu.ifi.dbs.elki.utilities.random.RandomFactory +import edu.ie3.datamodel.models.input.system.LoadInput +import edu.ie3.simona.config.RuntimeConfig.LoadRuntimeConfig +import edu.ie3.simona.model.participant.control.QControl +import edu.ie3.simona.model.participant2.ParticipantModel +import edu.ie3.simona.model.participant2.ParticipantModel.{ + ActivePowerOperatingPoint, + DateTimeState, + ParticipantDateTimeState, +} +import edu.ie3.simona.model.participant2.load.{ + DayType, + LoadModel, + LoadReferenceType, +} +import edu.ie3.simona.util.TickUtil +import edu.ie3.util.TimeUtil +import edu.ie3.util.scala.quantities.ApparentPower +import squants.Power +import squants.energy.{KilowattHours, Kilowatts, Watts} + +import java.time.ZonedDateTime +import java.util.UUID +import scala.collection.mutable +import scala.util.Random + +class RandomLoadModel( + override val uuid: UUID, + override val id: String, + override val sRated: ApparentPower, + override val cosPhiRated: Double, + override val qControl: QControl, + val referenceScalingFactor: Double, +) extends LoadModel[DateTimeState] + with ParticipantDateTimeState[ActivePowerOperatingPoint] { + + private val randomLoadParamStore = RandomLoadParamStore() + + private type GevKey = (DayType.Value, Int) + private val gevStorage = + mutable.Map.empty[GevKey, GeneralizedExtremeValueDistribution] + + override def determineOperatingPoint( + state: DateTimeState + ): (ParticipantModel.ActivePowerOperatingPoint, Option[Long]) = { + val resolution = RandomLoadParamStore.resolution.getSeconds + + val (modelTick, modelDateTime) = TickUtil.roundToResolution( + state.tick, + state.dateTime, + resolution.toInt, + ) + + val gev = getGevDistribution(modelDateTime) + + /* Get a next random power (in kW) */ + val randomPower = gev.nextRandom() + if (randomPower < 0) + determineOperatingPoint(state) + else { + val nextTick = modelTick + resolution + ( + ActivePowerOperatingPoint( + Kilowatts(randomPower) * referenceScalingFactor + ), + Some(nextTick), + ) + } + } + + /** Get the needed generalized extreme value distribution from the store or + * instantiate a new one and put it to the store. + * + * @param dateTime + * Questioned date time + * @return + * The needed generalized extreme value distribution + */ + private def getGevDistribution( + dateTime: ZonedDateTime + ): GeneralizedExtremeValueDistribution = { + /* Determine identifying key for a distinct generalized extreme value distribution and look it up. If it is not + * available, yet, instantiate one. */ + val key: GevKey = ( + DayType(dateTime.getDayOfWeek), + TimeUtil.withDefaults.getQuarterHourOfDay(dateTime), + ) + gevStorage.get(key) match { + case Some(foundIt) => foundIt + case None => + /* Instantiate new gev distribution, put it to storage and return it */ + val randomFactory = RandomFactory.get(Random.nextLong()) + val gevParameters = randomLoadParamStore.parameters(dateTime) + val newGev = new GeneralizedExtremeValueDistribution( + gevParameters.my, + gevParameters.sigma, + gevParameters.k, + randomFactory, + ) + gevStorage += (key -> newGev) + newGev + } + } + +} + +object RandomLoadModel { + + /** The annual energy consumption that the random profile is scaled to. + * + * It is said in 'Kays - Agent-based simulation environment for improving the + * planning of distribution grids', that the Generalized Extreme Value + * distribution's parameters are sampled from input data, that is normalized + * to 1,000 kWh annual energy consumption. However, due to inaccuracies in + * random data reproduction, the sampled values will lead to an average + * annual energy consumption of approx. this value. It has been found by + * 1,000 evaluations of the year 2019. + */ + private val profileReferenceEnergy = KilowattHours(716.5416966513656) + + /** This is the 95 % quantile resulting from 10,000 evaluations of the year + * 2019. It is only needed, when the load is meant to be scaled to rated + * active power. + */ + private val maxPower: Power = Watts(159d) + + def apply(input: LoadInput, config: LoadRuntimeConfig): RandomLoadModel = { + + val referenceType = LoadReferenceType(config.reference) + + val (referenceScalingFactor, scaledSRated) = + LoadModel.scaleToReference( + referenceType, + input, + maxPower, + profileReferenceEnergy, + ) + + /** Safety factor to address potential higher sRated values when using + * unrestricted probability functions + */ + val safetyFactor = 1.1 + + new RandomLoadModel( + input.getUuid, + input.getId, + scaledSRated * safetyFactor, + input.getCosPhiRated, + QControl.apply(input.getqCharacteristics()), + referenceScalingFactor, + ) + } + +} diff --git a/src/main/scala/edu/ie3/simona/model/participant/load/random/RandomLoadParamStore.scala b/src/main/scala/edu/ie3/simona/model/participant2/load/random/RandomLoadParamStore.scala similarity index 96% rename from src/main/scala/edu/ie3/simona/model/participant/load/random/RandomLoadParamStore.scala rename to src/main/scala/edu/ie3/simona/model/participant2/load/random/RandomLoadParamStore.scala index 33fb57a38a..0f84836013 100644 --- a/src/main/scala/edu/ie3/simona/model/participant/load/random/RandomLoadParamStore.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/load/random/RandomLoadParamStore.scala @@ -4,16 +4,16 @@ * Research group Distribution grid planning and operation */ -package edu.ie3.simona.model.participant.load.random +package edu.ie3.simona.model.participant2.load.random -import java.io.{InputStreamReader, Reader} -import java.time.{Duration, ZonedDateTime} import com.typesafe.scalalogging.LazyLogging import edu.ie3.simona.exceptions.FileIOException -import edu.ie3.simona.model.participant.load.DayType -import edu.ie3.simona.model.participant.load.random.RandomLoadParamStore.initializeDayTypeValues +import edu.ie3.simona.model.participant2.load.DayType +import edu.ie3.simona.model.participant2.load.random.RandomLoadParamStore.initializeDayTypeValues import org.apache.commons.csv.{CSVFormat, CSVRecord} +import java.io.{InputStreamReader, Reader} +import java.time.{Duration, ZonedDateTime} import scala.jdk.CollectionConverters._ /** Storage for a collection of random load parameters. @@ -42,7 +42,7 @@ final case class RandomLoadParamStore private (reader: Reader) { } } -case object RandomLoadParamStore extends LazyLogging { +object RandomLoadParamStore extends LazyLogging { val resolution: Duration = Duration.ofMinutes(15) /** Default value store, that uses information from a file diff --git a/src/main/scala/edu/ie3/simona/model/participant/load/random/RandomLoadParameters.scala b/src/main/scala/edu/ie3/simona/model/participant2/load/random/RandomLoadParameters.scala similarity index 94% rename from src/main/scala/edu/ie3/simona/model/participant/load/random/RandomLoadParameters.scala rename to src/main/scala/edu/ie3/simona/model/participant2/load/random/RandomLoadParameters.scala index 29399a499b..e244d56162 100644 --- a/src/main/scala/edu/ie3/simona/model/participant/load/random/RandomLoadParameters.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/load/random/RandomLoadParameters.scala @@ -4,7 +4,7 @@ * Research group Distribution grid planning and operation */ -package edu.ie3.simona.model.participant.load.random +package edu.ie3.simona.model.participant2.load.random import edu.ie3.simona.util.ParsableEnumeration diff --git a/src/main/scala/edu/ie3/simona/model/participant/load/random/TypeDayParameters.scala b/src/main/scala/edu/ie3/simona/model/participant2/load/random/TypeDayParameters.scala similarity index 91% rename from src/main/scala/edu/ie3/simona/model/participant/load/random/TypeDayParameters.scala rename to src/main/scala/edu/ie3/simona/model/participant2/load/random/TypeDayParameters.scala index 57210c68b2..f67df16e3b 100644 --- a/src/main/scala/edu/ie3/simona/model/participant/load/random/TypeDayParameters.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/load/random/TypeDayParameters.scala @@ -4,13 +4,13 @@ * Research group Distribution grid planning and operation */ -package edu.ie3.simona.model.participant.load.random +package edu.ie3.simona.model.participant2.load.random -import java.time.ZonedDateTime - -import edu.ie3.simona.model.participant.load.DayType +import edu.ie3.simona.model.participant2.load.DayType import edu.ie3.util.TimeUtil +import java.time.ZonedDateTime + /** Stores a slice of random load parameters, that comprises a whole day (96 * quarter-hours). The data describes a typical day, that can unequivocally be * identified by its [[DayType]]. diff --git a/src/main/scala/edu/ie3/simona/model/thermal/ThermalGrid.scala b/src/main/scala/edu/ie3/simona/model/thermal/ThermalGrid.scala index d3ca12a59d..7f9c30c9c6 100644 --- a/src/main/scala/edu/ie3/simona/model/thermal/ThermalGrid.scala +++ b/src/main/scala/edu/ie3/simona/model/thermal/ThermalGrid.scala @@ -34,12 +34,12 @@ import scala.jdk.CollectionConverters.SetHasAsScala import scala.language.postfixOps /** Calculation model for a thermal grid. It is assumed, that all elements are - * connected directly with exactly one thermal bus + * connected directly with exactly one thermal bus. * * @param house - * Thermal houses connected to the bus + * Thermal houses connected to the bus. * @param storage - * Thermal storages + * Thermal storages connected to the bus. */ final case class ThermalGrid( house: Option[ThermalHouse], @@ -47,15 +47,15 @@ final case class ThermalGrid( ) extends LazyLogging { /** Determine the energy demand of the total grid at the given instance in - * time and returns it including the updatedState + * time and returns it including the updatedState. * * @param lastHpState - * Last state of the heat pump + * Last state of the heat pump. * @param relevantData - * data of heat pump including + * Required data for calculation. * @return * The total energy demand of the house and the storage and an updated - * [[ThermalGridState]] + * [[ThermalGridState]]. */ def energyDemandAndUpdatedState( relevantData: HpRelevantData, @@ -145,23 +145,24 @@ final case class ThermalGrid( ) } - /** Update the current state of the grid + /** Update the current state of the grid. Whether there are two cases to + * handle, external infeed into the thermal grid and no infeed. * * @param relevantData - * data of heat pump including state of the heat pump + * Data of heat pump including state of the heat pump. * @param lastThermalGridState - * state of the thermalGrid until this tick + * State of the thermalGrid until this tick. * @param lastAmbientTemperature - * Ambient temperature valid up until (not including) the current tick + * Ambient temperature valid up until (not including) the current tick. * @param isRunning - * determines whether the heat pump is running or not + * Determines whether the heat pump is running or not. * @param qDot * Infeed to the grid from thermal generation (e.g. heat pump) or thermal - * storages + * storages. * @param thermalDemands - * holds the thermal demands of the thermal units (house, storage) + * Holds the thermal demands of the thermal units (house, storage). * @return - * The updated state of the grid + * The updated state of the grid. */ def updateState( relevantData: HpRelevantData, @@ -192,23 +193,23 @@ final case class ThermalGrid( * First the actions from lastState will be considered and checked if the * behaviour should be continued. This might be the case, if we got activated * by updated weather data. If this is not the case, all other cases will be - * handled by [[ThermalGrid.handleFinalInfeedCases]] + * handled by [[ThermalGrid.handleFinalInfeedCases]]. * * @param relevantData - * data of heat pump including state of the heat pump + * data of heat pump including state of the heat pump. * @param lastAmbientTemperature - * Ambient temperature valid up until (not including) the current tick + * Ambient temperature valid up until (not including) the current tick. * @param lastThermalGridState - * state of the thermalGrid until this tick + * state of the thermalGrid until this tick. * @param isRunning - * determines whether the heat pump is running or not + * determines whether the heat pump is running or not. * @param qDot * Infeed to the grid from thermal generation (e.g. heat pump) or thermal - * storages + * storages. * @param thermalDemands - * holds the thermal demands of the thermal units (house, storage) + * holds the thermal demands of the thermal units (house, storage). * @return - * Updated thermal grid state and the thermalThreshold if there is one + * Updated thermal grid state and the thermalThreshold if there is one. */ private def handleInfeed( relevantData: HpRelevantData, @@ -347,18 +348,18 @@ final case class ThermalGrid( * | 4 | else | no output | * * @param thermalDemands - * holds the thermal demands of the thermal units (house, storage) + * holds the thermal demands of the thermal units (house, storage). * @param relevantData - * data of heat pump including state of the heat pump + * data of heat pump including state of the heat pump. * @param lastAmbientTemperature - * Ambient temperature valid up until (not including) the current tick + * Ambient temperature valid up until (not including) the current tick. * @param gridState - * Current state of the thermalGrid + * Current state of the thermalGrid. * @param qDot * Infeed to the grid from thermal generation (e.g. heat pump) or thermal - * storages + * storages. * @return - * Updated thermal grid state and the thermalThreshold if there is one + * Updated thermal grid state and the thermalThreshold if there is one. */ private def handleFinalInfeedCases( thermalDemands: ThermalDemandWrapper, @@ -409,18 +410,18 @@ final case class ThermalGrid( * grid. * * @param relevantData - * data of heat pump including state of the heat pump + * data of heat pump including state of the heat pump. * @param lastAmbientTemperature - * Ambient temperature until this tick + * Ambient temperature until this tick. * @param state - * Current state of the thermal grid + * Current state of the thermal grid. * @param qDotHouse - * Infeed to the house + * Infeed to the house. * @param qDotHeatStorage * Infeed to the heat storage (positive: Storage is charging, negative: - * Storage is discharging) + * Storage is discharging). * @return - * Updated thermal grid state and the next threshold if there is one + * Updated thermal grid state and the next threshold if there is one. */ private def handleCases( relevantData: HpRelevantData, @@ -458,15 +459,15 @@ final case class ThermalGrid( * here. * * @param relevantData - * data of heat pump including state of the heat pump + * Data of heat pump including state of the heat pump. * @param lastAmbientTemperature - * Ambient temperature until this tick + * Ambient temperature until this tick. * @param state - * Current state of the houses + * Current state of the houses. * @param qDotHouse - * Infeed into the house + * Infeed into the house. * @return - * Updated thermal house state, a ThermalThreshold and the remaining qDot + * Updated thermal house state, a ThermalThreshold and the remaining qDot. */ private def handleInfeedHouse( relevantData: HpRelevantData, @@ -507,14 +508,14 @@ final case class ThermalGrid( * here (positive qDot) or will return its stored energy into the thermal * grid (negative qDot). * @param tick - * Current tick + * Current tick. * @param state - * Current state of the houses + * Current state of the houses. * @param qDotStorage * Infeed to the storage (positive: Storage is charging, negative: Storage - * is discharging) + * is discharging). * @return - * Updated thermal grid state + * Updated thermal grid state. */ private def handleStorageCases( tick: Long, @@ -533,14 +534,14 @@ final case class ThermalGrid( } } - /** Determines the most recent threshold of two given input thresholds + /** Determines the most recent threshold of two given input thresholds. * * @param maybeHouseThreshold - * Option of a possible next threshold of the thermal house + * Option of a possible next threshold of the thermal house. * @param maybeStorageThreshold - * Option of a possible next threshold of the thermal storage + * Option of a possible next threshold of the thermal storage. * @return - * The next threshold + * The next threshold. */ private def determineMostRecentThreshold( maybeHouseThreshold: Option[ThermalThreshold], @@ -557,19 +558,19 @@ final case class ThermalGrid( case _ => None } - /** Handle consumption (or no infeed) from thermal grid + /** Handle consumption (or no infeed) from thermal grid. * * @param relevantData - * data of heat pump including state of the heat pump + * data of heat pump including state of the heat pump. * @param lastAmbientTemperature - * Ambient temperature valid up until (not including) the current tick + * Ambient temperature valid up until (not including) the current tick. * @param lastThermalGridState - * state of the thermalGrid until this tick + * state of the thermalGrid until this tick. * @param qDot * Infeed to the grid from thermal generation (e.g. heat pump) or thermal - * storages + * storages. * @return - * Updated thermal grid state + * Updated thermal grid state. */ private def handleConsumption( relevantData: HpRelevantData, @@ -627,22 +628,22 @@ final case class ThermalGrid( * itself * * @param relevantData - * data of heat pump including state of the heat pump + * data of heat pump including state of the heat pump. * @param maybeHouseState - * Optional thermal house state + * Optional thermal house state. * @param maybeStorageState - * Optional thermal storage state + * Optional thermal storage state. * @param formerHouseState - * Previous thermal house state before a first update was performed + * Previous thermal house state before a first update was performed. * @param formerStorageState - * Previous thermal storage state before a first update was performed + * Previous thermal storage state before a first update was performed. * @param lastAmbientTemperature - * Ambient temperature valid up until (not including) the current tick + * Ambient temperature valid up until (not including) the current tick. * @param qDot * Infeed to the grid from thermal generation (e.g. heat pump) or thermal - * storages + * storages. * @return - * Options to revised thermal house and storage state + * Options to revised thermal house and storage state. */ def reviseInfeedFromStorage( relevantData: HpRelevantData, @@ -693,16 +694,16 @@ final case class ThermalGrid( } /** Convert the given state of the thermal grid into result models of its - * constituent models + * constituent models. * * @param currentTick - * Actual simulation tick + * Actual simulation tick. * @param state - * State to be converted + * State to be converted. * @param startDateTime - * Start date time of the simulation + * Start date time of the simulation. * @return - * A [[Seq]] of results of the constituent thermal model + * A [[Seq]] of results of the constituent thermal model. */ def results(currentTick: Long, state: ThermalGridState)(implicit startDateTime: ZonedDateTime @@ -769,11 +770,11 @@ object ThermalGrid { ) } - /** Current state of a grid + /** Current state of a grid. * @param houseState - * State of the thermal house + * State of the thermal house. * @param storageState - * State of the thermal storage + * State of the thermal storage. */ final case class ThermalGridState( houseState: Option[ThermalHouseState], @@ -805,9 +806,9 @@ object ThermalGrid { /** Wraps the demand of thermal units (thermal house, thermal storage). * * @param houseDemand - * the demand of the thermal house + * The demand of the thermal house. * @param heatStorageDemand - * the demand of the thermal heat storage + * The demand of the thermal heat storage. */ final case class ThermalDemandWrapper private ( houseDemand: ThermalEnergyDemand, @@ -823,10 +824,10 @@ object ThermalGrid { * The absolutely required energy to reach target state. For * [[ThermalHouse]] this would be the energy demand to reach the boundary * or targetTemperature. For [[ThermalStorage]] this would be the amount of - * energy to get fully charged when empty. If the [[ThermalStorage]] is not + * Energy to get fully charged when empty. If the [[ThermalStorage]] is not * empty, the required energy is zero. * @param possible - * The maximum possible energy, that can be handled + * The maximum possible energy, that can be handled. */ final case class ThermalEnergyDemand private ( required: Energy, @@ -846,11 +847,11 @@ object ThermalGrid { /** Builds a new instance of [[ThermalEnergyDemand]]. If the possible energy * is less than the required energy, this is considered to be a bad state. * @param required - * The absolutely required energy to reach target state + * The absolutely required energy to reach target state. * @param possible - * The maximum possible energy, that can be handled + * The maximum possible energy, that can be handled. * @return - * Thermal energy demand container class, that meets all specifications + * Thermal energy demand container class, that meets all specifications. */ def apply( required: Energy, diff --git a/src/main/scala/edu/ie3/simona/model/thermal/ThermalHouse.scala b/src/main/scala/edu/ie3/simona/model/thermal/ThermalHouse.scala index b6aa4a949c..e01c2bb8e8 100644 --- a/src/main/scala/edu/ie3/simona/model/thermal/ThermalHouse.scala +++ b/src/main/scala/edu/ie3/simona/model/thermal/ThermalHouse.scala @@ -84,11 +84,11 @@ final case class ThermalHouse( * place. * * @param relevantData - * data of heat pump including state of the heat pump + * Data of heat pump including state of the heat pump. * @param state - * most recent state, that is valid for this model + * Most recent state, that is valid for this model. * @return - * the needed energy in the questioned tick + * The needed energy in the questioned tick. */ def energyDemand( relevantData: HpRelevantData, @@ -141,11 +141,11 @@ final case class ThermalHouse( * temperature difference to zero, resulting in an energy demand of 0 kWh. * * @param targetTemperature - * The target temperature to reach + * The target temperature to reach. * @param startTemperature - * The starting temperature + * The starting temperature. * @return - * The needed energy to change + * The needed energy to change. */ private def energy( targetTemperature: Temperature, @@ -159,9 +159,13 @@ final case class ThermalHouse( } /** Check if inner temperature is higher than preferred maximum temperature + * @param innerTemperature + * The inner temperature of the house + * @param boundaryTemperature + * The applied boundary temperature to check against * * @return - * true, if inner temperature is too high + * True, if inner temperature is too high. */ def isInnerTemperatureTooHigh( innerTemperature: Temperature, @@ -221,10 +225,10 @@ final case class ThermalHouse( currentInnerTemperature + temperatureChange } - /** Update the current state of the house + /** Update the current state of the house. * * @param relevantData - * data of heat pump including state of the heat pump + * Data of heat pump including state of the heat pump. * @param state * Currently applicable state * @param lastAmbientTemperature @@ -267,9 +271,9 @@ final case class ThermalHouse( ) } - /** Determine the next threshold, that will be reached + /** Determine the next threshold, that will be reached. * @param tick - * The current tick + * The current tick. * @param qDotExternal * The external influx * @param innerTemperature diff --git a/src/main/scala/edu/ie3/simona/service/weather/WeatherService.scala b/src/main/scala/edu/ie3/simona/service/weather/WeatherService.scala index d1838ab56d..f323238f4a 100644 --- a/src/main/scala/edu/ie3/simona/service/weather/WeatherService.scala +++ b/src/main/scala/edu/ie3/simona/service/weather/WeatherService.scala @@ -90,7 +90,6 @@ object WeatherService { sourceDefinition: SimonaConfig.Simona.Input.Weather.Datasource ) extends InitializeServiceStateData - val FALLBACK_WEATHER_STEM_DISTANCE = 3600L } /** Weather Service is responsible to register other actors that require weather 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 672a5969dd..c90b3177c9 100644 --- a/src/main/scala/edu/ie3/simona/sim/setup/SimonaStandaloneSetup.scala +++ b/src/main/scala/edu/ie3/simona/sim/setup/SimonaStandaloneSetup.scala @@ -18,7 +18,7 @@ import edu.ie3.simona.agent.grid.GridAgentMessages.CreateGridAgent import edu.ie3.simona.api.ExtSimAdapter import edu.ie3.simona.api.data.ev.ExtEvDataConnection import edu.ie3.simona.api.simulation.ExtSimAdapterData -import edu.ie3.simona.config.{ArgsParser, GridConfigParser, SimonaConfig} +import edu.ie3.simona.config.{GridConfigParser, SimonaConfig} import edu.ie3.simona.event.listener.{ResultEventListener, RuntimeEventListener} import edu.ie3.simona.event.{ResultEvent, RuntimeEvent} import edu.ie3.simona.exceptions.agent.GridAgentInitializationException @@ -319,24 +319,15 @@ class SimonaStandaloneSetup( context: ActorContext[_] ): Seq[ActorRef[ResultEventListener.Request]] = { // append ResultEventListener as well to write raw output files - ArgsParser - .parseListenerConfigOption(simonaConfig.simona.event.listener) - .zipWithIndex - .map { case ((listenerCompanion, events), index) => - context.toClassic - .simonaActorOf( - listenerCompanion.props(events), - index.toString, - ) - .toTyped - } - .toSeq :+ context - .spawn( - ResultEventListener( - resultFileHierarchy - ), - ResultEventListener.getClass.getSimpleName, - ) + Seq( + context + .spawn( + ResultEventListener( + resultFileHierarchy + ), + ResultEventListener.getClass.getSimpleName, + ) + ) } def buildSubGridToActorRefMap( diff --git a/src/main/scala/edu/ie3/simona/util/ConfigUtil.scala b/src/main/scala/edu/ie3/simona/util/ConfigUtil.scala index 6ab7d8a02a..a76f550cc6 100644 --- a/src/main/scala/edu/ie3/simona/util/ConfigUtil.scala +++ b/src/main/scala/edu/ie3/simona/util/ConfigUtil.scala @@ -23,9 +23,9 @@ import edu.ie3.datamodel.models.result.{ NodeResult, ResultEntity, } -import edu.ie3.simona.config.RuntimeConfig.BaseRuntimeConfig -import edu.ie3.simona.config.{RuntimeConfig, SimonaConfig} +import edu.ie3.simona.config.RuntimeConfig.{BaseRuntimeConfig, EmRuntimeConfig} import edu.ie3.simona.config.SimonaConfig._ +import edu.ie3.simona.config.{RuntimeConfig, SimonaConfig} import edu.ie3.simona.event.notifier.{Notifier, NotifierConfig} import edu.ie3.simona.exceptions.InvalidConfigParameterException import org.apache.kafka.clients.admin.AdminClient @@ -41,6 +41,40 @@ import scala.util.{Failure, Success, Try, Using} object ConfigUtil { + final case class EmConfigUtil private ( + private val configs: Map[UUID, EmRuntimeConfig], + private val defaultConfigs: EmRuntimeConfig, + ) { + + /** Queries for a [[EmRuntimeConfig]], that applies for the given uuid and + * either returns the config for the requested uuid or the default config. + * + * @param uuid + * Identifier of the requested load model + * @return + * the requested config or a default value + */ + def getOrDefault(uuid: UUID): EmRuntimeConfig = + configs.getOrElse(uuid, defaultConfigs) + } + + object EmConfigUtil { + + /** Creates an em config utility from the given em configuration. It builds + * a map from uuid to individual em config for faster access. + * + * @param subConfig + * Configuration subtree for the behaviour of ems + * @return + * a matching config utility + */ + def apply(subConfig: AssetConfigs[EmRuntimeConfig]): EmConfigUtil = + EmConfigUtil( + buildUuidMapping(subConfig.individualConfigs), + subConfig.defaultConfig, + ) + } + final case class ParticipantConfigUtil private ( private val configs: Map[UUID, BaseRuntimeConfig], private val defaultConfigs: Map[Class[_], BaseRuntimeConfig], @@ -94,7 +128,6 @@ object ConfigUtil { subConfig.evcs.individualConfigs, subConfig.wec.individualConfigs, subConfig.storage.individualConfigs, - subConfig.em.individualConfigs, ).flatten ), Seq( @@ -105,21 +138,10 @@ object ConfigUtil { subConfig.wec.defaultConfig, subConfig.hp.defaultConfig, subConfig.storage.defaultConfig, - subConfig.em.defaultConfig, ).map { conf => conf.getClass -> conf }.toMap, ) } - private def buildUuidMapping( - configs: Seq[BaseRuntimeConfig] - ): Map[UUID, BaseRuntimeConfig] = - configs - .flatMap(modelConfig => - modelConfig.uuids - .map(UUID.fromString(_) -> modelConfig) - ) - .toMap - } /** A config utility to handle the output configuration for participant @@ -543,4 +565,14 @@ object ConfigUtil { } } + private def buildUuidMapping[T <: BaseRuntimeConfig]( + configs: Seq[T] + ): Map[UUID, T] = + configs + .flatMap(modelConfig => + modelConfig.uuids + .map(UUID.fromString(_) -> modelConfig) + ) + .toMap + } diff --git a/src/main/scala/edu/ie3/simona/util/TickUtil.scala b/src/main/scala/edu/ie3/simona/util/TickUtil.scala index 34f16d8e6e..b15d2e4c35 100644 --- a/src/main/scala/edu/ie3/simona/util/TickUtil.scala +++ b/src/main/scala/edu/ie3/simona/util/TickUtil.scala @@ -75,4 +75,36 @@ object TickUtil { (firstFullHourTick to lastAvailableTick by resolution.intValue).toArray } + /** Rounds given tick and datetime with regard to their (implicit) minutes and + * seconds. + * + * @param tick + * The given tick to round + * @param dateTime + * The given date and time to round + * @param resolution + * Resolution in seconds. Should divide 3600 without remainder, i.e. + * {{{3600 % resolution == 0}}} + */ + def roundToResolution( + tick: Long, + dateTime: ZonedDateTime, + resolution: Int, + ): (Long, ZonedDateTime) = { + + val givenHourSeconds = dateTime.getMinute * 60 + dateTime.getSecond + + val adaptedHourSeconds = givenHourSeconds / resolution * resolution + val adaptedMinute = adaptedHourSeconds / 60 + val adaptedSecond = adaptedHourSeconds % 60 + + val adaptedDateTime = + dateTime.withMinute(adaptedMinute).withSecond(adaptedSecond).withNano(0) + + val tickDifference = givenHourSeconds - adaptedHourSeconds + + val adaptedTick = tick - tickDifference + + (adaptedTick, adaptedDateTime) + } } diff --git a/src/main/scala/edu/ie3/util/scala/ReflectionTools.scala b/src/main/scala/edu/ie3/util/scala/ReflectionTools.scala deleted file mode 100644 index be6df1b955..0000000000 --- a/src/main/scala/edu/ie3/util/scala/ReflectionTools.scala +++ /dev/null @@ -1,64 +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.util.scala - -import scala.reflect.ClassTag -import scala.reflect.internal.util.ScalaClassLoader -import scala.reflect.runtime.universe -import scala.reflect.runtime.universe.TypeTag -import scala.util.Try - -/** Function collection for reflection tasks - */ -object ReflectionTools { - - def identifyValidCompanions(classNames: Iterable[String]): Iterable[Any] = { - classNames.flatMap(name => resolveClassNameToCompanion(name)).collect { - case companion: Any => companion - } - } - - def resolveClassNameToCompanion(className: String): Option[Any] = { - val clazz = ScalaClassLoader(getClass.getClassLoader) - .tryToLoadClass(className) - .getOrElse( - throw new RuntimeException( - s"Could not load class $className (It needs to be a fully-qualified class name)" - ) - ) - val mirror = universe.runtimeMirror(getClass.getClassLoader) - Try( - mirror.reflectModule(mirror.moduleSymbol(clazz)).instance - ).toOption - } - - /** Determine the field and their value of the provided object instance - * - * @param a - * the object that should be processed - * @param tt - * the type tag of the objects class - * @param ct - * the class tag of the objects class - * @tparam A - * type of the object - * @return - * a map containing the field method, and it's value of the object instance - */ - def classFieldToVal[A](a: A)(implicit - tt: TypeTag[A], - ct: ClassTag[A], - ): Map[universe.MethodSymbol, Any] = { - val members = tt.tpe.members.collect { - case m if m.isMethod && m.asMethod.isCaseAccessor => m.asMethod - } - members.map { member => - val memberValue = tt.mirror.reflect(a).reflectMethod(member)() - member -> memberValue - }.toMap - } -} diff --git a/src/test/resources/application-test.conf b/src/test/resources/application-test.conf new file mode 100644 index 0000000000..d6003344fc --- /dev/null +++ b/src/test/resources/application-test.conf @@ -0,0 +1,9 @@ +# Configuration for used for all scala tests + +pekko { + test { + # Multiply all relevant timeouts by this factor. + # This can prevent tests failing because the machine is too busy. + timefactor = 2 + } +} diff --git a/src/test/resources/edu/ie3/simona/model/participant/load/standard_load_profiles_test.csv b/src/test/resources/edu/ie3/simona/model/participant2/load/profile/standard_load_profiles_test.csv similarity index 100% rename from src/test/resources/edu/ie3/simona/model/participant/load/standard_load_profiles_test.csv rename to src/test/resources/edu/ie3/simona/model/participant2/load/profile/standard_load_profiles_test.csv diff --git a/src/test/resources/edu/ie3/simona/model/participant/load/random_load_parameters_test.csv b/src/test/resources/edu/ie3/simona/model/participant2/load/random/random_load_parameters_test.csv similarity index 100% rename from src/test/resources/edu/ie3/simona/model/participant/load/random_load_parameters_test.csv rename to src/test/resources/edu/ie3/simona/model/participant2/load/random/random_load_parameters_test.csv diff --git a/src/test/scala/edu/ie3/simona/agent/em/EmAgentIT.scala b/src/test/scala/edu/ie3/simona/agent/em/EmAgentIT.scala index ba54d5baf4..dfdad66a6f 100644 --- a/src/test/scala/edu/ie3/simona/agent/em/EmAgentIT.scala +++ b/src/test/scala/edu/ie3/simona/agent/em/EmAgentIT.scala @@ -7,9 +7,9 @@ package edu.ie3.simona.agent.em import edu.ie3.datamodel.models.result.system.EmResult +import edu.ie3.simona.agent.grid.GridAgent import edu.ie3.simona.agent.participant.data.secondary.SecondaryDataService.ActorWeatherService import edu.ie3.simona.agent.participant.hp.HpAgent -import edu.ie3.simona.agent.participant.load.LoadAgent.FixedLoadAgent import edu.ie3.simona.agent.participant.pv.PvAgent import edu.ie3.simona.agent.participant.statedata.ParticipantStateData.ParticipantInitializeStateData import edu.ie3.simona.agent.participant.storage.StorageAgent @@ -18,6 +18,11 @@ import edu.ie3.simona.agent.participant2.ParticipantAgent.{ RegistrationFailedMessage, RegistrationSuccessfulMessage, } +import edu.ie3.simona.agent.participant2.ParticipantAgentInit +import edu.ie3.simona.agent.participant2.ParticipantAgentInit.{ + ParticipantRefs, + SimulationParameters, +} import edu.ie3.simona.config.RuntimeConfig._ import edu.ie3.simona.event.ResultEvent import edu.ie3.simona.event.ResultEvent.ParticipantResultEvent @@ -33,6 +38,7 @@ import edu.ie3.simona.ontology.messages.services.WeatherMessage.{ WeatherData, } import edu.ie3.simona.ontology.messages.{Activation, SchedulerMessage} +import edu.ie3.simona.service.ServiceType import edu.ie3.simona.test.common.input.EmInputTestData import edu.ie3.simona.util.SimonaConstants.INIT_SIM_TICK import edu.ie3.simona.util.TickUtil.TickLong @@ -47,10 +53,10 @@ import org.apache.pekko.actor.testkit.typed.scaladsl.{ } import org.apache.pekko.actor.typed.scaladsl.adapter._ import org.apache.pekko.testkit.TestActorRef -import org.scalatest.OptionValues._ import org.scalatest.matchers.should import org.scalatest.wordspec.AnyWordSpecLike import org.scalatestplus.mockito.MockitoSugar +import squants.Each import squants.motion.MetersPerSecond import squants.thermal.Celsius @@ -71,6 +77,13 @@ class EmAgentIT private val resolution = simonaConfig.simona.powerflow.resolution.toSeconds + private val simulationParams = SimulationParameters( + expectedPowerRequestTick = Long.MaxValue, + requestVoltageDeviationTolerance = Each(1e-14d), + simulationStart = simulationStartDate, + simulationEnd = simulationEndDate, + ) + private val outputConfigOn = NotifierConfig( simulationResultInfo = true, powerRequestReply = false, @@ -95,12 +108,21 @@ class EmAgentIT "An em agent" when { "having load, pv and storage agents connected" should { "be initialized correctly and run through some activations" in { + val gridAgent = TestProbe[GridAgent.Request]("GridAgent") val resultListener = TestProbe[ResultEvent]("ResultListener") val primaryServiceProxy = TestProbe[ServiceMessage]("PrimaryServiceProxy") val weatherService = TestProbe[ServiceMessage]("WeatherService") val scheduler = TestProbe[SchedulerMessage]("Scheduler") + val participantRefs = ParticipantRefs( + gridAgent = gridAgent.ref, + primaryServiceProxy = primaryServiceProxy.ref.toClassic, + services = + Map(ServiceType.WeatherService -> weatherService.ref.toClassic), + resultListener = Iterable(resultListener.ref), + ) + val emAgent = spawn( EmAgent( emInput, @@ -114,24 +136,14 @@ class EmAgentIT "EmAgent", ) - val loadAgent = TestActorRef( - new FixedLoadAgent( - scheduler = scheduler.ref.toClassic, - initStateData = ParticipantInitializeStateData( - loadInput, - LoadRuntimeConfig( - calculateMissingReactivePowerWithModel = true - ), - primaryServiceProxy.ref.toClassic, - None, - simulationStartDate, - simulationEndDate, - resolution, - simonaConfig.simona.runtime.participant.requestVoltageDeviationThreshold, - outputConfigOff, - Some(emAgent), - ), - listener = Iterable(resultListener.ref.toClassic), + val loadAgent = spawn( + ParticipantAgentInit( + loadInput, + LoadRuntimeConfig(), + outputConfigOff, + participantRefs, + simulationParams, + Right(emAgent), ), "LoadAgent", ) @@ -181,36 +193,24 @@ class EmAgentIT "StorageAgent", ) - scheduler.expectNoMessage() + val emInitSchedule = scheduler.expectMessageType[ScheduleActivation] + emInitSchedule.tick shouldBe INIT_SIM_TICK + val emAgentActivation = emInitSchedule.actor /* INIT */ - // load - loadAgent ! Activation(INIT_SIM_TICK) + emAgentActivation ! Activation(INIT_SIM_TICK) + // load primaryServiceProxy.expectMessage( - PrimaryServiceRegistrationMessage(loadAgent.ref, loadInput.getUuid) + PrimaryServiceRegistrationMessage( + loadAgent.ref.toClassic, + loadInput.getUuid, + ) ) loadAgent ! RegistrationFailedMessage(primaryServiceProxy.ref.toClassic) - // the order of the two messages is not given - val emAgentActivation = scheduler - .receiveMessages(2) - .flatMap { - case Completion(ref, maybeNewTick) => - ref shouldBe loadAgent.toTyped - maybeNewTick shouldBe None - None - case ScheduleActivation(ref, tick, unlockKey) => - // em agent schedules itself - tick shouldBe 0 - unlockKey shouldBe None - Some(ref) - case unexpected => - fail(s"Received unexpected message $unexpected") - } - .headOption - .value + scheduler.expectMessage(Completion(emAgentActivation, Some(0))) // pv pvAgent ! Activation(INIT_SIM_TICK) @@ -288,7 +288,7 @@ class EmAgentIT /* TICK 7200 LOAD: 0.269 kW (unchanged) - PV: -3.791 kW + PV: -3.715 kW STORAGE: SOC 63.3 % -> charge with 3.522 kW -> remaining 0 kW @@ -300,8 +300,8 @@ class EmAgentIT 7200, weatherService.ref.toClassic, WeatherData( - WattsPerSquareMeter(50d), - WattsPerSquareMeter(150d), + WattsPerSquareMeter(45d), + WattsPerSquareMeter(140d), Celsius(0d), MetersPerSecond(0d), ), @@ -318,24 +318,24 @@ class EmAgentIT emResult.getQ should equalWithTolerance(0.0000882855367.asMegaVar) } - scheduler.expectMessage(Completion(emAgentActivation, Some(13115))) + scheduler.expectMessage(Completion(emAgentActivation, Some(13247))) - /* TICK 13115 + /* TICK 13247 LOAD: 0.269 kW (unchanged) - PV: -3.791 kW (unchanged) + PV: -3.715 kW (unchanged) STORAGE: SOC 100 % -> charge with 0 kW - -> remaining -3.522 kW + -> remaining -3.447 kW */ - emAgentActivation ! Activation(13115) + emAgentActivation ! Activation(13247) resultListener.expectMessageType[ParticipantResultEvent] match { case ParticipantResultEvent(emResult: EmResult) => emResult.getInputModel shouldBe emInput.getUuid - emResult.getTime shouldBe 13115L.toDateTime + emResult.getTime shouldBe 13247.toDateTime emResult.getP should equalWithTolerance( - -0.0035233186089842434.asMegaWatt + -0.0034468567291.asMegaWatt ) emResult.getQ should equalWithTolerance(0.0000882855367.asMegaVar) } @@ -344,9 +344,9 @@ class EmAgentIT /* TICK 14400 LOAD: 0.269 kW (unchanged) - PV: -0.069 kW + PV: -0.07 kW STORAGE: SOC 100 % - -> discharge with 0.2 kW + -> discharge with 0.199 kW -> remaining 0.0 kW */ @@ -383,12 +383,21 @@ class EmAgentIT "having load, pv and heat pump agents connected" should { "be initialized correctly and run through some activations" in { + val gridAgent = TestProbe[GridAgent.Request]("GridAgent") val resultListener = TestProbe[ResultEvent]("ResultListener") val primaryServiceProxy = TestProbe[ServiceMessage]("PrimaryServiceProxy") val weatherService = TestProbe[ServiceMessage]("WeatherService") val scheduler = TestProbe[SchedulerMessage]("Scheduler") + val participantRefs = ParticipantRefs( + gridAgent = gridAgent.ref, + primaryServiceProxy = primaryServiceProxy.ref.toClassic, + services = + Map(ServiceType.WeatherService -> weatherService.ref.toClassic), + resultListener = Iterable(resultListener.ref), + ) + val emAgent = spawn( EmAgent( emInput, @@ -402,24 +411,14 @@ class EmAgentIT "EmAgent1", ) - val loadAgent = TestActorRef( - new FixedLoadAgent( - scheduler = scheduler.ref.toClassic, - initStateData = ParticipantInitializeStateData( - loadInput, - LoadRuntimeConfig( - calculateMissingReactivePowerWithModel = true - ), - primaryServiceProxy.ref.toClassic, - None, - simulationStartDate, - simulationEndDate, - resolution, - simonaConfig.simona.runtime.participant.requestVoltageDeviationThreshold, - outputConfigOff, - Some(emAgent), - ), - listener = Iterable(resultListener.ref.toClassic), + val loadAgent = spawn( + ParticipantAgentInit( + loadInput, + LoadRuntimeConfig(), + outputConfigOff, + participantRefs, + simulationParams, + Right(emAgent), ), "LoadAgent1", ) @@ -470,36 +469,24 @@ class EmAgentIT "HeatPumpAgent1", ) - scheduler.expectNoMessage() + val emInitSchedule = scheduler.expectMessageType[ScheduleActivation] + emInitSchedule.tick shouldBe INIT_SIM_TICK + val emAgentActivation = emInitSchedule.actor /* INIT */ - // load - loadAgent ! Activation(INIT_SIM_TICK) + emAgentActivation ! Activation(INIT_SIM_TICK) + // load primaryServiceProxy.expectMessage( - PrimaryServiceRegistrationMessage(loadAgent.ref, loadInput.getUuid) + PrimaryServiceRegistrationMessage( + loadAgent.ref.toClassic, + loadInput.getUuid, + ) ) loadAgent ! RegistrationFailedMessage(primaryServiceProxy.ref.toClassic) - // the order of the two messages is not given - val emAgentActivation = scheduler - .receiveMessages(2) - .flatMap { - case Completion(ref, maybeNewTick) => - ref shouldBe loadAgent.toTyped - maybeNewTick shouldBe None - None - case ScheduleActivation(ref, tick, unlockKey) => - // em agent schedules itself - tick shouldBe 0 - unlockKey shouldBe None - Some(ref) - case unexpected => - fail(s"Received unexpected message $unexpected") - } - .headOption - .value + scheduler.expectMessage(Completion(emAgentActivation, Some(0))) // pv pvAgent ! Activation(INIT_SIM_TICK) @@ -593,10 +580,10 @@ class EmAgentIT /* TICK 7200 LOAD: 0.269 kW (unchanged) - PV: -3.791 kW + PV: -3.715 kW Heat pump: running (turned on from last request), can also be turned off -> set point ~3.5 kW (bigger than 50 % rated apparent power): stays turned on with unchanged state - -> remaining 1.327 kW + -> remaining 1.403 kW */ emAgentActivation ! Activation(7200) @@ -606,8 +593,8 @@ class EmAgentIT 7200, weatherService.ref.toClassic, WeatherData( - WattsPerSquareMeter(50d), - WattsPerSquareMeter(150d), + WattsPerSquareMeter(45d), + WattsPerSquareMeter(140d), Celsius(0d), MetersPerSecond(0d), ), @@ -620,10 +607,10 @@ class EmAgentIT emResult.getInputModel shouldBe emInput.getUuid emResult.getTime shouldBe 7200.toDateTime emResult.getP should equalWithTolerance( - 0.0013266813910157566.asMegaWatt + 0.0014031432709.asMegaWatt ) emResult.getQ should equalWithTolerance( - 0.0010731200407782782.asMegaVar + 0.0010731200408.asMegaVar ) } @@ -657,12 +644,12 @@ class EmAgentIT resultListener.expectMessageType[ParticipantResultEvent] match { case ParticipantResultEvent(emResult: EmResult) => emResult.getInputModel shouldBe emInput.getUuid - emResult.getTime shouldBe 14400L.toDateTime + emResult.getTime shouldBe 14400.toDateTime emResult.getP should equalWithTolerance( - 0.00019892577822992104.asMegaWatt + 0.0001988993578.asMegaWatt ) emResult.getQ should equalWithTolerance( - 0.0000882855367033582.asMegaVar + 0.0000882855367.asMegaVar ) } @@ -670,10 +657,10 @@ class EmAgentIT /* TICK 21600 LOAD: 0.269 kW (unchanged) - PV: -0.023 kW + PV: -0.024 kW Heat pump: Is not running, can run or stay off -> flex signal is 0 MW: Heat pump is turned off - -> remaining 0.245 kW + -> remaining 0.244 kW */ emAgentActivation ! Activation(21600) @@ -698,10 +685,10 @@ class EmAgentIT emResult.getInputModel shouldBe emInput.getUuid emResult.getTime shouldBe 21600.toDateTime emResult.getP should equalWithTolerance( - 0.0002450436827011999.asMegaWatt + 0.0002442471208.asMegaWatt ) emResult.getQ should equalWithTolerance( - 0.0000882855367033582.asMegaVar + 0.0000882855367.asMegaVar ) } 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 2fe1f0fbb5..b3bf299c4b 100644 --- a/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmParticipantSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmParticipantSpec.scala @@ -133,7 +133,7 @@ class DBFSAlgorithmParticipantSpec loadAgent ! Activation(0) // the load agent should send a Completion - scheduler.expectMessage(Completion(loadAgent, None)) + scheduler.expectMessage(Completion(loadAgent, Some(3600))) } diff --git a/src/test/scala/edu/ie3/simona/agent/participant/FixedFeedInAgentModelCalculationSpec.scala b/src/test/scala/edu/ie3/simona/agent/participant/FixedFeedInAgentModelCalculationSpec.scala deleted file mode 100644 index e6174a6e12..0000000000 --- a/src/test/scala/edu/ie3/simona/agent/participant/FixedFeedInAgentModelCalculationSpec.scala +++ /dev/null @@ -1,468 +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.participant - -import com.typesafe.config.ConfigFactory -import edu.ie3.datamodel.models.input.system.FixedFeedInInput -import edu.ie3.datamodel.models.input.system.characteristic.QV -import edu.ie3.simona.agent.ValueStore -import edu.ie3.simona.agent.grid.GridAgentMessages.{ - AssetPowerChangedMessage, - AssetPowerUnchangedMessage, -} -import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ComplexPower -import edu.ie3.simona.agent.participant.fixedfeedin.FixedFeedInAgent -import edu.ie3.simona.agent.participant.statedata.BaseStateData.ParticipantModelBaseStateData -import edu.ie3.simona.agent.participant.statedata.ParticipantStateData.{ - ParticipantInitializeStateData, - ParticipantInitializingStateData, - ParticipantUninitializedStateData, - SimpleInputContainer, -} -import edu.ie3.simona.agent.participant2.ParticipantAgent.{ - RegistrationFailedMessage, - RequestAssetPowerMessage, -} -import edu.ie3.simona.agent.state.AgentState.{Idle, Uninitialized} -import edu.ie3.simona.agent.state.ParticipantAgentState.HandleInformation -import edu.ie3.simona.config.SimonaConfig -import edu.ie3.simona.config.RuntimeConfig.FixedFeedInRuntimeConfig -import edu.ie3.simona.event.notifier.NotifierConfig -import edu.ie3.simona.model.participant.load.{LoadModelBehaviour, LoadReference} -import edu.ie3.simona.ontology.messages.Activation -import edu.ie3.simona.ontology.messages.SchedulerMessage.Completion -import edu.ie3.simona.ontology.messages.services.ServiceMessage.PrimaryServiceRegistrationMessage -import edu.ie3.simona.test.ParticipantAgentSpec -import edu.ie3.simona.test.common.input.FixedFeedInputTestData -import edu.ie3.simona.util.ConfigUtil -import edu.ie3.simona.util.SimonaConstants.INIT_SIM_TICK -import edu.ie3.util.TimeUtil -import edu.ie3.util.scala.quantities.{Megavars, ReactivePower, Vars} -import org.apache.pekko.actor.ActorSystem -import org.apache.pekko.actor.typed.scaladsl.adapter.ClassicActorRefOps -import org.apache.pekko.testkit.TestFSMRef -import org.apache.pekko.util.Timeout -import squants.Each -import squants.energy.{Kilowatts, Megawatts, Watts} - -import java.time.ZonedDateTime -import java.util.concurrent.TimeUnit -import scala.collection.SortedMap - -class FixedFeedInAgentModelCalculationSpec - extends ParticipantAgentSpec( - ActorSystem( - "FixedFeedInAgentModelCalculationSpec", - ConfigFactory - .parseString(""" - |pekko.loggers =["org.apache.pekko.event.slf4j.Slf4jLogger"] - |pekko.loglevel="DEBUG" - """.stripMargin), - ) - ) - with FixedFeedInputTestData { - implicit val receiveTimeOut: Timeout = Timeout(10, TimeUnit.SECONDS) - implicit val noReceiveTimeOut: Timeout = Timeout(1, TimeUnit.SECONDS) - - /* Alter the input model to have a voltage sensitive reactive power calculation */ - val voltageSensitiveInput: FixedFeedInInput = fixedFeedInput - .copy() - .qCharacteristics(new QV("qV:{(0.95,-0.625),(1.05,0.625)}")) - .build() - - protected val simulationStartDate: ZonedDateTime = - TimeUtil.withDefaults.toZonedDateTime("2020-01-01T00:00:00Z") - protected val simulationEndDate: ZonedDateTime = - TimeUtil.withDefaults.toZonedDateTime("2020-01-01T01:00:00Z") - - private implicit val powerTolerance: squants.Power = Watts(0.1) - private implicit val reactivePowerTolerance: ReactivePower = Vars(0.1) - - private val simonaConfig: SimonaConfig = - createSimonaConfig( - LoadModelBehaviour.FIX, - LoadReference.ActivePower(Kilowatts(0d)), - ) - private val defaultOutputConfig = NotifierConfig( - simonaConfig.simona.output.participant.defaultConfig.simulationResult, - simonaConfig.simona.output.participant.defaultConfig.powerRequestReply, - simonaConfig.simona.output.participant.defaultConfig.flexResult, - ) - - private val fixedFeedConfigUtil = ConfigUtil.ParticipantConfigUtil( - simonaConfig.simona.runtime.participant - ) - private val modelConfig = - fixedFeedConfigUtil.getOrDefault[FixedFeedInRuntimeConfig]( - voltageSensitiveInput.getUuid - ) - private val services = Iterable.empty - private val resolution = simonaConfig.simona.powerflow.resolution.toSeconds - - "A fixed feed in agent with model calculation " should { - val initStateData = ParticipantInitializeStateData[ - FixedFeedInInput, - FixedFeedInRuntimeConfig, - ComplexPower, - ]( - inputModel = voltageSensitiveInput, - modelConfig = modelConfig, - secondaryDataServices = services, - simulationStartDate = simulationStartDate, - simulationEndDate = simulationEndDate, - resolution = resolution, - requestVoltageDeviationThreshold = - simonaConfig.simona.runtime.participant.requestVoltageDeviationThreshold, - outputConfig = defaultOutputConfig, - primaryServiceProxy = primaryServiceProxy.ref, - ) - - "be instantiated correctly" in { - val fixedFeedAgent = TestFSMRef( - new FixedFeedInAgent( - scheduler = scheduler.ref, - initStateData = initStateData, - listener = systemListener, - ) - ) - - fixedFeedAgent.stateName shouldBe Uninitialized - // ParticipantUninitializedStateData is an empty class (due to typing). If it contains content one day - inside(fixedFeedAgent.stateData) { - case _: ParticipantUninitializedStateData[_] => succeed - case _ => - fail( - s"Expected $ParticipantUninitializedStateData, but got ${fixedFeedAgent.stateData}." - ) - } - } - - "end in correct state with correct state data after initialisation" in { - val fixedFeedAgent = TestFSMRef( - new FixedFeedInAgent( - scheduler = scheduler.ref, - initStateData = initStateData, - listener = systemListener, - ) - ) - - scheduler.send(fixedFeedAgent, Activation(INIT_SIM_TICK)) - - /* Actor should ask for registration with primary service */ - primaryServiceProxy.expectMsg( - PrimaryServiceRegistrationMessage( - fixedFeedAgent.ref, - voltageSensitiveInput.getUuid, - ) - ) - /* State should be information handling and having correct state data */ - fixedFeedAgent.stateName shouldBe HandleInformation - fixedFeedAgent.stateData match { - case ParticipantInitializingStateData( - inputModel, - modelConfig, - secondaryDataServices, - simulationStartDate, - simulationEndDate, - resolution, - requestVoltageDeviationThreshold, - outputConfig, - maybeEmAgent, - ) => - inputModel shouldBe SimpleInputContainer(voltageSensitiveInput) - modelConfig shouldBe modelConfig - secondaryDataServices shouldBe services - simulationStartDate shouldBe this.simulationStartDate - simulationEndDate shouldBe this.simulationEndDate - resolution shouldBe this.resolution - requestVoltageDeviationThreshold shouldBe simonaConfig.simona.runtime.participant.requestVoltageDeviationThreshold - outputConfig shouldBe defaultOutputConfig - maybeEmAgent shouldBe None - case unsuitableStateData => - fail(s"Agent has unsuitable state data '$unsuitableStateData'.") - } - - /* Refuse registration */ - primaryServiceProxy.send( - fixedFeedAgent, - RegistrationFailedMessage(primaryServiceProxy.ref), - ) - - /* Expect a completion notification */ - scheduler.expectMsg(Completion(fixedFeedAgent.toTyped, Some(0))) - - /* ... as well as corresponding state and state data */ - fixedFeedAgent.stateName shouldBe Idle - fixedFeedAgent.stateData match { - case ParticipantModelBaseStateData( - startDate, - endDate, - _, - services, - outputConfig, - additionalActivationTicks, - foreseenDataTicks, - _, - voltageValueStore, - resultValueStore, - requestValueStore, - _, - _, - _, - ) => - /* Base state data */ - startDate shouldBe simulationStartDate - endDate shouldBe simulationEndDate - services shouldBe Iterable.empty - outputConfig shouldBe defaultOutputConfig - additionalActivationTicks shouldBe empty - foreseenDataTicks shouldBe Map.empty - voltageValueStore shouldBe ValueStore( - resolution, - SortedMap(0L -> Each(1.0)), - ) - resultValueStore shouldBe ValueStore(resolution) - requestValueStore shouldBe ValueStore[ComplexPower]( - resolution - ) - case _ => - fail( - s"Did not find expected state data $ParticipantModelBaseStateData, but ${fixedFeedAgent.stateData}" - ) - } - } - - "answer with zero power, if asked directly after initialisation" in { - val fixedFeedAgent = TestFSMRef( - new FixedFeedInAgent( - scheduler = scheduler.ref, - initStateData = initStateData, - listener = systemListener, - ) - ) - - scheduler.send(fixedFeedAgent, Activation(INIT_SIM_TICK)) - - /* Refuse registration with primary service */ - primaryServiceProxy.expectMsgType[PrimaryServiceRegistrationMessage] - primaryServiceProxy.send( - fixedFeedAgent, - RegistrationFailedMessage(primaryServiceProxy.ref), - ) - - /* I'm not interested in the content of the Completion */ - scheduler.expectMsgType[Completion] - - fixedFeedAgent.stateName shouldBe Idle - /* State data has already been tested */ - - fixedFeedAgent ! RequestAssetPowerMessage( - 0L, - Each(1d), - Each(0d), - self.toTyped, - ) - expectMsg( - AssetPowerChangedMessage( - Megawatts(0d), - Megavars(0d), - ) - ) - - inside(fixedFeedAgent.stateData) { - case baseStateData: ParticipantModelBaseStateData[_, _, _, _] => - baseStateData.requestValueStore shouldBe ValueStore[ - ComplexPower - ]( - resolution, - SortedMap( - 0L -> ComplexPower( - Megawatts(0d), - Megavars(0d), - ) - ), - ) - case _ => - fail( - s"Did not found the expected state data $ParticipantModelBaseStateData, but ${fixedFeedAgent.stateData}" - ) - } - } - - "do correct transitions when triggered in Idle" in { - val fixedFeedAgent = TestFSMRef( - new FixedFeedInAgent( - scheduler = scheduler.ref, - initStateData = initStateData, - listener = systemListener, - ) - ) - - scheduler.send(fixedFeedAgent, Activation(INIT_SIM_TICK)) - - /* Refuse registration with primary service */ - primaryServiceProxy.expectMsgType[PrimaryServiceRegistrationMessage] - primaryServiceProxy.send( - fixedFeedAgent, - RegistrationFailedMessage(primaryServiceProxy.ref), - ) - - /* I am not interested in the Completion */ - scheduler.expectMsgType[Completion] - awaitAssert(fixedFeedAgent.stateName shouldBe Idle) - /* State data is tested in another test */ - - scheduler.send(fixedFeedAgent, Activation(0)) - - /* Expect confirmation */ - scheduler.expectMsg(Completion(fixedFeedAgent.toTyped, None)) - - /* Intermediate transitions and states cannot be tested, as the agents triggers itself - * too fast */ - - awaitAssert(fixedFeedAgent.stateName shouldBe Idle) - inside(fixedFeedAgent.stateData) { - case baseStateData: ParticipantModelBaseStateData[_, _, _, _] => - baseStateData.resultValueStore.last(0L) match { - case Some((tick, entry)) => - tick shouldBe 0L - inside(entry) { case ComplexPower(p, q) => - p should approximate(Megawatts(-268.603e-6)) - q should approximate(Megavars(0.0)) - } - case None => - fail("Result value store does not contain entry for tick 900.") - } - case _ => - fail( - s"Did not find the expected state data $ParticipantModelBaseStateData, but ${fixedFeedAgent.stateData}" - ) - } - } - - "provide the correct average power after one data tick is available" in { - val fixedFeedAgent = TestFSMRef( - new FixedFeedInAgent( - scheduler = scheduler.ref, - initStateData = initStateData, - listener = systemListener, - ) - ) - - /* Trigger the initialisation */ - scheduler.send(fixedFeedAgent, Activation(INIT_SIM_TICK)) - - /* Refuse registration with primary service */ - primaryServiceProxy.expectMsgType[PrimaryServiceRegistrationMessage] - primaryServiceProxy.send( - fixedFeedAgent, - RegistrationFailedMessage(primaryServiceProxy.ref), - ) - - scheduler.expectMsg(Completion(fixedFeedAgent.toTyped, Some(0))) - - /* Trigger the data generation in tick 0 */ - scheduler.send(fixedFeedAgent, Activation(0)) - - scheduler.expectMsg(Completion(fixedFeedAgent.toTyped)) - - awaitAssert(fixedFeedAgent.stateName shouldBe Idle) - - /* Ask the agent for average power in tick 3000 */ - fixedFeedAgent ! RequestAssetPowerMessage( - 3000L, - Each(1d), - Each(0d), - self.toTyped, - ) - - expectMsgType[AssetPowerChangedMessage] match { - case AssetPowerChangedMessage(p, q) => - p should approximate(Megawatts(-268.603e-6)) - q should approximate(Megavars(0.0)) - } - } - - val fixedFeedAgent = TestFSMRef( - new FixedFeedInAgent( - scheduler = scheduler.ref, - initStateData = initStateData, - listener = systemListener, - ) - ) - - "does answer unchanged power values after asking a second time with considerably same voltage" in { - - /* Trigger the initialisation */ - scheduler.send(fixedFeedAgent, Activation(INIT_SIM_TICK)) - - /* Refuse registration with primary service */ - primaryServiceProxy.expectMsgType[PrimaryServiceRegistrationMessage] - primaryServiceProxy.send( - fixedFeedAgent, - RegistrationFailedMessage(primaryServiceProxy.ref), - ) - - scheduler.expectMsg(Completion(fixedFeedAgent.toTyped, Some(0))) - - /* Trigger the data generation in tick 0 */ - scheduler.send(fixedFeedAgent, Activation(0)) - - scheduler.expectMsg(Completion(fixedFeedAgent.toTyped)) - - /* Ask the agent for average power in tick 3000 */ - fixedFeedAgent ! RequestAssetPowerMessage( - 3000L, - Each(1d), - Each(0d), - self.toTyped, - ) - - expectMsgType[AssetPowerChangedMessage] match { - case AssetPowerChangedMessage(p, q) => - p should approximate(Megawatts(-268.603e-6)) - q should approximate(Megavars(0.0)) - case answer => fail(s"Did not expect to get that answer: $answer") - } - } - - "replies unchanged power values after asking a second time" in { - /* Previous request stems from previous test */ - /* Ask again with unchanged information */ - fixedFeedAgent ! RequestAssetPowerMessage( - 3000L, - Each(1.000000000000001d), - Each(0d), - self.toTyped, - ) - - /* Expect, that nothing has changed */ - expectMsgType[AssetPowerUnchangedMessage] match { - case AssetPowerUnchangedMessage(p, q) => - p should approximate(Megawatts(-268.603e-6)) - q should approximate(Megavars(0.0)) - } - } - - "does answer changed power values after asking a second time with different voltage" in { - /* Ask again with changed information */ - fixedFeedAgent ! RequestAssetPowerMessage( - 3000L, - Each(0.98), - Each(0d), - self.toTyped, - ) - - /* Expect, the correct values (this model has fixed power factor) */ - expectMsgClass(classOf[AssetPowerChangedMessage]) match { - case AssetPowerChangedMessage(p, q) => - p should approximate(Megawatts(-0.000268603)) - q should approximate(Megavars(-22.07138418e-6)) - } - } - } -} diff --git a/src/test/scala/edu/ie3/simona/agent/participant/LoadAgentFixedModelCalculationSpec.scala b/src/test/scala/edu/ie3/simona/agent/participant/LoadAgentFixedModelCalculationSpec.scala deleted file mode 100644 index 6ffaba9066..0000000000 --- a/src/test/scala/edu/ie3/simona/agent/participant/LoadAgentFixedModelCalculationSpec.scala +++ /dev/null @@ -1,462 +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.participant - -import com.typesafe.config.ConfigFactory -import edu.ie3.datamodel.models.input.system.LoadInput -import edu.ie3.datamodel.models.input.system.characteristic.QV -import edu.ie3.simona.agent.ValueStore -import edu.ie3.simona.agent.grid.GridAgentMessages.{ - AssetPowerChangedMessage, - AssetPowerUnchangedMessage, -} -import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ComplexPower -import edu.ie3.simona.agent.participant.load.LoadAgent.FixedLoadAgent -import edu.ie3.simona.agent.participant.statedata.BaseStateData.ParticipantModelBaseStateData -import edu.ie3.simona.agent.participant.statedata.ParticipantStateData.{ - ParticipantInitializeStateData, - ParticipantInitializingStateData, - ParticipantUninitializedStateData, - SimpleInputContainer, -} -import edu.ie3.simona.agent.participant2.ParticipantAgent.{ - RegistrationFailedMessage, - RequestAssetPowerMessage, -} -import edu.ie3.simona.agent.state.AgentState.{Idle, Uninitialized} -import edu.ie3.simona.agent.state.ParticipantAgentState.HandleInformation -import edu.ie3.simona.config.SimonaConfig -import edu.ie3.simona.config.RuntimeConfig.LoadRuntimeConfig -import edu.ie3.simona.event.notifier.NotifierConfig -import edu.ie3.simona.model.participant.load.{LoadModelBehaviour, LoadReference} -import edu.ie3.simona.ontology.messages.Activation -import edu.ie3.simona.ontology.messages.SchedulerMessage.Completion -import edu.ie3.simona.ontology.messages.services.ServiceMessage.PrimaryServiceRegistrationMessage -import edu.ie3.simona.test.ParticipantAgentSpec -import edu.ie3.simona.test.common.model.participant.LoadTestData -import edu.ie3.simona.util.ConfigUtil -import edu.ie3.simona.util.SimonaConstants.INIT_SIM_TICK -import edu.ie3.util.scala.quantities.{Megavars, ReactivePower, Vars} -import org.apache.pekko.actor.ActorSystem -import org.apache.pekko.actor.typed.scaladsl.adapter.ClassicActorRefOps -import org.apache.pekko.testkit.TestFSMRef -import org.apache.pekko.util.Timeout -import org.scalatest.PrivateMethodTester -import squants.Each -import squants.energy.{Kilowatts, Megawatts, Watts} - -import java.util.concurrent.TimeUnit -import scala.collection.SortedMap - -class LoadAgentFixedModelCalculationSpec - extends ParticipantAgentSpec( - ActorSystem( - "LoadAgentFixedModelCalculationSpec", - ConfigFactory - .parseString(""" - |pekko.loggers =["org.apache.pekko.event.slf4j.Slf4jLogger"] - |pekko.loglevel="DEBUG" - """.stripMargin), - ) - ) - with LoadTestData - with PrivateMethodTester { - implicit val receiveTimeOut: Timeout = Timeout(10, TimeUnit.SECONDS) - implicit val noReceiveTimeOut: Timeout = Timeout(1, TimeUnit.SECONDS) - - /* Alter the input model to have a voltage sensitive reactive power calculation */ - val voltageSensitiveInput: LoadInput = loadInput - .copy() - .qCharacteristics(new QV("qV:{(0.95,-0.625),(1.05,0.625)}")) - .build() - - private val simonaConfig: SimonaConfig = - createSimonaConfig( - LoadModelBehaviour.FIX, - LoadReference.ActivePower(Kilowatts(0d)), - ) - private val defaultOutputConfig = NotifierConfig( - simonaConfig.simona.output.participant.defaultConfig.simulationResult, - simonaConfig.simona.output.participant.defaultConfig.powerRequestReply, - simonaConfig.simona.output.participant.defaultConfig.flexResult, - ) - private val loadConfigUtil = ConfigUtil.ParticipantConfigUtil( - simonaConfig.simona.runtime.participant - ) - private val modelConfig = - loadConfigUtil.getOrDefault[LoadRuntimeConfig]( - voltageSensitiveInput.getUuid - ) - private val services = Iterable.empty - private val resolution = simonaConfig.simona.powerflow.resolution.toSeconds - - private implicit val powerTolerance: squants.Power = Watts(0.1) - private implicit val reactivePowerTolerance: ReactivePower = Vars(0.1) - - "A load agent with fixed model calculation depending on no secondary data service" should { - val initStateData = ParticipantInitializeStateData[ - LoadInput, - LoadRuntimeConfig, - ComplexPower, - ]( - inputModel = voltageSensitiveInput, - modelConfig = modelConfig, - secondaryDataServices = services, - simulationStartDate = simulationStartDate, - simulationEndDate = simulationEndDate, - resolution = resolution, - requestVoltageDeviationThreshold = - simonaConfig.simona.runtime.participant.requestVoltageDeviationThreshold, - outputConfig = defaultOutputConfig, - primaryServiceProxy = primaryServiceProxy.ref, - ) - - "be instantiated correctly" in { - val loadAgent = TestFSMRef( - new FixedLoadAgent( - scheduler = scheduler.ref, - initStateData = initStateData, - listener = systemListener, - ) - ) - - loadAgent.stateName shouldBe Uninitialized - // ParticipantUninitializedStateData is an empty class (due to typing). If it contains content one day - inside(loadAgent.stateData) { - case _: ParticipantUninitializedStateData[_] => succeed - case _ => - fail( - s"Expected $ParticipantUninitializedStateData, but got ${loadAgent.stateData}." - ) - } - } - - "end in correct state with correct state data after initialisation" in { - val loadAgent = TestFSMRef( - new FixedLoadAgent( - scheduler = scheduler.ref, - initStateData = initStateData, - listener = systemListener, - ) - ) - - scheduler.send(loadAgent, Activation(INIT_SIM_TICK)) - - /* Actor should ask for registration with primary service */ - primaryServiceProxy.expectMsg( - PrimaryServiceRegistrationMessage( - loadAgent.ref, - voltageSensitiveInput.getUuid, - ) - ) - /* State should be information handling and having correct state data */ - loadAgent.stateName shouldBe HandleInformation - loadAgent.stateData match { - case ParticipantInitializingStateData( - inputModel, - modelConfig, - secondaryDataServices, - simulationStartDate, - simulationEndDate, - resolution, - requestVoltageDeviationThreshold, - outputConfig, - maybeEmAgent, - ) => - inputModel shouldBe SimpleInputContainer(voltageSensitiveInput) - modelConfig shouldBe modelConfig - secondaryDataServices shouldBe services - simulationStartDate shouldBe this.simulationStartDate - simulationEndDate shouldBe this.simulationEndDate - resolution shouldBe this.resolution - requestVoltageDeviationThreshold shouldBe simonaConfig.simona.runtime.participant.requestVoltageDeviationThreshold - outputConfig shouldBe defaultOutputConfig - maybeEmAgent shouldBe None - case unsuitableStateData => - fail(s"Agent has unsuitable state data '$unsuitableStateData'.") - } - - /* Refuse registration */ - primaryServiceProxy.send( - loadAgent, - RegistrationFailedMessage(primaryServiceProxy.ref), - ) - - /* Expect a completion notification */ - scheduler.expectMsg(Completion(loadAgent.toTyped, Some(0))) - - /* ... as well as corresponding state and state data */ - loadAgent.stateName shouldBe Idle - loadAgent.stateData match { - case ParticipantModelBaseStateData( - startDate, - endDate, - _, - services, - outputConfig, - additionalActivationTicks, - foreseenDataTicks, - _, - voltageValueStore, - resultValueStore, - requestValueStore, - _, - _, - _, - ) => - /* Base state data */ - startDate shouldBe simulationStartDate - endDate shouldBe simulationEndDate - services shouldBe Iterable.empty - outputConfig shouldBe defaultOutputConfig - additionalActivationTicks shouldBe empty - foreseenDataTicks shouldBe Map.empty - voltageValueStore shouldBe ValueStore( - resolution, - SortedMap(0L -> Each(1.0)), - ) - resultValueStore shouldBe ValueStore(resolution) - requestValueStore shouldBe ValueStore[ComplexPower]( - resolution - ) - case _ => - fail( - s"Did not find expected state data $ParticipantModelBaseStateData, but ${loadAgent.stateData}" - ) - } - } - - "answer with zero power, if asked directly after initialisation" in { - val loadAgent = TestFSMRef( - new FixedLoadAgent( - scheduler = scheduler.ref, - initStateData = initStateData, - listener = systemListener, - ) - ) - - scheduler.send(loadAgent, Activation(INIT_SIM_TICK)) - - /* Refuse registration with primary service */ - primaryServiceProxy.expectMsgType[PrimaryServiceRegistrationMessage] - primaryServiceProxy.send( - loadAgent, - RegistrationFailedMessage(primaryServiceProxy.ref), - ) - - /* I'm not interested in the content of the Completion */ - scheduler.expectMsgType[Completion] - - loadAgent.stateName shouldBe Idle - /* State data has already been tested */ - - loadAgent ! RequestAssetPowerMessage( - 0L, - Each(1d), - Each(0d), - self.toTyped, - ) - expectMsg( - AssetPowerChangedMessage( - Megawatts(0d), - Megavars(0d), - ) - ) - - inside(loadAgent.stateData) { - case baseStateData: ParticipantModelBaseStateData[_, _, _, _] => - baseStateData.requestValueStore shouldBe ValueStore[ - ComplexPower - ]( - resolution, - SortedMap( - 0L -> ComplexPower( - Megawatts(0d), - Megavars(0d), - ) - ), - ) - case _ => - fail( - s"Did not find expected state data $ParticipantModelBaseStateData, but ${loadAgent.stateData}" - ) - } - } - - "do correct transitions when triggered in Idle" in { - val loadAgent = TestFSMRef( - new FixedLoadAgent( - scheduler = scheduler.ref, - initStateData = initStateData, - listener = systemListener, - ) - ) - - scheduler.send(loadAgent, Activation(INIT_SIM_TICK)) - - /* Refuse registration with primary service */ - primaryServiceProxy.expectMsgType[PrimaryServiceRegistrationMessage] - primaryServiceProxy.send( - loadAgent, - RegistrationFailedMessage(primaryServiceProxy.ref), - ) - - /* I am not interested in the Completion */ - scheduler.expectMsgType[Completion] - awaitAssert(loadAgent.stateName shouldBe Idle) - /* State data is tested in another test */ - - scheduler.send(loadAgent, Activation(0)) - - /* Expect confirmation */ - scheduler.expectMsg(Completion(loadAgent.toTyped, None)) - - /* Intermediate transitions and states cannot be tested, as the agents triggers itself - * too fast */ - - awaitAssert(loadAgent.stateName shouldBe Idle) - inside(loadAgent.stateData) { - case baseStateData: ParticipantModelBaseStateData[_, _, _, _] => - baseStateData.resultValueStore.last(0L) match { - case Some((tick, entry)) => - tick shouldBe 0L - inside(entry) { case ComplexPower(p, q) => - p should approximate(Megawatts(268.603e-6)) - q should approximate(Megavars(0.0)) - } - case None => - fail("Result value store does not contain entry for tick 0.") - } - case _ => - fail( - s"Did not find the expected state data $ParticipantModelBaseStateData, but ${loadAgent.stateData}" - ) - } - } - - "provide the correct average power after one data tick is available" in { - val loadAgent = TestFSMRef( - new FixedLoadAgent( - scheduler = scheduler.ref, - initStateData = initStateData, - listener = systemListener, - ) - ) - - /* Trigger the initialisation */ - scheduler.send(loadAgent, Activation(INIT_SIM_TICK)) - - /* Refuse registration with primary service */ - primaryServiceProxy.expectMsgType[PrimaryServiceRegistrationMessage] - primaryServiceProxy.send( - loadAgent, - RegistrationFailedMessage(primaryServiceProxy.ref), - ) - - scheduler.expectMsg(Completion(loadAgent.toTyped, Some(0))) - - /* Trigger the data generation in tick 0 */ - scheduler.send(loadAgent, Activation(0)) - - scheduler.expectMsg(Completion(loadAgent.toTyped)) - - awaitAssert(loadAgent.stateName shouldBe Idle) - - /* Ask the agent for average power in tick 3000 */ - loadAgent ! RequestAssetPowerMessage( - 3000L, - Each(1d), - Each(0d), - self.toTyped, - ) - - expectMsgType[AssetPowerChangedMessage] match { - case AssetPowerChangedMessage(p, q) => - p should approximate(Megawatts(268.603e-6)) - q should approximate(Megavars(0.0)) - } - } - - val loadAgent = TestFSMRef( - new FixedLoadAgent( - scheduler = scheduler.ref, - initStateData = initStateData, - listener = systemListener, - ) - ) - - "does answer unchanged power values after asking a second time with considerably same voltage" in { - - /* Trigger the initialisation */ - scheduler.send(loadAgent, Activation(INIT_SIM_TICK)) - - /* Refuse registration with primary service */ - primaryServiceProxy.expectMsgType[PrimaryServiceRegistrationMessage] - primaryServiceProxy.send( - loadAgent, - RegistrationFailedMessage(primaryServiceProxy.ref), - ) - - scheduler.expectMsg(Completion(loadAgent.toTyped, Some(0))) - - /* Trigger the data generation in tick 0 */ - scheduler.send(loadAgent, Activation(0)) - - scheduler.expectMsg(Completion(loadAgent.toTyped)) - - /* Ask the agent for average power in tick 3000 */ - loadAgent ! RequestAssetPowerMessage( - 3000L, - Each(1d), - Each(0d), - self.toTyped, - ) - - expectMsgType[AssetPowerChangedMessage] match { - case AssetPowerChangedMessage(p, q) => - p should approximate(Megawatts(268.603e-6)) - q should approximate(Megavars(0.0)) - case answer => fail(s"Did not expect to get that answer: $answer") - } - } - - "reply unchanged power values after asking a second time with considerably same voltage" in { - /* Previous request stems from previous test */ - /* Ask again with unchanged information */ - loadAgent ! RequestAssetPowerMessage( - 3000L, - Each(1.000000000000001d), - Each(0d), - self.toTyped, - ) - - /* Expect, that nothing has changed */ - expectMsgType[AssetPowerUnchangedMessage] match { - case AssetPowerUnchangedMessage(p, q) => - p should approximate(Megawatts(268.603e-6)) - q should approximate(Megavars(0.0)) - } - } - - "answer changed power values after asking a second time with different voltage" in { - /* Ask again with changed information */ - loadAgent ! RequestAssetPowerMessage( - 3000L, - Each(0.98), - Each(0d), - self.toTyped, - ) - - /* Expect, the correct values (this model has fixed power factor) */ - expectMsgType[AssetPowerChangedMessage] match { - case AssetPowerChangedMessage(p, q) => - p should approximate(Megawatts(268.603e-6)) - q should approximate(Megavars(-22.07138e-6)) - } - } - } -} diff --git a/src/test/scala/edu/ie3/simona/agent/participant/LoadAgentProfileModelCalculationSpec.scala b/src/test/scala/edu/ie3/simona/agent/participant/LoadAgentProfileModelCalculationSpec.scala deleted file mode 100644 index ec6d93f0fc..0000000000 --- a/src/test/scala/edu/ie3/simona/agent/participant/LoadAgentProfileModelCalculationSpec.scala +++ /dev/null @@ -1,418 +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.participant - -import com.typesafe.config.ConfigFactory -import edu.ie3.datamodel.models.input.system.LoadInput -import edu.ie3.datamodel.models.input.system.characteristic.QV -import edu.ie3.simona.agent.ValueStore -import edu.ie3.simona.agent.grid.GridAgentMessages.{ - AssetPowerChangedMessage, - AssetPowerUnchangedMessage, -} -import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ComplexPower -import edu.ie3.simona.agent.participant.load.LoadAgent.ProfileLoadAgent -import edu.ie3.simona.agent.participant.statedata.BaseStateData.ParticipantModelBaseStateData -import edu.ie3.simona.agent.participant.statedata.ParticipantStateData.{ - ParticipantInitializeStateData, - ParticipantInitializingStateData, - ParticipantUninitializedStateData, - SimpleInputContainer, -} -import edu.ie3.simona.agent.participant2.ParticipantAgent.{ - RegistrationFailedMessage, - RequestAssetPowerMessage, -} -import edu.ie3.simona.agent.state.AgentState.{Idle, Uninitialized} -import edu.ie3.simona.agent.state.ParticipantAgentState.HandleInformation -import edu.ie3.simona.config.SimonaConfig -import edu.ie3.simona.config.RuntimeConfig.LoadRuntimeConfig -import edu.ie3.simona.event.notifier.NotifierConfig -import edu.ie3.simona.model.participant.load.{LoadModelBehaviour, LoadReference} -import edu.ie3.simona.ontology.messages.Activation -import edu.ie3.simona.ontology.messages.SchedulerMessage.Completion -import edu.ie3.simona.ontology.messages.services.ServiceMessage.PrimaryServiceRegistrationMessage -import edu.ie3.simona.test.ParticipantAgentSpec -import edu.ie3.simona.test.common.model.participant.LoadTestData -import edu.ie3.simona.util.ConfigUtil -import edu.ie3.simona.util.SimonaConstants.INIT_SIM_TICK -import edu.ie3.util.scala.quantities.{Megavars, ReactivePower, Vars} -import org.apache.pekko.actor.ActorSystem -import org.apache.pekko.actor.typed.scaladsl.adapter.ClassicActorRefOps -import org.apache.pekko.testkit.TestFSMRef -import org.apache.pekko.util.Timeout -import org.scalatest.PrivateMethodTester -import squants.Each -import squants.energy.{Kilowatts, Megawatts, Watts} - -import java.util.concurrent.TimeUnit -import scala.collection.SortedMap - -class LoadAgentProfileModelCalculationSpec - extends ParticipantAgentSpec( - ActorSystem( - "LoadAgentSpec", - ConfigFactory - .parseString(""" - |pekko.loggers =["org.apache.pekko.event.slf4j.Slf4jLogger"] - |pekko.loglevel="DEBUG" - """.stripMargin), - ) - ) - with LoadTestData - with PrivateMethodTester { - implicit val receiveTimeOut: Timeout = Timeout(10, TimeUnit.SECONDS) - implicit val noReceiveTimeOut: Timeout = Timeout(1, TimeUnit.SECONDS) - - /* Alter the input model to have a voltage sensitive reactive power calculation */ - val voltageSensitiveInput: LoadInput = loadInput - .copy() - .qCharacteristics(new QV("qV:{(0.95,-0.625),(1.05,0.625)}")) - .build() - - private val simonaConfig: SimonaConfig = - createSimonaConfig( - LoadModelBehaviour.PROFILE, - LoadReference.ActivePower(Kilowatts(0d)), - ) - private val defaultOutputConfig = NotifierConfig( - simonaConfig.simona.output.participant.defaultConfig.simulationResult, - simonaConfig.simona.output.participant.defaultConfig.powerRequestReply, - simonaConfig.simona.output.participant.defaultConfig.flexResult, - ) - private val loadConfigUtil = ConfigUtil.ParticipantConfigUtil( - simonaConfig.simona.runtime.participant - ) - private val modelConfig = - loadConfigUtil.getOrDefault[LoadRuntimeConfig]( - voltageSensitiveInput.getUuid - ) - private val services = Iterable.empty - private val resolution = simonaConfig.simona.powerflow.resolution.toSeconds - - private implicit val powerTolerance: squants.Power = Watts(0.1) - private implicit val reactivePowerTolerance: ReactivePower = Vars(0.1) - - "A load agent with profile model calculation depending on no secondary data service" should { - val initStateData = ParticipantInitializeStateData[ - LoadInput, - LoadRuntimeConfig, - ComplexPower, - ]( - inputModel = voltageSensitiveInput, - modelConfig = modelConfig, - secondaryDataServices = services, - simulationStartDate = simulationStartDate, - simulationEndDate = simulationEndDate, - resolution = resolution, - requestVoltageDeviationThreshold = - simonaConfig.simona.runtime.participant.requestVoltageDeviationThreshold, - outputConfig = defaultOutputConfig, - primaryServiceProxy = primaryServiceProxy.ref, - ) - - "be instantiated correctly" in { - val loadAgent = TestFSMRef( - new ProfileLoadAgent( - scheduler = scheduler.ref, - initStateData = initStateData, - listener = systemListener, - ) - ) - - loadAgent.stateName shouldBe Uninitialized - // ParticipantUninitializedStateData is an empty class (due to typing). If it contains content one day - inside(loadAgent.stateData) { - case _: ParticipantUninitializedStateData[_] => succeed - case _ => - fail( - s"Expected $ParticipantUninitializedStateData, but got ${loadAgent.stateData}." - ) - } - } - - "end in correct state with correct state data after initialisation" in { - val loadAgent = TestFSMRef( - new ProfileLoadAgent( - scheduler = scheduler.ref, - initStateData = initStateData, - listener = systemListener, - ) - ) - - scheduler.send(loadAgent, Activation(INIT_SIM_TICK)) - - /* Actor should ask for registration with primary service */ - primaryServiceProxy.expectMsgType[PrimaryServiceRegistrationMessage] - - /* State should be information handling and having correct state data */ - loadAgent.stateName shouldBe HandleInformation - loadAgent.stateData match { - case ParticipantInitializingStateData( - inputModel, - modelConfig, - secondaryDataServices, - simulationStartDate, - simulationEndDate, - resolution, - requestVoltageDeviationThreshold, - outputConfig, - maybeEmAgent, - ) => - inputModel shouldBe SimpleInputContainer(voltageSensitiveInput) - modelConfig shouldBe modelConfig - secondaryDataServices shouldBe services - simulationStartDate shouldBe this.simulationStartDate - simulationEndDate shouldBe this.simulationEndDate - resolution shouldBe this.resolution - requestVoltageDeviationThreshold shouldBe simonaConfig.simona.runtime.participant.requestVoltageDeviationThreshold - outputConfig shouldBe defaultOutputConfig - maybeEmAgent shouldBe None - case unsuitableStateData => - fail(s"Agent has unsuitable state data '$unsuitableStateData'.") - } - - /* Refuse registration */ - primaryServiceProxy.send( - loadAgent, - RegistrationFailedMessage(primaryServiceProxy.ref), - ) - - /* Expect a completion notification */ - scheduler.expectMsg(Completion(loadAgent.toTyped, Some(0))) - - /* ... as well as corresponding state and state data */ - loadAgent.stateName shouldBe Idle - loadAgent.stateData match { - case ParticipantModelBaseStateData( - startDate, - endDate, - _, - services, - outputConfig, - additionalActivationTicks, - foreseenDataTicks, - _, - voltageValueStore, - resultValueStore, - requestValueStore, - _, - _, - _, - ) => - /* Base state data */ - startDate shouldBe simulationStartDate - endDate shouldBe simulationEndDate - services shouldBe Iterable.empty - outputConfig shouldBe defaultOutputConfig - additionalActivationTicks - .corresponds(Seq(900L, 1800L, 2700L, 3600L))(_ == _) shouldBe true - foreseenDataTicks shouldBe Map.empty - voltageValueStore shouldBe ValueStore( - resolution, - SortedMap(0L -> Each(1.0)), - ) - resultValueStore shouldBe ValueStore(resolution) - requestValueStore shouldBe ValueStore[ComplexPower]( - resolution - ) - case _ => - fail( - s"Did not find expected state data $ParticipantModelBaseStateData, but ${loadAgent.stateData}" - ) - } - } - - "answer with zero power, if asked directly after initialisation" in { - val loadAgent = TestFSMRef( - new ProfileLoadAgent( - scheduler = scheduler.ref, - initStateData = initStateData, - listener = systemListener, - ) - ) - - scheduler.send(loadAgent, Activation(INIT_SIM_TICK)) - - /* Refuse registration with primary service */ - primaryServiceProxy.expectMsgType[PrimaryServiceRegistrationMessage] - primaryServiceProxy.send( - loadAgent, - RegistrationFailedMessage(primaryServiceProxy.ref), - ) - - /* I'm not interested in the content of the Completion */ - scheduler.expectMsgType[Completion] - - loadAgent.stateName shouldBe Idle - /* State data has already been tested */ - - loadAgent ! RequestAssetPowerMessage( - 0L, - Each(1d), - Each(0d), - self.toTyped, - ) - expectMsg( - AssetPowerChangedMessage( - Megawatts(0d), - Megavars(0d), - ) - ) - - inside(loadAgent.stateData) { - case baseStateData: ParticipantModelBaseStateData[_, _, _, _] => - baseStateData.requestValueStore shouldBe ValueStore[ - ComplexPower - ]( - resolution, - SortedMap( - 0L -> ComplexPower( - Megawatts(0d), - Megavars(0d), - ) - ), - ) - case _ => - fail( - s"Did not find expected state data $ParticipantModelBaseStateData, but ${loadAgent.stateData}" - ) - } - } - - "do correct transitions when triggered in Idle" in { - val loadAgent = TestFSMRef( - new ProfileLoadAgent( - scheduler = scheduler.ref, - initStateData = initStateData, - listener = systemListener, - ) - ) - - scheduler.send(loadAgent, Activation(INIT_SIM_TICK)) - - /* Refuse registration with primary service */ - primaryServiceProxy.expectMsgType[PrimaryServiceRegistrationMessage] - primaryServiceProxy.send( - loadAgent, - RegistrationFailedMessage(primaryServiceProxy.ref), - ) - - /* I am not interested in the Completion */ - scheduler.expectMsgType[Completion] - awaitAssert(loadAgent.stateName shouldBe Idle) - /* State data is tested in another test */ - - scheduler.send(loadAgent, Activation(0)) - - /* Expect confirmation */ - scheduler.expectMsg(Completion(loadAgent.toTyped, Some(900))) - - /* Intermediate transitions and states cannot be tested, as the agents triggers itself - * too fast */ - - awaitAssert(loadAgent.stateName shouldBe Idle) - inside(loadAgent.stateData) { - case baseStateData: ParticipantModelBaseStateData[_, _, _, _] => - baseStateData.resultValueStore.last(0L) match { - case Some((tick, entry)) => - tick shouldBe 0L - inside(entry) { case ComplexPower(p, q) => - p should approximate(Megawatts(84.000938e-6)) - q should approximate(Megavars(0.0)) - } - case None => - fail("Result value store does not contain entry for tick 0.") - } - case _ => - fail( - s"Did not find the expected state data $ParticipantModelBaseStateData, but ${loadAgent.stateData}" - ) - } - } - - val loadAgent = TestFSMRef( - new ProfileLoadAgent( - scheduler = scheduler.ref, - initStateData = initStateData, - listener = systemListener, - ) - ) - - "provide the correct average power after three data ticks are available" in { - /* Trigger the initialisation */ - scheduler.send(loadAgent, Activation(INIT_SIM_TICK)) - - /* Refuse registration with primary service */ - primaryServiceProxy.expectMsgType[PrimaryServiceRegistrationMessage] - primaryServiceProxy.send( - loadAgent, - RegistrationFailedMessage(primaryServiceProxy.ref), - ) - - scheduler.expectMsg(Completion(loadAgent.toTyped, Some(0))) - - /* Trigger the data generation in tick 0, 900, 1800 */ - scheduler.send(loadAgent, Activation(0)) - scheduler.expectMsg(Completion(loadAgent.toTyped, Some(900))) - - scheduler.send(loadAgent, Activation(900)) - scheduler.expectMsg(Completion(loadAgent.toTyped, Some(1800))) - - awaitAssert(loadAgent.stateName shouldBe Idle) - - /* Ask the agent for average power in tick 1800 */ - loadAgent ! RequestAssetPowerMessage( - 1800L, - Each(1d), - Each(0d), - self.toTyped, - ) - - expectMsgType[AssetPowerChangedMessage] match { - case AssetPowerChangedMessage(p, q) => - p should approximate(Megawatts(79.750890e-6)) - q should approximate(Megavars(0.0)) - } - } - - "answer unchanged power values after asking a second time with considerably same voltage" in { - /* Previous request stems from previous test */ - /* Ask again with unchanged information */ - loadAgent ! RequestAssetPowerMessage( - 1800L, - Each(1.000000000000001d), - Each(0d), - self.toTyped, - ) - - /* Expect, that nothing has changed */ - expectMsgType[AssetPowerUnchangedMessage] match { - case AssetPowerUnchangedMessage(p, q) => - p should approximate(Megawatts(79.750890e-6)) - q should approximate(Megavars(0.0)) - } - } - - "answer changed power values after asking a second time with different voltage" in { - /* Ask again with changed information */ - loadAgent ! RequestAssetPowerMessage( - 1800L, - Each(0.98), - Each(0), - self.toTyped, - ) - - /* Expect, the correct values (this model has fixed power factor) */ - expectMsgType[AssetPowerChangedMessage] match { - case AssetPowerChangedMessage(p, q) => - p should approximate(Megawatts(79.750890e-6)) - q should approximate(Megavars(-22.0714e-6)) - } - } - } -} diff --git a/src/test/scala/edu/ie3/simona/agent/participant/ParticipantAgent2ListenerSpec.scala b/src/test/scala/edu/ie3/simona/agent/participant/ParticipantAgent2ListenerSpec.scala deleted file mode 100644 index 4e35c91514..0000000000 --- a/src/test/scala/edu/ie3/simona/agent/participant/ParticipantAgent2ListenerSpec.scala +++ /dev/null @@ -1,316 +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.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.GridAgentMessages.{ - AssetPowerChangedMessage, - AssetPowerUnchangedMessage, -} -import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ComplexPower -import edu.ie3.simona.agent.participant.statedata.ParticipantStateData.ParticipantInitializeStateData -import edu.ie3.simona.agent.participant2.ParticipantAgent.{ - GridSimulationFinished, - RegistrationFailedMessage, - RequestAssetPowerMessage, -} -import edu.ie3.simona.config.SimonaConfig -import edu.ie3.simona.config.RuntimeConfig.BaseRuntimeConfig -import edu.ie3.simona.event.ResultEvent.ParticipantResultEvent -import edu.ie3.simona.event.notifier.NotifierConfig -import edu.ie3.simona.model.participant.load.{LoadModelBehaviour, LoadReference} -import edu.ie3.simona.ontology.messages.Activation -import edu.ie3.simona.ontology.messages.SchedulerMessage.Completion -import edu.ie3.simona.ontology.messages.services.ServiceMessage.PrimaryServiceRegistrationMessage -import edu.ie3.simona.test.ParticipantAgentSpec -import edu.ie3.simona.test.common.DefaultTestData -import edu.ie3.simona.util.SimonaConstants.INIT_SIM_TICK -import edu.ie3.util.quantities.PowerSystemUnits.{MEGAVAR, MEGAWATT} -import org.apache.pekko.actor.typed.scaladsl.adapter.ClassicActorRefOps -import org.apache.pekko.actor.{ActorRef, ActorSystem} -import org.apache.pekko.testkit.TestFSMRef -import org.apache.pekko.util.Timeout -import org.mockito.Mockito.when -import org.scalatest.PrivateMethodTester -import org.scalatestplus.mockito.MockitoSugar -import squants.Each -import squants.energy.Kilowatts -import tech.units.indriya.quantity.Quantities - -import java.util.UUID -import java.util.concurrent.TimeUnit - -class ParticipantAgent2ListenerSpec - extends ParticipantAgentSpec( - ActorSystem( - "ParticipantAgent2ListenerSpec", - ConfigFactory - .parseString(""" - |pekko.loggers =["org.apache.pekko.event.slf4j.Slf4jLogger"] - |pekko.loglevel="OFF" - """.stripMargin), - ) - ) - with DefaultTestData - with PrivateMethodTester - with MockitoSugar { - - implicit val receiveTimeOut: Timeout = Timeout(10, TimeUnit.SECONDS) - implicit val noReceiveTimeOut: Timeout = Timeout(1, TimeUnit.SECONDS) - - /* Assign this test to receive the result events from agent */ - override val systemListener: Iterable[ActorRef] = Iterable(self) - - private val testUUID = UUID.randomUUID - private val testID = "PartAgentExternalMock" - - private implicit val quantityTolerance: Double = 1e-6 // Equals to 1 W power - private val simonaConfig: SimonaConfig = - createSimonaConfig( - LoadModelBehaviour.FIX, - LoadReference.ActivePower(Kilowatts(0d)), - ) - - private val mockInputModel = mock[SystemParticipantInput] - when(mockInputModel.getUuid).thenReturn(testUUID) - when(mockInputModel.getId).thenReturn(testID) - - private val services = Iterable.empty - - "A participant agent" should { - val initStateData: NotifierConfig => ParticipantInitializeStateData[ - SystemParticipantInput, - BaseRuntimeConfig, - ComplexPower, - ] = outputConfig => - ParticipantInitializeStateData[ - SystemParticipantInput, - BaseRuntimeConfig, - ComplexPower, - ]( - inputModel = mockInputModel, - modelConfig = mock[BaseRuntimeConfig], - secondaryDataServices = services, - simulationStartDate = defaultSimulationStart, - simulationEndDate = defaultSimulationEnd, - resolution = simonaConfig.simona.powerflow.resolution.toSeconds, - requestVoltageDeviationThreshold = - simonaConfig.simona.runtime.participant.requestVoltageDeviationThreshold, - outputConfig = outputConfig, - primaryServiceProxy = primaryServiceProxy.ref, - ) - - "inform listeners about new simulation results, when asked to do" in { - /* Let the agent send announcements, when there is anew request reply */ - val outputConfig = NotifierConfig( - simulationResultInfo = true, - powerRequestReply = false, - flexResult = false, - ) - - val mockAgent = TestFSMRef( - new ParticipantAgentMock( - scheduler = scheduler.ref, - initStateData = initStateData(outputConfig), - listener = systemListener, - ) - ) - - /* Trigger the initialisation */ - scheduler.send(mockAgent, Activation(INIT_SIM_TICK)) - - /* Refuse registration with primary service */ - primaryServiceProxy.expectMsgType[PrimaryServiceRegistrationMessage] - primaryServiceProxy.send( - mockAgent, - RegistrationFailedMessage(primaryServiceProxy.ref), - ) - - scheduler.expectMsg(Completion(mockAgent.toTyped)) - - /* Trigger the data generation in tick 0 */ - scheduler.send(mockAgent, Activation(0)) - - /* Receive the completion message for the calculation */ - scheduler.expectMsgType[Completion] - logger.debug("Agent completed model calculation.") - - /* Receive the listener announcement */ - expectMsgType[ParticipantResultEvent] match { - case ParticipantResultEvent( - systemParticipantResult: SystemParticipantResult - ) => - systemParticipantResult.getP should equalWithTolerance( - Quantities.getQuantity(2, MEGAWATT), - quantityTolerance, - ) - systemParticipantResult.getQ should equalWithTolerance( - Quantities.getQuantity(1, MEGAVAR), - quantityTolerance, - ) - case _ => fail("Expected a SystemParticipantResult") - } - } - - "not inform listeners about new simulation results, when not asked to do" in { - /* Let the agent send announcements, when there is anew request reply */ - val outputConfig = NotifierConfig( - simulationResultInfo = false, - powerRequestReply = false, - flexResult = false, - ) - - val mockAgent = TestFSMRef( - new ParticipantAgentMock( - scheduler = scheduler.ref, - initStateData = initStateData(outputConfig), - listener = systemListener, - ) - ) - - /* Trigger the initialisation */ - scheduler.send(mockAgent, Activation(INIT_SIM_TICK)) - - /* Refuse registration with primary service */ - primaryServiceProxy.expectMsgType[PrimaryServiceRegistrationMessage] - primaryServiceProxy.send( - mockAgent, - RegistrationFailedMessage(primaryServiceProxy.ref), - ) - - scheduler.expectMsg(Completion(mockAgent.toTyped)) - - /* Trigger the data generation in tick 0 */ - scheduler.send(mockAgent, Activation(0)) - - /* Receive the completion message for the calculation and no listener announcement */ - scheduler.expectMsgType[Completion] - expectNoMessage(noReceiveTimeOut.duration) - } - - "not inform listeners about request reply, when asked to do (currently not implemented)" in { - /* Let the agent send announcements, when there is anew request reply */ - val outputConfig = NotifierConfig( - simulationResultInfo = false, - powerRequestReply = true, - flexResult = false, - ) - - val mockAgent = TestFSMRef( - new ParticipantAgentMock( - scheduler = scheduler.ref, - initStateData = initStateData(outputConfig), - listener = systemListener, - ) - ) - - /* Trigger the initialisation */ - scheduler.send(mockAgent, Activation(INIT_SIM_TICK)) - - /* Refuse registration with primary service */ - primaryServiceProxy.expectMsgType[PrimaryServiceRegistrationMessage] - primaryServiceProxy.send( - mockAgent, - RegistrationFailedMessage(primaryServiceProxy.ref), - ) - - scheduler.expectMsg(Completion(mockAgent.toTyped)) - - /* Trigger the data generation in tick 0 */ - scheduler.send(mockAgent, Activation(0)) - - scheduler.expectMsg(Completion(mockAgent.toTyped)) - - /* Ask the agent for average power in tick 3000 */ - mockAgent ! RequestAssetPowerMessage( - 3000L, - Each(1d), - Each(0d), - self.toTyped, - ) - - /* Wait for original reply (this is the querying agent) */ - receiveOne(receiveTimeOut.duration) match { - case AssetPowerChangedMessage(p, q) => - logger.debug(s"Agent answered with changed power ($p, $q)") - case AssetPowerUnchangedMessage(p, q) => - logger.debug(s"Agent answered with unchanged power ($p, $q)") - case unknownMsg => fail(s"Received unexpected message: $unknownMsg") - } - - scheduler.send(mockAgent, GridSimulationFinished(3000L, 6000L)) - - /* Wait for the result event (this is the event listener) */ - logger.warn( - "Writing out power request replies is currently not implemented. Reimplement this test, as soon as" + - "the function is available!" - ) - expectNoMessage(noReceiveTimeOut.duration) - - scheduler.expectNoMessage() - } - - "not inform listeners about request reply, when not asked to do" in { - /* Let the agent send announcements, when there is anew request reply */ - val outputConfig = NotifierConfig( - simulationResultInfo = false, - powerRequestReply = false, - flexResult = false, - ) - - val mockAgent = TestFSMRef( - new ParticipantAgentMock( - scheduler = scheduler.ref, - initStateData = initStateData(outputConfig), - listener = systemListener, - ) - ) - - /* Trigger the initialisation */ - scheduler.send(mockAgent, Activation(INIT_SIM_TICK)) - - /* Refuse registration with primary service */ - primaryServiceProxy.expectMsgType[PrimaryServiceRegistrationMessage] - primaryServiceProxy.send( - mockAgent, - RegistrationFailedMessage(primaryServiceProxy.ref), - ) - - scheduler.expectMsg(Completion(mockAgent.toTyped)) - - /* Trigger the data generation in tick 0 */ - scheduler.send(mockAgent, Activation(0)) - - /* Appreciate the existence of two Completion */ - scheduler.expectMsg(Completion(mockAgent.toTyped)) - - /* Ask the agent for average power in tick 3000 */ - mockAgent ! RequestAssetPowerMessage( - 3000L, - Each(1d), - Each(0d), - self.toTyped, - ) - - /* Wait for original reply (this is the querying agent) */ - receiveOne(receiveTimeOut.duration) match { - case AssetPowerChangedMessage(p, q) => - logger.debug(s"Agent answered with changed power ($p, $q)") - case AssetPowerUnchangedMessage(p, q) => - logger.debug(s"Agent answered with unchanged power ($p, $q)") - case unknownMsg => fail(s"Received unexpected message: $unknownMsg") - } - - scheduler.send(mockAgent, GridSimulationFinished(3000L, 6000L)) - - /* Make sure nothing else is sent */ - expectNoMessage(noReceiveTimeOut.duration) - } - } -} diff --git a/src/test/scala/edu/ie3/simona/agent/participant/ParticipantAgentExternalSourceSpec.scala b/src/test/scala/edu/ie3/simona/agent/participant/ParticipantAgentExternalSourceSpec.scala deleted file mode 100644 index bb720bf7a6..0000000000 --- a/src/test/scala/edu/ie3/simona/agent/participant/ParticipantAgentExternalSourceSpec.scala +++ /dev/null @@ -1,859 +0,0 @@ -/* - * © 2021. TU Dortmund University, - * Institute of Energy Systems, Energy Efficiency and Energy Economics, - * Research group Distribution grid planning and operation - */ - -package edu.ie3.simona.agent.participant - -import org.apache.pekko.actor.typed.scaladsl.adapter.ClassicActorRefOps -import org.apache.pekko.actor.{ActorRef, ActorSystem} -import org.apache.pekko.testkit.TestFSMRef -import org.apache.pekko.util.Timeout -import breeze.numerics.{acos, tan} -import com.typesafe.config.ConfigFactory -import edu.ie3.datamodel.models.input.NodeInput -import edu.ie3.datamodel.models.input.system.SystemParticipantInput -import edu.ie3.simona.agent.ValueStore -import edu.ie3.simona.agent.grid.GridAgentMessages.{ - AssetPowerChangedMessage, - AssetPowerUnchangedMessage, -} -import edu.ie3.simona.agent.participant.data.Data.PrimaryData.{ - ActivePower, - ActivePowerAndHeat, - ActivePowerExtra, - ComplexPower, - ComplexPowerAndHeat, - ComplexPowerExtra, -} -import edu.ie3.simona.agent.participant.statedata.BaseStateData.FromOutsideBaseStateData -import edu.ie3.simona.agent.participant.statedata.DataCollectionStateData -import edu.ie3.simona.agent.participant.statedata.ParticipantStateData.{ - ParticipantInitializeStateData, - ParticipantInitializingStateData, - ParticipantUninitializedStateData, - SimpleInputContainer, -} -import edu.ie3.simona.agent.participant2.ParticipantAgent.{ - DataProvision, - PrimaryRegistrationSuccessfulMessage, - RequestAssetPowerMessage, -} -import edu.ie3.simona.agent.state.AgentState.{Idle, Uninitialized} -import edu.ie3.simona.agent.state.ParticipantAgentState.HandleInformation -import edu.ie3.simona.config.SimonaConfig -import edu.ie3.simona.config.RuntimeConfig.BaseRuntimeConfig -import edu.ie3.simona.event.notifier.NotifierConfig -import edu.ie3.simona.model.participant.CalcRelevantData.FixedRelevantData -import edu.ie3.simona.model.participant.ModelState.ConstantState -import edu.ie3.simona.model.participant.load.{LoadModelBehaviour, LoadReference} -import edu.ie3.simona.model.participant.{CalcRelevantData, SystemParticipant} -import edu.ie3.simona.ontology.messages.Activation -import edu.ie3.simona.ontology.messages.SchedulerMessage.Completion -import edu.ie3.simona.ontology.messages.services.ServiceMessage.PrimaryServiceRegistrationMessage -import edu.ie3.simona.test.ParticipantAgentSpec -import edu.ie3.simona.test.common.DefaultTestData -import edu.ie3.simona.util.SimonaConstants.INIT_SIM_TICK -import edu.ie3.util.quantities.PowerSystemUnits -import edu.ie3.util.scala.quantities.{Kilovars, Megavars, ReactivePower, Vars} -import org.mockito.ArgumentMatchers.any -import org.mockito.Mockito.when -import org.scalatestplus.mockito.MockitoSugar -import squants.{Each, Power} -import squants.energy.{Kilowatts, Megawatts, Watts} -import tech.units.indriya.quantity.Quantities - -import java.util.UUID -import java.util.concurrent.TimeUnit -import scala.collection.{SortedMap, SortedSet} -import scala.util.{Failure, Success} - -/** Tests a mock participant agent with external data (primary data). Since - * primary data is exclusively handled on the level of ParticipantAgent, it - * only needs to be tested once. - */ -class ParticipantAgentExternalSourceSpec - extends ParticipantAgentSpec( - ActorSystem( - "ParticipantAgentExternalSourceSpec", - ConfigFactory - .parseString(""" - |pekko.loggers =["org.apache.pekko.event.slf4j.Slf4jLogger"] - |pekko.loglevel="DEBUG" - """.stripMargin), - ) - ) - with DefaultTestData - with MockitoSugar { - implicit val receiveTimeOut: Timeout = Timeout(10, TimeUnit.SECONDS) - implicit val noReceiveTimeOut: Timeout = Timeout(1, TimeUnit.SECONDS) - - private val testUUID = UUID.randomUUID - private val testID = "PartAgentExternalMock" - private val mockNode = mock[NodeInput] - when(mockNode.getvTarget()) - .thenReturn(Quantities.getQuantity(1d, PowerSystemUnits.PU)) - private val mockInputModel = mock[SystemParticipantInput] - when(mockInputModel.getUuid).thenReturn(testUUID) - when(mockInputModel.getId).thenReturn(testID) - when(mockInputModel.getNode).thenReturn(mockNode) - private val mockModel = - mock[SystemParticipant[ - CalcRelevantData.FixedRelevantData.type, - ComplexPower, - ConstantState.type, - ]] - when(mockModel.getUuid).thenReturn(testUUID) - private val activeToReactivePowerFunction: Power => ReactivePower = - (p: Power) => Kilovars(p.toKilowatts * tan(acos(0.9))) - when( - mockModel.activeToReactivePowerFunc( - any(classOf[squants.Dimensionless]) - ) - ).thenReturn(activeToReactivePowerFunction) - - private val simonaConfig: SimonaConfig = createSimonaConfig( - LoadModelBehaviour.FIX, - LoadReference.ActivePower(Kilowatts(0.0)), - ) - private val defaultOutputConfig = NotifierConfig( - simulationResultInfo = false, - powerRequestReply = false, - flexResult = false, - ) - - private val resolution = simonaConfig.simona.powerflow.resolution.toSeconds - - private implicit val powerTolerance: Power = Watts(0.1) - private implicit val reactivePowerTolerance: ReactivePower = Vars(0.1) - - "A participant agent with externally given data provider" should { - val initStateData = ParticipantInitializeStateData[ - SystemParticipantInput, - BaseRuntimeConfig, - ComplexPower, - ]( - inputModel = mockInputModel, - modelConfig = mock[BaseRuntimeConfig], - secondaryDataServices = Iterable.empty, - simulationStartDate = defaultSimulationStart, - simulationEndDate = defaultSimulationEnd, - resolution = simonaConfig.simona.powerflow.resolution.toSeconds, - requestVoltageDeviationThreshold = - simonaConfig.simona.runtime.participant.requestVoltageDeviationThreshold, - outputConfig = defaultOutputConfig, - primaryServiceProxy = primaryServiceProxy.ref, - ) - - "be instantiated correctly" in { - val mockAgent = TestFSMRef( - new ParticipantAgentMock( - scheduler = scheduler.ref, - initStateData = initStateData, - ) - ) - - mockAgent.stateName shouldBe Uninitialized - // ParticipantUninitializedStateData is an empty class (due to typing). If it contains content one day - inside(mockAgent.stateData) { - case _: ParticipantUninitializedStateData[_] => succeed - case _ => - fail( - s"Expected $ParticipantUninitializedStateData, but got ${mockAgent.stateData}." - ) - } - } - - "end in correct state with correct state data after initialisation" in { - val mockAgent = TestFSMRef( - new ParticipantAgentMock( - scheduler = scheduler.ref, - initStateData = initStateData, - ) - ) - - scheduler.send(mockAgent, Activation(INIT_SIM_TICK)) - - /* Expect a registration message */ - primaryServiceProxy.expectMsg( - PrimaryServiceRegistrationMessage(mockAgent.ref, testUUID) - ) - - /* ... as well as corresponding state and state data */ - mockAgent.stateName shouldBe HandleInformation - mockAgent.stateData match { - case ParticipantInitializingStateData( - inputModel, - modelConfig, - secondaryDataServices, - simulationStartDate, - simulationEndDate, - resolution, - requestVoltageDeviationThreshold, - outputConfig, - maybeEmAgent, - ) => - inputModel shouldBe SimpleInputContainer(mockInputModel) - modelConfig shouldBe modelConfig - secondaryDataServices shouldBe Iterable.empty - simulationStartDate shouldBe defaultSimulationStart - simulationEndDate shouldBe defaultSimulationEnd - resolution shouldBe this.resolution - requestVoltageDeviationThreshold shouldBe simonaConfig.simona.runtime.participant.requestVoltageDeviationThreshold - outputConfig shouldBe defaultOutputConfig - maybeEmAgent shouldBe None - case unsuitableStateData => - fail(s"Agent has unsuitable state data '$unsuitableStateData'.") - } - - /* Reply, that registration was successful */ - primaryServiceProxy.send( - mockAgent, - PrimaryRegistrationSuccessfulMessage( - primaryServiceProxy.ref, - 4711L, - ActivePowerExtra, - ), - ) - - scheduler.expectMsg(Completion(mockAgent.toTyped, Some(4711L))) - - /* ... as well as corresponding state and state data */ - mockAgent.stateName shouldBe Idle - mockAgent.stateData match { - case baseStateData: FromOutsideBaseStateData[SystemParticipant[ - FixedRelevantData.type, - ComplexPower, - ConstantState.type, - ], ComplexPower] => - /* Only check the awaited next data ticks, as the rest has yet been checked */ - baseStateData.foreseenDataTicks shouldBe Map( - primaryServiceProxy.ref -> Some(4711L) - ) - case _ => - fail( - s"Did not find expected state data $FromOutsideBaseStateData, but ${mockAgent.stateData}" - ) - } - } - - "answer with zero power, if asked directly after initialisation" in { - val mockAgent = TestFSMRef( - new ParticipantAgentMock( - scheduler = scheduler.ref, - initStateData = initStateData, - ) - ) - - scheduler.send(mockAgent, Activation(INIT_SIM_TICK)) - - /* I'm not interested in the content of the RegistrationMessage */ - primaryServiceProxy.expectMsgType[PrimaryServiceRegistrationMessage] - primaryServiceProxy.send( - mockAgent, - PrimaryRegistrationSuccessfulMessage( - primaryServiceProxy.ref, - 900L, - ActivePowerExtra, - ), - ) - - /* I'm not interested in the content of the Completion */ - scheduler.expectMsgType[Completion] - awaitAssert(mockAgent.stateName shouldBe Idle) - - /* State data has already been tested */ - - mockAgent ! RequestAssetPowerMessage( - 0L, - Each(1.0), - Each(0.0), - self.toTyped, - ) - expectMsg( - AssetPowerChangedMessage( - Megawatts(0.0), - Megavars(0.0), - ) - ) - - inside(mockAgent.stateData) { - case FromOutsideBaseStateData( - _, - _, - _, - _, - _, - _, - _, - _, - _, - _, - requestValueStore, - ) => - requestValueStore shouldBe ValueStore[ComplexPower]( - resolution, - SortedMap( - 0L -> ComplexPower( - Megawatts(0.0), - Megavars(0.0), - ) - ), - ) - case _ => - fail( - s"Did not found the expected state data $FromOutsideBaseStateData, but ${mockAgent.stateData}" - ) - } - } - - "do correct transitions faced to new data in Idle" in { - val mockAgent = TestFSMRef( - new ParticipantAgentMock( - scheduler = scheduler.ref, - initStateData = initStateData, - ) - ) - - scheduler.send(mockAgent, Activation(INIT_SIM_TICK)) - - /* I'm not interested in the content of the RegistrationMessage */ - primaryServiceProxy.expectMsgType[PrimaryServiceRegistrationMessage] - primaryServiceProxy.send( - mockAgent, - PrimaryRegistrationSuccessfulMessage( - primaryServiceProxy.ref, - 900L, - ComplexPowerExtra, - ), - ) - - /* I'm not interested in the content of the Completion */ - scheduler.expectMsgType[Completion] - awaitAssert(mockAgent.stateName shouldBe Idle) - - /* Send out new data */ - primaryServiceProxy.send( - mockAgent, - DataProvision( - 900L, - primaryServiceProxy.ref, - ComplexPower( - Kilowatts(0.0), - Kilovars(900.0), - ), - Some(1800L), - ), - ) - - /* Find yourself in corresponding state and state data */ - mockAgent.stateName shouldBe HandleInformation - mockAgent.stateData match { - case DataCollectionStateData( - baseStateData: FromOutsideBaseStateData[SystemParticipant[ - CalcRelevantData, - ComplexPower, - ConstantState.type, - ], ComplexPower], - expectedSenders, - isYetTriggered, - ) => - /* The next data tick is already registered */ - baseStateData.foreseenDataTicks shouldBe Map( - primaryServiceProxy.ref -> Some(1800L) - ) - - /* The yet sent data is also registered */ - expectedSenders shouldBe Map( - primaryServiceProxy.ref -> Some( - ComplexPower( - Kilowatts(0.0), - Kilovars(900.0), - ) - ) - ) - - /* It is not yet triggered */ - isYetTriggered shouldBe false - case _ => - fail( - s"Did not find expected state data $DataCollectionStateData, but ${mockAgent.stateData}" - ) - } - - /* Trigger the agent */ - scheduler.send(mockAgent, Activation(900)) - - /* Expect confirmation */ - scheduler.expectMsg(Completion(mockAgent.toTyped, Some(1800))) - - /* Expect the state change to idle with updated base state data */ - mockAgent.stateName shouldBe Idle - mockAgent.stateData match { - case baseStateData: FromOutsideBaseStateData[SystemParticipant[ - CalcRelevantData, - ComplexPower, - ConstantState.type, - ], ComplexPower] => - /* The new data is apparent in the result value store */ - baseStateData.resultValueStore match { - case ValueStore(_, store) => - store shouldBe Map( - 900L -> ComplexPower( - Kilowatts(0.0), - Kilovars(900.0), - ) - ) - } - case _ => - fail( - s"Did not found the expected state data $FromOutsideBaseStateData, but ${mockAgent.stateData}" - ) - } - } - - "do correct transitions triggered for activation in idle" in { - val mockAgent = TestFSMRef( - new ParticipantAgentMock( - scheduler = scheduler.ref, - initStateData = initStateData, - ) - ) - - scheduler.send(mockAgent, Activation(INIT_SIM_TICK)) - - /* I'm not interested in the content of the RegistrationMessage */ - primaryServiceProxy.expectMsgType[PrimaryServiceRegistrationMessage] - primaryServiceProxy.send( - mockAgent, - PrimaryRegistrationSuccessfulMessage( - primaryServiceProxy.ref, - 900L, - ComplexPowerExtra, - ), - ) - - /* I'm not interested in the content of the Completion */ - scheduler.expectMsgType[Completion] - awaitAssert(mockAgent.stateName shouldBe Idle) - - /* Send out an activity start trigger */ - scheduler.send(mockAgent, Activation(900)) - - /* Find yourself in appropriate state with state data */ - mockAgent.stateName shouldBe HandleInformation - mockAgent.stateData match { - case DataCollectionStateData( - baseStateData: FromOutsideBaseStateData[SystemParticipant[ - CalcRelevantData, - ComplexPower, - ConstantState.type, - ], ComplexPower], - expectedSenders, - isYetTriggered, - ) => - /* The next data tick is already registered */ - baseStateData.foreseenDataTicks shouldBe Map( - primaryServiceProxy.ref -> Some(900L) - ) - - /* The yet sent data is also registered */ - expectedSenders shouldBe Map(primaryServiceProxy.ref -> None) - - /* It is yet triggered */ - isYetTriggered shouldBe true - case _ => - fail( - s"Did not find expected state data $DataCollectionStateData, but ${mockAgent.stateData}" - ) - } - - /* Providing the awaited data will lead to the foreseen transitions */ - primaryServiceProxy.send( - mockAgent, - DataProvision( - 900L, - primaryServiceProxy.ref, - ComplexPower( - Kilowatts(0.0), - Kilovars(900.0), - ), - Some(1800L), - ), - ) - - /* Expect confirmation */ - scheduler.expectMsg(Completion(mockAgent.toTyped, Some(1800))) - - /* Expect the state change to idle with updated base state data */ - mockAgent.stateName shouldBe Idle - mockAgent.stateData match { - case baseStateData: FromOutsideBaseStateData[SystemParticipant[ - CalcRelevantData, - ComplexPower, - ConstantState.type, - ], ComplexPower] => - /* The new data is apparent in the result value store */ - baseStateData.resultValueStore match { - case ValueStore(_, store) => - store shouldBe Map( - 900L -> ComplexPower( - Kilowatts(0.0), - Kilovars(900.0), - ) - ) - } - case _ => - fail( - s"Did not found the expected state data $FromOutsideBaseStateData, but ${mockAgent.stateData}" - ) - } - } - - "does not provide power if data is awaited in an earlier tick, but answers it, if all expected data is there" in { - val mockAgent = TestFSMRef( - new ParticipantAgentMock( - scheduler = scheduler.ref, - initStateData = initStateData, - ) - ) - - /* Trigger the initialisation */ - scheduler.send(mockAgent, Activation(INIT_SIM_TICK)) - - /* I'm not interested in the content of the RegistrationMessage */ - primaryServiceProxy.expectMsgType[PrimaryServiceRegistrationMessage] - primaryServiceProxy.send( - mockAgent, - PrimaryRegistrationSuccessfulMessage( - primaryServiceProxy.ref, - 900L, - ComplexPowerExtra, - ), - ) - - /* I'm not interested in the content of the Completion */ - scheduler.expectMsgType[Completion] - awaitAssert(mockAgent.stateName shouldBe Idle) - - /* Ask the agent for average power in tick 1800 */ - mockAgent ! RequestAssetPowerMessage( - 1800L, - Each(1.0), - Each(0.0), - self.toTyped, - ) - expectNoMessage(noReceiveTimeOut.duration) - awaitAssert(mockAgent.stateName == Idle) - - /* Send out the expected data and wait for the reply */ - primaryServiceProxy.send( - mockAgent, - DataProvision( - 900L, - primaryServiceProxy.ref, - ComplexPower( - Kilowatts(0.0), - Kilovars(900.0), - ), - Some(1800L), - ), - ) - - /* Trigger the agent */ - scheduler.send(mockAgent, Activation(900)) - - scheduler.expectMsg(Completion(mockAgent.toTyped, Some(1800))) - - /* Appreciate the answer to my previous request */ - expectMsgType[AssetPowerChangedMessage] - } - - val mockAgent = TestFSMRef( - new ParticipantAgentMock( - scheduler = scheduler.ref, - initStateData = initStateData, - ) - ) - - "correctly determine the reactive power function when trivial reactive power is requested" in { - val baseStateData: FromOutsideBaseStateData[SystemParticipant[ - CalcRelevantData.FixedRelevantData.type, - ComplexPower, - ConstantState.type, - ], ComplexPower] = FromOutsideBaseStateData[SystemParticipant[ - CalcRelevantData.FixedRelevantData.type, - ComplexPower, - ConstantState.type, - ], ComplexPower]( - mockModel, - defaultSimulationStart, - defaultSimulationEnd, - defaultOutputConfig, - SortedSet.empty, - Map.empty[ActorRef, Option[Long]], - fillUpReactivePowerWithModelFunc = false, - 1e-4, - ValueStore.forVoltage( - 900L, - Each(1.0), - ), - ValueStore.forResult(900L, 1L), - ValueStore(900L), - ) - - val actualFunction = - mockAgent.underlyingActor.getReactivePowerFunction(0L, baseStateData) - actualFunction(Kilowatts(100.0)) should approximate(Kilovars(0.0)) - } - - "correctly determine the reactive power function from model when requested" in { - val baseStateData: FromOutsideBaseStateData[SystemParticipant[ - CalcRelevantData.FixedRelevantData.type, - ComplexPower, - ConstantState.type, - ], ComplexPower] = FromOutsideBaseStateData[SystemParticipant[ - CalcRelevantData.FixedRelevantData.type, - ComplexPower, - ConstantState.type, - ], ComplexPower]( - mockModel, - defaultSimulationStart, - defaultSimulationEnd, - defaultOutputConfig, - SortedSet.empty, - Map.empty[ActorRef, Option[Long]], - fillUpReactivePowerWithModelFunc = true, - 1e-4, - ValueStore.forVoltage( - 900L, - Each(1.0), - ), - ValueStore.forResult(900L, 1L), - ValueStore(900L), - ) - - val actualFunction = - mockAgent.underlyingActor.getReactivePowerFunction(0L, baseStateData) - actualFunction(Kilowatts(100.0)) should approximate(Kilovars(48.43221)) - } - - "provide correct average power after three data ticks are available" in { - /* Trigger the initialisation */ - scheduler.send(mockAgent, Activation(INIT_SIM_TICK)) - - /* I'm not interested in the content of the RegistrationMessage */ - primaryServiceProxy.expectMsgType[PrimaryServiceRegistrationMessage] - primaryServiceProxy.send( - mockAgent, - PrimaryRegistrationSuccessfulMessage( - primaryServiceProxy.ref, - 900L, - ComplexPowerExtra, - ), - ) - - /* I'm not interested in the content of the Completion */ - scheduler.expectMsgType[Completion] - awaitAssert(mockAgent.stateName shouldBe Idle) - - /* Send out the three data points */ - /* ... for tick 900 */ - primaryServiceProxy.send( - mockAgent, - DataProvision( - 900L, - primaryServiceProxy.ref, - ComplexPower( - Kilowatts(100.0), - Kilovars(33.0), - ), - Some(1800L), - ), - ) - scheduler.send(mockAgent, Activation(900)) - scheduler.expectMsg(Completion(mockAgent.toTyped, Some(1800))) - - /* ... for tick 1800 */ - primaryServiceProxy.send( - mockAgent, - DataProvision( - 1800L, - primaryServiceProxy.ref, - ComplexPower( - Kilowatts(150.0), - Kilovars(49.0), - ), - Some(2700L), - ), - ) - scheduler.send(mockAgent, Activation(1800)) - scheduler.expectMsg(Completion(mockAgent.toTyped, Some(2700))) - - /* ... for tick 2700 */ - primaryServiceProxy.send( - mockAgent, - DataProvision( - 2700L, - primaryServiceProxy.ref, - ComplexPower( - Kilowatts(200.0), - Kilovars(66.0), - ), - None, - ), - ) - scheduler.send(mockAgent, Activation(2700)) - scheduler.expectMsg(Completion(mockAgent.toTyped)) - - awaitAssert(mockAgent.stateName == Idle) - - /* Ask the agent for average power in tick 3000 */ - mockAgent ! RequestAssetPowerMessage( - 3000L, - Each(1.0), - Each(0.0), - self.toTyped, - ) - - expectMsgType[AssetPowerChangedMessage] match { - case AssetPowerChangedMessage(p, q) => - p should approximate(Megawatts(0.095)) - q should approximate(Megavars(0.0312)) - } - } - - "replies unchanged power values after asking a second time" in { - /* Previous request stems from previous test */ - /* Ask again with unchanged information */ - mockAgent ! RequestAssetPowerMessage( - 3000L, - Each(1.000000000000001d), - Each(0.0), - self.toTyped, - ) - - /* Expect, that nothing has changed */ - expectMsgType[AssetPowerUnchangedMessage] match { - case AssetPowerUnchangedMessage(p, q) => - p should approximate(Megawatts(0.095)) - q should approximate(Megavars(0.0312)) - } - } - - "replies unchanged power values after asking a second time with altered voltage" in { - /* Previous request stems from previous test */ - /* Ask again with unchanged information */ - mockAgent ! RequestAssetPowerMessage( - 3000L, - Each(0.98d), - Each(0.0), - self.toTyped, - ) - - /* Expect, that nothing has changed, as this model is meant to forward information from outside */ - expectMsgType[AssetPowerUnchangedMessage] match { - case AssetPowerUnchangedMessage(p, q) => - p should approximate(Megawatts(0.095)) - q should approximate(Megavars(0.0312)) - } - } - - "preparing incoming primary data" when { - val participantAgent = - TestFSMRef( - new ParticipantAgentMock(scheduler.ref, initStateData = initStateData) - ).underlyingActor - val reactivePowerFunction = (_: squants.Power) => Kilovars(0.0) - - "sending unsupported data" should { - "fail" in { - val data = Map( - primaryServiceProxy.ref -> Some( - ComplexPowerAndHeat( - Kilowatts(0.0), - Kilovars(0.0), - Kilowatts(0.0), - ) - ) - ) - - participantAgent.prepareData(data, reactivePowerFunction) match { - case Failure(exception: IllegalStateException) => - exception.getMessage shouldBe "Got the wrong primary data. Expected: edu.ie3.simona.agent.participant.data.Data$PrimaryData$ComplexPower, got: edu.ie3.simona.agent.participant.data.Data$PrimaryData$ComplexPowerAndHeat" - case Failure(exception) => - fail(s"Failed with wrong exception:\n\t$exception") - case Success(_) => fail("Was meant to fail, but succeeded") - } - } - } - - "sending enhanceable data" should { - "fail, if enhanced data are not supported" in { - val data = Map( - primaryServiceProxy.ref -> Some( - ActivePowerAndHeat( - Kilowatts(0.0), - Kilowatts(0.0), - ) - ) - ) - - participantAgent.prepareData(data, reactivePowerFunction) match { - case Failure(exception: IllegalStateException) => - exception.getMessage shouldBe "Received primary data cannot be enriched to expected data. Expected: edu.ie3.simona.agent.participant.data.Data$PrimaryData$ComplexPower, got: edu.ie3.simona.agent.participant.data.Data$PrimaryData$ActivePowerAndHeat, enriched to: edu.ie3.simona.agent.participant.data.Data$PrimaryData$ComplexPowerAndHeat" - case Failure(exception) => - fail(s"Failed with wrong exception:\n\t$exception") - case Success(_) => fail("Was meant to fail, but succeeded") - } - } - - "lead to proper enriched data, if supported" in { - val data = Map( - primaryServiceProxy.ref -> Some( - ActivePower(Kilowatts(0.0)) - ) - ) - - participantAgent.prepareData(data, reactivePowerFunction) match { - case Success(ComplexPower(p, q)) => - p should approximate(Megawatts(0.0)) - q should approximate(Megavars(0.0)) - case Success(value) => - fail(s"Succeeded, but with wrong data: '$value'.") - case Failure(exception) => - fail( - "Was meant to succeed, but failed with exception.", - exception, - ) - } - } - - "lead to proper enriched data, if supported and utilizing a active to reactive power function" in { - val data = Map( - primaryServiceProxy.ref -> Some( - ActivePower(Kilowatts(100.0)) - ) - ) - - participantAgent.prepareData( - data, - (p: squants.Power) => Kilovars(p.toKilowatts * tan(acos(0.9))), - ) match { - case Success(ComplexPower(p, q)) => - p should approximate(Kilowatts(100.0)) - q should approximate(Kilovars(48.43221)) - case Success(value) => - fail(s"Succeeded, but with wrong data: '$value'.") - case Failure(exception) => - fail( - "Was meant to succeed, but failed with exception.", - exception, - ) - } - } - } - } - } -} diff --git a/src/test/scala/edu/ie3/simona/agent/participant/ParticipantAgentFundamentalsSpec.scala b/src/test/scala/edu/ie3/simona/agent/participant/ParticipantAgentFundamentalsSpec.scala index 50c60d246a..d409a3f0a6 100644 --- a/src/test/scala/edu/ie3/simona/agent/participant/ParticipantAgentFundamentalsSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/participant/ParticipantAgentFundamentalsSpec.scala @@ -18,26 +18,14 @@ import edu.ie3.simona.agent.participant.statedata.ParticipantStateData.Participa import edu.ie3.simona.agent.state.AgentState import edu.ie3.simona.config.RuntimeConfig.BaseRuntimeConfig import edu.ie3.simona.event.notifier.NotifierConfig -import edu.ie3.simona.exceptions.agent.{ - AgentInitializationException, - InconsistentStateException, -} +import edu.ie3.simona.exceptions.agent.AgentInitializationException import edu.ie3.simona.model.participant.CalcRelevantData.FixedRelevantData import edu.ie3.simona.model.participant.ModelState.ConstantState import edu.ie3.simona.model.participant.SystemParticipant -import edu.ie3.simona.model.participant.control.QControl.CosPhiFixed -import edu.ie3.simona.model.participant.load.FixedLoadModel.FixedLoadRelevantData -import edu.ie3.simona.model.participant.load.{FixedLoadModel, LoadReference} import edu.ie3.simona.test.common.AgentSpec import edu.ie3.simona.test.common.model.participant.LoadTestData import edu.ie3.util.TimeUtil -import edu.ie3.util.scala.OperationInterval -import edu.ie3.util.scala.quantities.{ - Kilovoltamperes, - Megavars, - ReactivePower, - Vars, -} +import edu.ie3.util.scala.quantities.{Megavars, ReactivePower, Vars} import org.apache.pekko.actor.ActorRef.noSender import org.apache.pekko.actor.{ActorRef, ActorSystem} import org.apache.pekko.testkit.TestFSMRef @@ -46,8 +34,8 @@ import org.mockito.Mockito.when import org.scalatest.PrivateMethodTester import org.scalatest.prop.{TableDrivenPropertyChecks, TableFor3, TableFor5} import org.scalatestplus.mockito.MockitoSugar -import squants.energy.{Kilowatts, Megawatts, Watts} -import squants.{Each, Power} +import squants.Power +import squants.energy.{Megawatts, Watts} import java.util.UUID import java.util.concurrent.TimeUnit @@ -73,13 +61,6 @@ class ParticipantAgentFundamentalsSpec private implicit val pTolerance: Power = Watts(0.1) private implicit val qTolerance: ReactivePower = Vars(0.1) - private val outputConfig: NotifierConfig = - NotifierConfig( - simulationResultInfo = false, - powerRequestReply = false, - flexResult = false, - ) - /* Get one instance of the mock for participant agent */ private val mockAgentTestRef: TestFSMRef[AgentState, ParticipantStateData[ ComplexPower @@ -528,80 +509,6 @@ class ParticipantAgentFundamentalsSpec } } - "Determining the applicable nodal voltage" should { - "deliver the correct voltage" in { - val baseStateData = ParticipantModelBaseStateData[ - ComplexPower, - FixedLoadRelevantData.type, - ConstantState.type, - FixedLoadModel, - ]( - simulationStartDate, - simulationEndDate, - FixedLoadModel( - UUID.randomUUID(), - "test_load", - OperationInterval(0L, 1800L), - CosPhiFixed(0.95), - Kilovoltamperes(100.0), - 0.95, - LoadReference.ActivePower(Kilowatts(95.0)), - ), - None, - outputConfig, - SortedSet(0L, 900L, 1800L), - Map.empty, - 1e-12, - ValueStore.forVoltage(901L, Each(1.0)), - ValueStore(901L), - ValueStore(901L), - ValueStore(901L), - ValueStore(901L), - None, - ) - - ParticipantAgent.getAndCheckNodalVoltage( - baseStateData, - 1000L, - ) shouldBe Each(1.0) - } - - "throw an error, if no nodal voltage is available" in { - val baseStateData = ParticipantModelBaseStateData[ - ComplexPower, - FixedLoadRelevantData.type, - ConstantState.type, - FixedLoadModel, - ]( - simulationStartDate, - simulationEndDate, - FixedLoadModel( - UUID.randomUUID(), - "test_load", - OperationInterval(0L, 1800L), - CosPhiFixed(0.95), - Kilovoltamperes(100.0), - 0.95, - LoadReference.ActivePower(Kilowatts(95.0)), - ), - None, - outputConfig, - SortedSet(0L, 900L, 1800L), - Map.empty, - 1e-12, - ValueStore(901L), - ValueStore(901L), - ValueStore(901L), - ValueStore(901L), - ValueStore(901L), - None, - ) - - intercept[InconsistentStateException] { - ParticipantAgent.getAndCheckNodalVoltage(baseStateData, 1000L) - }.getMessage shouldBe "Not knowing any nodal voltage is not supposed to happen." - } - } } case object ParticipantAgentFundamentalsSpec extends MockitoSugar { diff --git a/src/test/scala/edu/ie3/simona/agent/participant/PvAgentModelCalculationSpec.scala b/src/test/scala/edu/ie3/simona/agent/participant/PvAgentModelCalculationSpec.scala index e511670bb4..7f1cf615fe 100644 --- a/src/test/scala/edu/ie3/simona/agent/participant/PvAgentModelCalculationSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/participant/PvAgentModelCalculationSpec.scala @@ -31,7 +31,6 @@ import edu.ie3.simona.agent.state.ParticipantAgentState.HandleInformation import edu.ie3.simona.config.SimonaConfig import edu.ie3.simona.config.RuntimeConfig.PvRuntimeConfig import edu.ie3.simona.event.notifier.NotifierConfig -import edu.ie3.simona.model.participant.load.{LoadModelBehaviour, LoadReference} import edu.ie3.simona.ontology.messages.Activation import edu.ie3.simona.ontology.messages.SchedulerMessage.Completion import edu.ie3.simona.ontology.messages.services.ServiceMessage.PrimaryServiceRegistrationMessage @@ -93,11 +92,7 @@ class PvAgentModelCalculationSpec /* Assign this test to receive the result events from agent */ override val systemListener: Iterable[ActorRef] = Iterable(self) - private val simonaConfig: SimonaConfig = - createSimonaConfig( - LoadModelBehaviour.FIX, - LoadReference.ActivePower(Kilowatts(0d)), - ) + private val simonaConfig: SimonaConfig = createSimonaConfig() private val defaultOutputConfig = NotifierConfig( simonaConfig.simona.output.participant.defaultConfig.simulationResult, simonaConfig.simona.output.participant.defaultConfig.powerRequestReply, diff --git a/src/test/scala/edu/ie3/simona/agent/participant/StorageAgentModelCalculationSpec.scala b/src/test/scala/edu/ie3/simona/agent/participant/StorageAgentModelCalculationSpec.scala index 12a537cb44..09cc903434 100644 --- a/src/test/scala/edu/ie3/simona/agent/participant/StorageAgentModelCalculationSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/participant/StorageAgentModelCalculationSpec.scala @@ -33,7 +33,6 @@ import edu.ie3.simona.event.ResultEvent.{ ParticipantResultEvent, } import edu.ie3.simona.event.notifier.NotifierConfig -import edu.ie3.simona.model.participant.load.{LoadModelBehaviour, LoadReference} import edu.ie3.simona.ontology.messages.Activation import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage._ import edu.ie3.simona.ontology.messages.flex.MinMaxFlexibilityMessage.ProvideMinMaxFlexOptions @@ -84,11 +83,7 @@ class StorageAgentModelCalculationSpec /* Assign this test to receive the result events from agent */ override val systemListener: Iterable[ActorRef] = Vector(self) - private val simonaConfig: SimonaConfig = - createSimonaConfig( - LoadModelBehaviour.FIX, - LoadReference.ActivePower(Kilowatts(0d)), - ) + private val simonaConfig: SimonaConfig = createSimonaConfig() private val outputConfig = NotifierConfig( simulationResultInfo = true, powerRequestReply = false, diff --git a/src/test/scala/edu/ie3/simona/agent/participant/WecAgentModelCalculationSpec.scala b/src/test/scala/edu/ie3/simona/agent/participant/WecAgentModelCalculationSpec.scala index 35b616625a..579fe4095b 100644 --- a/src/test/scala/edu/ie3/simona/agent/participant/WecAgentModelCalculationSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/participant/WecAgentModelCalculationSpec.scala @@ -38,7 +38,6 @@ import edu.ie3.simona.event.notifier.NotifierConfig import edu.ie3.simona.model.participant.ModelState.ConstantState import edu.ie3.simona.model.participant.WecModel import edu.ie3.simona.model.participant.WecModel.WecRelevantData -import edu.ie3.simona.model.participant.load.{LoadModelBehaviour, LoadReference} import edu.ie3.simona.ontology.messages.Activation import edu.ie3.simona.ontology.messages.SchedulerMessage.Completion import edu.ie3.simona.ontology.messages.services.ServiceMessage.PrimaryServiceRegistrationMessage @@ -63,7 +62,7 @@ import org.apache.pekko.testkit.{TestFSMRef, TestProbe} import org.apache.pekko.util.Timeout import org.scalatest.PrivateMethodTester import squants.Each -import squants.energy.{Kilowatts, Megawatts, Watts} +import squants.energy.{Megawatts, Watts} import squants.motion.MetersPerSecond import squants.thermal.Celsius @@ -101,11 +100,7 @@ class WecAgentModelCalculationSpec /* Assign this test to receive the result events from agent */ override val systemListener: Iterable[ActorRef] = Iterable(self) - private val simonaConfig: SimonaConfig = - createSimonaConfig( - LoadModelBehaviour.FIX, - LoadReference.ActivePower(Kilowatts(0d)), - ) + private val simonaConfig: SimonaConfig = createSimonaConfig() private val configUtil = ConfigUtil.ParticipantConfigUtil( simonaConfig.simona.runtime.participant ) diff --git a/src/test/scala/edu/ie3/simona/config/ConfigFailFastSpec.scala b/src/test/scala/edu/ie3/simona/config/ConfigFailFastSpec.scala index e51296405e..637d589717 100644 --- a/src/test/scala/edu/ie3/simona/config/ConfigFailFastSpec.scala +++ b/src/test/scala/edu/ie3/simona/config/ConfigFailFastSpec.scala @@ -27,8 +27,7 @@ import edu.ie3.simona.test.common.{ConfigTestData, UnitSpec} import edu.ie3.simona.util.ConfigUtil.{CsvConfigUtil, NotifierIdentifier} import edu.ie3.util.TimeUtil -import java.time.temporal.ChronoUnit -import java.time.{Duration, ZonedDateTime} +import java.time.ZonedDateTime import scala.concurrent.duration.DurationInt class ConfigFailFastSpec extends UnitSpec with ConfigTestData { @@ -1001,7 +1000,7 @@ class ConfigFailFastSpec extends UnitSpec with ConfigTestData { "one sink is configured!" } - "throw an exception if an influxDb1x is configured, but not accessible" ignore { + "throw an exception if an influxDb1x is configured, but not accessible" in { intercept[java.lang.IllegalArgumentException] { ConfigFailFast invokePrivate checkDataSink( Sink(None, Some(InfluxDb1x("", 0, "")), None) diff --git a/src/test/scala/edu/ie3/simona/event/SimonaListenerSpec.scala b/src/test/scala/edu/ie3/simona/event/SimonaListenerSpec.scala deleted file mode 100644 index c9c6b0238c..0000000000 --- a/src/test/scala/edu/ie3/simona/event/SimonaListenerSpec.scala +++ /dev/null @@ -1,111 +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.event - -import org.apache.pekko.actor.{ActorRef, ActorSystem} -import org.apache.pekko.testkit.{EventFilter, TestActorRef} -import com.typesafe.config.ConfigFactory -import edu.ie3.simona.event.SimonaListenerSpec.{TestEvent, UnknownEvent} -import edu.ie3.simona.event.listener.SimonaListenerWithFilter -import edu.ie3.simona.logging.SimonaLogging.SimonaBusLogging -import edu.ie3.simona.test.common.TestKitWithShutdown -import org.scalatest.matchers.should.Matchers - -import java.util.{Calendar, Date} - -object SimonaListenerSpec { - - // test classes - final case class TestEvent(str: String, date: Date) extends Event - - final case object UnknownEvent extends Event - -} - -class SimonaListenerSpec - extends TestKitWithShutdown( - ActorSystem( - "SimonaListenerSpec", - ConfigFactory - .parseString( - """ - |pekko.loggers =["edu.ie3.simona.test.common.SilentTestEventListener"] - |pekko.loglevel="debug" - |""".stripMargin - ), - ) - ) - with Matchers { - - // test listenerActor - class SimonaListenerActor(eventsToProcess: Option[List[String]] = None) - extends SimonaListenerWithFilter(eventsToProcess) { - override def preStart(): Unit = { - log.debug(s"{} started!", self) - } - - override def processEvent(event: Event, sender: ActorRef): Unit = { - event match { - case TestEvent(str, date) => - log.debug(s"Received '$str' from date $date") - case _ => log.warning("Received unknown event") - } - } - } - - // global vals - private val listener = TestActorRef( - new SimonaListenerActor - ) - - private val logPrefix = listener.underlyingActor.log match { - case simonaLogging: SimonaBusLogging => - simonaLogging.prefix() - case x => - throw new IllegalArgumentException( - s"Invalid logger in RuntimeEventListener: $x" - ) - } - - "A simple SimonaListener" should { - "be able receive an event and process it if it is expected" in { - val msgDate = Calendar.getInstance().getTime - val msg = "Hello World" - EventFilter - .debug( - message = s"$logPrefix Received '$msg' from date $msgDate", - occurrences = 1, - ) - .intercept { - listener ! TestEvent(msg, msgDate) - } - } - } - - "A simple listener" should { - "process an unknown Event differently than any other unknown object" in { - EventFilter - .warning( - message = s"$logPrefix Received unknown event", - occurrences = 1, - ) - .intercept { - listener ! UnknownEvent - } - val unknownMessage = "StringMessage" - EventFilter - .warning( - message = s"$logPrefix Received unknown message: $unknownMessage", - occurrences = 1, - ) - .intercept { - listener ! unknownMessage - } - } - } - -} diff --git a/src/test/scala/edu/ie3/simona/model/participant/FixedFeedInModelSpec.scala b/src/test/scala/edu/ie3/simona/model/participant/FixedFeedInModelSpec.scala deleted file mode 100644 index 0b6a80ea52..0000000000 --- a/src/test/scala/edu/ie3/simona/model/participant/FixedFeedInModelSpec.scala +++ /dev/null @@ -1,111 +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.model.participant - -import edu.ie3.simona.config.SimonaConfig -import edu.ie3.simona.config.RuntimeConfig.FixedFeedInRuntimeConfig -import edu.ie3.simona.model.participant.control.QControl -import edu.ie3.simona.model.participant.load.{LoadModelBehaviour, LoadReference} -import edu.ie3.simona.test.common.input.FixedFeedInputTestData -import edu.ie3.simona.test.common.{DefaultTestData, UnitSpec} -import edu.ie3.simona.util.ConfigUtil -import edu.ie3.util.quantities.PowerSystemUnits -import edu.ie3.util.quantities.PowerSystemUnits.MEGAVOLTAMPERE -import edu.ie3.util.scala.quantities.{ - ApparentPower, - Kilovoltamperes, - Megavoltamperes, - Voltamperes, -} -import org.scalatest.PrivateMethodTester -import squants.energy.Kilowatts - -class FixedFeedInModelSpec - extends UnitSpec - with FixedFeedInputTestData - with DefaultTestData - with PrivateMethodTester { - - // Equals to 1 VA power - private implicit val powerTolerance: ApparentPower = Voltamperes( - 1.0 - ) - - "The fixed feed in model object" should { - - "build a correct FixedFeedModel from correct input" in { - val simonaConfig: SimonaConfig = - createSimonaConfig( - LoadModelBehaviour.FIX, - LoadReference.ActivePower(Kilowatts(0.0)), - ) - val modelConfig = ConfigUtil - .ParticipantConfigUtil( - simonaConfig.simona.runtime.participant - ) - .getOrDefault[FixedFeedInRuntimeConfig](fixedFeedInput.getUuid) - - val actualModel = FixedFeedInModel.apply( - fixedFeedInput, - modelConfig, - defaultSimulationStart, - defaultSimulationEnd, - ) - - inside(actualModel) { - case FixedFeedInModel( - uuid, - id, - operationInterval, - qControl, - sRated, - cosPhiRated, - ) => - uuid shouldBe fixedFeedInput.getUuid - id shouldBe fixedFeedInput.getId - operationInterval shouldBe defaultOperationInterval - qControl shouldBe QControl(fixedFeedInput.getqCharacteristics) - sRated should approximate( - Megavoltamperes( - fixedFeedInput.getsRated().to(MEGAVOLTAMPERE).getValue.doubleValue - ) - ) - cosPhiRated shouldBe fixedFeedInput.getCosPhiRated - } - } - - "return approximately correct power calculations" in { - val expectedPower = Kilowatts( - fixedFeedInput - .getsRated() - .to(PowerSystemUnits.KILOWATT) - .getValue - .doubleValue() * -1 * fixedFeedInput.getCosPhiRated - ) - - val actualModel = new FixedFeedInModel( - fixedFeedInput.getUuid, - fixedFeedInput.getId, - defaultOperationInterval, - QControl.apply(fixedFeedInput.getqCharacteristics()), - Kilovoltamperes( - fixedFeedInput - .getsRated() - .to(PowerSystemUnits.KILOVOLTAMPERE) - .getValue - .doubleValue() - ), - fixedFeedInput.getCosPhiRated, - ) - - actualModel.calculateActivePower( - ModelState.ConstantState, - CalcRelevantData.FixedRelevantData, - ) shouldBe expectedPower - } - } -} diff --git a/src/test/scala/edu/ie3/simona/model/participant/PvModelITSpec.scala b/src/test/scala/edu/ie3/simona/model/participant/PvModelITSpec.scala index 170a040aab..632a3a2423 100644 --- a/src/test/scala/edu/ie3/simona/model/participant/PvModelITSpec.scala +++ b/src/test/scala/edu/ie3/simona/model/participant/PvModelITSpec.scala @@ -38,7 +38,6 @@ class PvModelITSpec extends Matchers with UnitSpec with PvModelITHelper { val weather = modelToWeatherMap(modelId) val neededData = PvModel.PvRelevantData( dateTime, - 3600L, weather.diffIrr, weather.dirIrr, ) diff --git a/src/test/scala/edu/ie3/simona/model/participant/PvModelSpec.scala b/src/test/scala/edu/ie3/simona/model/participant/PvModelSpec.scala index c7aef19b1c..0e7fc3b93d 100644 --- a/src/test/scala/edu/ie3/simona/model/participant/PvModelSpec.scala +++ b/src/test/scala/edu/ie3/simona/model/participant/PvModelSpec.scala @@ -17,7 +17,7 @@ import edu.ie3.util.scala.quantities._ import org.locationtech.jts.geom.{Coordinate, GeometryFactory, Point} import org.scalatest.GivenWhenThen import squants.Each -import squants.energy.Kilowatts +import squants.energy.{Kilowatts, Megajoules} import squants.space.{Angle, Degrees, Radians} import tech.units.indriya.quantity.Quantities.getQuantity import tech.units.indriya.unit.Units._ @@ -85,11 +85,10 @@ class PvModelSpec extends UnitSpec with GivenWhenThen with DefaultTestData { ) private implicit val angleTolerance: Angle = Radians(1e-10) - private implicit val irradiationTolerance: Irradiation = - WattHoursPerSquareMeter(1e-10) - private implicit val apparentPowerTolerance: ApparentPower = Kilovoltamperes( - 1e-10 - ) + private implicit val irradianceTolerance: Irradiance = + WattsPerSquareMeter(1e-10) + private implicit val apparentPowerTolerance: ApparentPower = + Kilovoltamperes(1e-10) private implicit val reactivePowerTolerance: ReactivePower = Megavars(1e-10) "A PV Model" should { @@ -508,20 +507,20 @@ class PvModelSpec extends UnitSpec with GivenWhenThen with DefaultTestData { } } - "calculate the extraterrestrial radiation I0 correctly" in { + "calculate the extraterrestrial radiance g0 correctly" in { val testCases = Table( - ("j", "I0Sol"), + ("j", "g0Sol"), (0d, 1414.91335d), // Jan 1st (2.943629280897834d, 1322.494291080537598d), // Jun 21st (4.52733626243351d, 1355.944773587800003d), // Sep 21st ) - forAll(testCases) { (j, I0Sol) => - When("the extraterrestrial radiation is calculated") - val I0Calc = pvModel.calcExtraterrestrialRadiationI0(Radians(j)) + forAll(testCases) { (j, g0Sol) => + When("the extraterrestrial radiance is calculated") + val g0Calc = pvModel.calcExtraterrestrialRadianceG0(Radians(j)) Then("result should match the test data") - I0Calc should approximate(WattHoursPerSquareMeter(I0Sol)) + g0Calc should approximate(WattsPerSquareMeter(g0Sol)) } } @@ -612,7 +611,7 @@ class PvModelSpec extends UnitSpec with GivenWhenThen with DefaultTestData { ) - /** Calculate the angle of incidence of beam radiation on a surface + /** Calculate the angle of incidence of beam irradiance on a surface * located at a latitude at a certain hour angle (solar time) on a given * declination (date) if the surface is tilted by a certain slope from * the horizontal and pointed to a certain panel azimuth west of south. @@ -687,7 +686,7 @@ class PvModelSpec extends UnitSpec with GivenWhenThen with DefaultTestData { } } - "calculate the estimate beam radiation eBeamS correctly" in { + "calculate the estimate beam irradiance gBeamS correctly" in { val testCases = Table( ( "latitudeDeg", @@ -696,10 +695,10 @@ class PvModelSpec extends UnitSpec with GivenWhenThen with DefaultTestData { "deltaDeg", "omegaDeg", "thetaGDeg", - "eBeamSSol", + "gBeamSSol", ), (40d, 0d, 0d, -11.6d, -37.5d, 37.0d, - 67.777778d), // flat surface => eBeamS = eBeamH + 67.777778d), // flat surface => gBeamS = gBeamH (40d, 60d, 0d, -11.6d, -37.5d, 37.0d, 112.84217113154841369d), // 2011-02-20T09:00:00 (40d, 60d, 0d, -11.6d, -78.0d, 75.0d, 210.97937494450755d), // sunrise @@ -710,7 +709,7 @@ class PvModelSpec extends UnitSpec with GivenWhenThen with DefaultTestData { (40d, 60d, -90.0d, -11.6d, 60.0d, 91.0d, 0d), // no direct beam ) - /** For a given hour angle, the estimate beam radiation on a sloped + /** For a given hour angle, the estimated beam irradiance on a sloped * surface is calculated for the next 60 minutes. Reference p.95 * https://www.sku.ac.ir/Datafiles/BookLibrary/45/John%20A.%20Duffie,%20William%20A.%20Beckman(auth.)-Solar%20Engineering%20of%20Thermal%20Processes,%20Fourth%20Edition%20(2013).pdf */ @@ -722,12 +721,12 @@ class PvModelSpec extends UnitSpec with GivenWhenThen with DefaultTestData { deltaDeg, omegaDeg, thetaGDeg, - eBeamSSol, + gBeamSSol, ) => Given("using the input data") - // Beam Radiation on a horizontal surface - val eBeamH = - 67.777778d // 1 MJ/m^2 = 277,778 Wh/m^2 -> 0.244 MJ/m^2 = 67.777778 Wh/m^2 + // Beam irradiance on a horizontal surface + // 1 MJ/m^2 = 277,778 Wh/m^2 -> 0.244 MJ/m^2 = 67.777778 Wh/m^2 + val gBeamH = 67.777778d val omegaSS = pvModel.calcSunsetAngleOmegaSS( Degrees(latitudeDeg), Degrees(deltaDeg), @@ -740,9 +739,9 @@ class PvModelSpec extends UnitSpec with GivenWhenThen with DefaultTestData { omegaSR, ) // omega1 and omega2 - When("the beam radiation is calculated") - val eBeamSCalc = pvModel.calcBeamRadiationOnSlopedSurface( - WattHoursPerSquareMeter(eBeamH), + When("the beam irradiance is calculated") + val gBeamSCalc = pvModel.calcBeamIrradianceOnSlopedSurface( + WattsPerSquareMeter(gBeamH), omegas, Degrees(deltaDeg), Degrees(latitudeDeg), @@ -751,82 +750,82 @@ class PvModelSpec extends UnitSpec with GivenWhenThen with DefaultTestData { ) Then("result should match the test data") - eBeamSCalc should approximate(WattHoursPerSquareMeter(eBeamSSol)) + gBeamSCalc should approximate(WattsPerSquareMeter(gBeamSSol)) } } - "calculate the estimated diffuse radiation eDifS correctly" in { - def megaJoule2WattHours(megajoule: Double): Double = { - megajoule / (3.6 / 1000) - } + "calculate the estimated diffuse irradiance gDifS correctly" in { val testCases = Table( - ("thetaGDeg", "thetaZDeg", "gammaEDeg", "airMass", "I0", "eDifSSol"), + ("thetaGDeg", "thetaZDeg", "gammaEDeg", "airMass", "g0", "gDifSSol"), // Reference Duffie 4th ed., p.95 // I_0 = 5.025 MJ * 277.778 Wh/MJ = 1395.83445 Wh - // eDifSSol is 0.79607 MJ (0.444 + 0.348 + 0.003) if one only calculates the relevant terms + // gDifSSol is 0.79607 MJ (0.444 + 0.348 + 0.003) if one only calculates the relevant terms // from I_T on p. 96, but Duffie uses fixed f values, so the inaccuracy is fine (approx. 4.5 Wh/m^2 or 0.016 MJ/m^2) + // g0 and gDifSol are energies (radiations) in Duffie, but we interpret them as powers (radiances). ( 37.0d, 62.2d, 60d, 2.144d, - megaJoule2WattHours(5.025), - megaJoule2WattHours(0.812140993078191252), + Megajoules(5.025).toWattHours, + Megajoules(0.812140993078191252).toWattHours, ), ) forAll(testCases) { - (thetaGDeg, thetaZDeg, gammaEDeg, airMass, I0, eDifSSol) => + (thetaGDeg, thetaZDeg, gammaEDeg, airMass, g0, gDifSSol) => // Reference Duffie 4th ed., p.95 + // gBeamH and gDifH are energies (radiations) in Duffie, but we interpret them as powers (radiances). Given("using the input data") - // Beam Radiation on horizontal surface - val eBeamH = - megaJoule2WattHours(0.244) // 0.244 MJ/m^2 = 67.777778 Wh/m^2 - // Diffuse Radiation on a horizontal surface - val eDifH = - megaJoule2WattHours(0.796) // 0.796 MJ/m^2 = 221.111111 Wh/m^2 - - When("the diffuse radiation is calculated") - val eDifSCalc = pvModel.calcDiffuseRadiationOnSlopedSurfacePerez( - WattHoursPerSquareMeter(eDifH), - WattHoursPerSquareMeter(eBeamH), + // Beam irradiance on horizontal surface given a radiation for one hour + // 0.244 MJ/m^2 = 67.777778 Wh/m^2 + val gBeamH = Megajoules(0.244).toWattHours + // Diffuse irradiance on a horizontal surface given a radiation for one hour + // 0.796 MJ/m^2 = 221.111111 Wh/m^2 + val gDifH = Megajoules(0.796).toWattHours + + When("the diffuse irradiance is calculated") + val gDifSCalc = pvModel.calcDiffuseIrradianceOnSlopedSurfacePerez( + WattsPerSquareMeter(gDifH), + WattsPerSquareMeter(gBeamH), airMass, - WattHoursPerSquareMeter(I0), + WattsPerSquareMeter(g0), Degrees(thetaZDeg), Degrees(thetaGDeg), Degrees(gammaEDeg), ) Then("result should match the test data") - eDifSCalc should approximate(WattHoursPerSquareMeter(eDifSSol)) + gDifSCalc should approximate(WattsPerSquareMeter(gDifSSol)) } } "calculate the ground reflection eRefS" in { val testCases = Table( - ("gammaEDeg", "albedo", "eRefSSol"), + ("gammaEDeg", "albedo", "gRefSSol"), (60d, 0.60d, 42.20833319999999155833336d), // '2011-02-20T09:00:00' ) - forAll(testCases) { (gammaEDeg, albedo, eRefSSol) => + forAll(testCases) { (gammaEDeg, albedo, gRefSSol) => Given("using the input data") - // Beam Radiation on horizontal surface - val eBeamH = - 67.777778d // 1 MJ/m^2 = 277,778 Wh/m^2 -> 0.244 MJ/m^2 = 67.777778 Wh/m^2 - // Diffuse Radiation on a horizontal surface - val eDifH = 213.61111d // 0.769 MJ/m^2 = 213,61111 Wh/m^2 + // Beam irradiance on horizontal surface given a radiation for one hour + // 1 MJ/m^2 = 277,778 Wh/m^2 -> 0.244 MJ/m^2 = 67.777778 Wh/m^2 + val gBeamH = 67.777778d + // Diffuse irradiance on a horizontal surface given a radiation for one hour + // 0.769 MJ/m^2 = 213,61111 Wh/m^2 + val gDifH = 213.61111d When("the ground reflection is calculated") - val eRefSCalc = pvModel.calcReflectedRadiationOnSlopedSurface( - WattHoursPerSquareMeter(eBeamH), - WattHoursPerSquareMeter(eDifH), + val gRefSCalc = pvModel.calcReflectedIrradianceOnSlopedSurface( + WattsPerSquareMeter(gBeamH), + WattsPerSquareMeter(gDifH), Degrees(gammaEDeg), albedo, ) Then("result should match the test data") - eRefSCalc should approximate(WattHoursPerSquareMeter(eRefSSol)) + gRefSCalc should approximate(WattsPerSquareMeter(gRefSSol)) } } } diff --git a/src/test/scala/edu/ie3/simona/model/participant/load/FixedLoadModelSpec.scala b/src/test/scala/edu/ie3/simona/model/participant/load/FixedLoadModelSpec.scala deleted file mode 100644 index 6a75bd93e5..0000000000 --- a/src/test/scala/edu/ie3/simona/model/participant/load/FixedLoadModelSpec.scala +++ /dev/null @@ -1,151 +0,0 @@ -/* - * © 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.participant.load - -import edu.ie3.simona.model.SystemComponent -import edu.ie3.simona.model.participant.ModelState -import edu.ie3.simona.model.participant.control.QControl -import edu.ie3.simona.model.participant.load.LoadReference.{ - ActivePower, - EnergyConsumption, -} -import edu.ie3.simona.test.common.input.LoadInputTestData -import edu.ie3.simona.test.common.{DefaultTestData, UnitSpec} -import edu.ie3.util.quantities.PowerSystemUnits -import edu.ie3.util.scala.quantities.Kilovoltamperes -import org.scalatest.prop.TableDrivenPropertyChecks -import squants.Power -import squants.energy.{KilowattHours, Watts} - -class FixedLoadModelSpec - extends UnitSpec - with LoadInputTestData - with DefaultTestData - with TableDrivenPropertyChecks { - - private implicit val tolerance: Power = Watts(1d) - - "A fixed load model" should { - - val defaultOperationInterval = - SystemComponent.determineOperationInterval( - defaultSimulationStart, - defaultSimulationEnd, - loadInput.getOperationTime, - ) - - "be instantiated from valid input correctly" in { - val testData = Table( - ("reference", "expectedReferenceActivePower"), - (ActivePower(Watts(268.6)), Watts(268.6)), - (EnergyConsumption(KilowattHours(3000d)), Watts(342.24)), - ) - - forAll(testData) { (reference, expectedReferenceActivePower: Power) => - val actual = new FixedLoadModel( - loadInput.getUuid, - loadInput.getId, - defaultOperationInterval, - QControl.apply(loadInput.getqCharacteristics), - Kilovoltamperes( - loadInput.getsRated - .to(PowerSystemUnits.KILOVOLTAMPERE) - .getValue - .doubleValue() - ), - loadInput.getCosPhiRated, - reference, - ) - - val calculatedPower = actual - .calculateActivePower( - ModelState.ConstantState, - FixedLoadModel.FixedLoadRelevantData, - ) - - calculatedPower should approximate(expectedReferenceActivePower) - } - } - - "return approximately the same power in 10,000 calculations" in { - - val testData = Table( - ("reference", "expectedPower"), - (ActivePower(Watts(268.6)), Watts(268.6)), - (EnergyConsumption(KilowattHours(3000d)), Watts(342.24)), - ) - - forAll(testData) { (reference, expectedPower: Power) => - val dut = new FixedLoadModel( - loadInput.getUuid, - loadInput.getId, - defaultOperationInterval, - QControl.apply(loadInput.getqCharacteristics), - Kilovoltamperes( - loadInput.getsRated - .to(PowerSystemUnits.KILOVOLTAMPERE) - .getValue - .doubleValue() - ), - loadInput.getCosPhiRated, - reference, - ) - - (1 to 10000).foreach { _ => - val calculatedPower = dut - .calculateActivePower( - ModelState.ConstantState, - FixedLoadModel.FixedLoadRelevantData, - ) - calculatedPower should approximate(expectedPower) - } - } - } - - "consider the (global) scaling factor correctly" in { - val testData = Table( - ("reference", "expectedPower"), - (ActivePower(Watts(268.6d)), Watts(268.6)), - (EnergyConsumption(KilowattHours(3000d)), Watts(342.24)), - ) - - forAll(testData) { (reference, expectedPower: Power) => - val relevantData = FixedLoadModel.FixedLoadRelevantData - - val scales: LazyList[Double] = - LazyList.iterate(0.0)(_ + 0.1).takeWhile(_ <= 2.0) - - scales.foreach { scale => - val scaledSRated = Kilovoltamperes( - loadInput.getsRated - .to(PowerSystemUnits.KILOWATT) - .getValue - .doubleValue() * scale - ) - val dut = new FixedLoadModel( - loadInput.getUuid, - loadInput.getId, - defaultOperationInterval, - QControl.apply(loadInput.getqCharacteristics), - scaledSRated, - loadInput.getCosPhiRated, - reference, - ) - - val calculatedPower = dut - .calculateActivePower( - ModelState.ConstantState, - relevantData, - ) * scale - val expectedScaledPower = expectedPower * scale - - calculatedPower should approximate(expectedScaledPower) - } - } - } - } -} diff --git a/src/test/scala/edu/ie3/simona/model/participant/load/LoadModelScalingSpec.scala b/src/test/scala/edu/ie3/simona/model/participant/load/LoadModelScalingSpec.scala deleted file mode 100644 index c791664bd5..0000000000 --- a/src/test/scala/edu/ie3/simona/model/participant/load/LoadModelScalingSpec.scala +++ /dev/null @@ -1,353 +0,0 @@ -/* - * © 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.participant.load - -import edu.ie3.datamodel.models.OperationTime -import edu.ie3.datamodel.models.input.system.LoadInput -import edu.ie3.datamodel.models.input.system.characteristic.CosPhiFixed -import edu.ie3.datamodel.models.input.{NodeInput, OperatorInput} -import edu.ie3.datamodel.models.profile.BdewStandardLoadProfile -import edu.ie3.datamodel.models.voltagelevels.GermanVoltageLevelUtils -import edu.ie3.simona.model.SystemComponent -import edu.ie3.simona.model.participant.CalcRelevantData.LoadRelevantData -import edu.ie3.simona.model.participant.ModelState.ConstantState -import edu.ie3.simona.model.participant.load.LoadReference.{ - ActivePower, - EnergyConsumption, -} -import edu.ie3.simona.model.participant.load.profile.ProfileLoadModel -import edu.ie3.simona.model.participant.load.random.RandomLoadModel -import edu.ie3.simona.test.common.UnitSpec -import edu.ie3.util.TimeUtil -import edu.ie3.util.quantities.PowerSystemUnits -import org.scalatest.prop.TableDrivenPropertyChecks -import squants.energy.{KilowattHours, Watts} -import squants.time.Minutes -import squants.{Dimensionless, Each, Energy, Percent, Power, Quantity} -import tech.units.indriya.quantity.Quantities - -import java.time.ZonedDateTime -import java.time.temporal.ChronoUnit -import java.util.UUID - -class LoadModelScalingSpec extends UnitSpec with TableDrivenPropertyChecks { - - "Testing correct scaling of load models" when { - val simulationStartDate = - TimeUtil.withDefaults.toZonedDateTime("2019-01-01T00:00:00Z") - val simulationEndDate = - TimeUtil.withDefaults.toZonedDateTime("2019-12-31T23:59:00Z") - - "having a profile load model" should { - val profileLoadInput = - new LoadInput( - UUID.fromString("4eeaf76a-ec17-4fc3-872d-34b7d6004b03"), - "testLoad", - OperatorInput.NO_OPERATOR_ASSIGNED, - OperationTime.notLimited(), - new NodeInput( - UUID.fromString("e5c1cde5-c161-4a4f-997f-fcf31fecbf57"), - "TestNodeInputModel", - OperatorInput.NO_OPERATOR_ASSIGNED, - OperationTime.notLimited(), - Quantities.getQuantity(1d, PowerSystemUnits.PU), - false, - NodeInput.DEFAULT_GEO_POSITION, - GermanVoltageLevelUtils.LV, - -1, - ), - new CosPhiFixed("cosPhiFixed:{(0.0,0.95)}"), - null, - BdewStandardLoadProfile.H0, - false, - Quantities.getQuantity(3000d, PowerSystemUnits.KILOWATTHOUR), - Quantities.getQuantity(282.74d, PowerSystemUnits.VOLTAMPERE), - 0.95, - ) - val operationInterval = - SystemComponent.determineOperationInterval( - simulationStartDate, - simulationEndDate, - profileLoadInput.getOperationTime, - ) - - val targetEnergyConsumption = KilowattHours(3000d) - - "reach the targeted annual energy consumption" in { - forAll( - Table( - "profile", - BdewStandardLoadProfile.H0, - BdewStandardLoadProfile.L0, - BdewStandardLoadProfile.G0, - ) - ) { profile => - val loadInput = profileLoadInput.copy().loadprofile(profile).build() - - val model = ProfileLoadModel( - loadInput, - operationInterval, - 1.0d, - EnergyConsumption(targetEnergyConsumption), - ) - - /* Test against a permissible deviation of 2 %. As per official documentation of the bdew load profiles - * [https://www.bdew.de/media/documents/2000131_Anwendung-repraesentativen_Lastprofile-Step-by-step.pdf] 1.5 % - * are officially permissible. But, as we currently do not take (bank) holidays into account, we cannot reach - * this accuracy. */ - - calculateEnergyDiffForYear( - model, - simulationStartDate, - targetEnergyConsumption, - ) should be < Percent(2d) - } - } - - "correctly account for the scaling factor, when targeting a given annual energy consumption" in { - val scalingFactor = 1.5 - val expectedEnergy = KilowattHours(4500d) - - val model = ProfileLoadModel( - profileLoadInput, - operationInterval, - scalingFactor, - EnergyConsumption(targetEnergyConsumption), - ) - - calculateEnergyDiffForYear( - model, - simulationStartDate, - expectedEnergy, - ) should be < Percent(2d) - } - - val targetMaximumPower = Watts(268.6) - - "approximately reach the maximum power" in { - forAll( - Table( - "profile", - BdewStandardLoadProfile.H0, - BdewStandardLoadProfile.L0, - BdewStandardLoadProfile.G0, - ) - ) { profile => - val loadInput = profileLoadInput.copy().loadprofile(profile).build() - - val model = ProfileLoadModel( - loadInput, - operationInterval, - 1.0d, - ActivePower(targetMaximumPower), - ) - - val maximumPower = calculatePowerForYear( - model, - simulationStartDate, - ).maxOption.value - - implicit val tolerance: Power = Watts(1d) - maximumPower should approximate(targetMaximumPower) - } - } - - "correctly account for the scaling factor when targeting at maximum power" in { - val scalingFactor = 1.5 - val expectedMaximum = Watts(402.9) - - val model = ProfileLoadModel( - profileLoadInput, - operationInterval, - scalingFactor, - ActivePower(targetMaximumPower), - ) - - val maximumPower = calculatePowerForYear( - model, - simulationStartDate, - ).maxOption.value - - implicit val tolerance: Power = Watts(1.5d) - maximumPower should approximate(expectedMaximum) - } - } - - "having a random load model" should { - val randomLoadInput = - new LoadInput( - UUID.fromString("4eeaf76a-ec17-4fc3-872d-34b7d6004b03"), - "testLoad", - OperatorInput.NO_OPERATOR_ASSIGNED, - OperationTime.notLimited(), - new NodeInput( - UUID.fromString("e5c1cde5-c161-4a4f-997f-fcf31fecbf57"), - "TestNodeInputModel", - OperatorInput.NO_OPERATOR_ASSIGNED, - OperationTime.notLimited(), - Quantities.getQuantity(1d, PowerSystemUnits.PU), - false, - NodeInput.DEFAULT_GEO_POSITION, - GermanVoltageLevelUtils.LV, - -1, - ), - new CosPhiFixed("cosPhiFixed:{(0.0,0.95)}"), - null, - BdewStandardLoadProfile.H0, - false, - Quantities.getQuantity(3000d, PowerSystemUnits.KILOWATTHOUR), - Quantities.getQuantity(282.74d, PowerSystemUnits.VOLTAMPERE), - 0.95, - ) - val operationInterval = - SystemComponent.determineOperationInterval( - simulationStartDate, - simulationEndDate, - randomLoadInput.getOperationTime, - ) - - val targetEnergyConsumption = KilowattHours(3000d) - - "reach the targeted annual energy consumption" in { - val model = RandomLoadModel( - randomLoadInput, - operationInterval, - 1.0d, - EnergyConsumption(targetEnergyConsumption), - ) - - calculateEnergyDiffForYear( - model, - simulationStartDate, - targetEnergyConsumption, - ) should be < Percent(1d) - } - - "correctly account for the scaling factor, when targeting a given annual energy consumption" in { - val scalingFactor = 1.5 - val expectedEnergy = KilowattHours(4500d) - - val model = RandomLoadModel( - randomLoadInput, - operationInterval, - scalingFactor, - EnergyConsumption(targetEnergyConsumption), - ) - - calculateEnergyDiffForYear( - model, - simulationStartDate, - expectedEnergy, - ) should be < Percent(2d) - } - - val targetMaximumPower = Watts(268.6) - "approximately reach the maximum power" in { - val model = RandomLoadModel( - randomLoadInput, - operationInterval, - 1.0d, - ActivePower(targetMaximumPower), - ) - - val powers = calculatePowerForYear( - model, - simulationStartDate, - ).toIndexedSeq.sorted.toArray - - val quantile95 = RandomLoadModelSpec.get95Quantile(powers) - - getRelativeDifference( - quantile95, - targetMaximumPower, - ) should be < Percent(2d) - } - - "correctly account for the scaling factor when targeting at maximum power" in { - val scalingFactor = 1.5 - val expectedMaximum = targetMaximumPower * scalingFactor - - val model = RandomLoadModel( - randomLoadInput, - operationInterval, - scalingFactor, - ActivePower(targetMaximumPower), - ) - - val powers = calculatePowerForYear( - model, - simulationStartDate, - ).toIndexedSeq.sorted.toArray - - /* Tolerance is equivalent to 10 W difference between the 95%-percentile of the obtained random results and the - * target maximum power. Because of the stochastic nature, the maximum power cannot be met perfectly */ - implicit val tolerance: Power = Watts(10d) - RandomLoadModelSpec.get95Quantile(powers) should - approximate(expectedMaximum) - } - } - } - - def calculateEnergyDiffForYear[C <: LoadRelevantData]( - model: LoadModel[C], - simulationStartDate: ZonedDateTime, - expectedEnergy: Energy, - ): Dimensionless = { - val duration = Minutes(15d) - - val avgEnergy = calculatePowerForYear( - model: LoadModel[C], - simulationStartDate: ZonedDateTime, - ).foldLeft(KilowattHours(0)) { case (energySum, power) => - energySum + (power * duration) - } - - getRelativeDifference( - avgEnergy, - expectedEnergy, - ) - } - - def calculatePowerForYear[C <: LoadRelevantData]( - model: LoadModel[C], - simulationStartDate: ZonedDateTime, - ): Iterable[Power] = { - val quarterHoursInYear = 365L * 96L - - (0L until quarterHoursInYear) - .map { quarterHour => - val tick = quarterHour * 15 * 60 - val relevantData = createRelevantData(model)( - simulationStartDate.plus(quarterHour * 15, ChronoUnit.MINUTES) - ) - - model - .calculatePower( - tick, - Each(0d), - ConstantState, - relevantData, - ) - .p - } - } - - def createRelevantData[C <: LoadRelevantData]( - model: LoadModel[C] - ): ZonedDateTime => C = - model match { - case _: RandomLoadModel => RandomLoadModel.RandomRelevantData - case _: ProfileLoadModel => ProfileLoadModel.ProfileRelevantData - } - - def getRelativeDifference[Q <: Quantity[Q]]( - actualResult: Q, - expectedResult: Q, - ): Dimensionless = - Each((expectedResult - actualResult).abs / expectedResult) - -} diff --git a/src/test/scala/edu/ie3/simona/model/participant/load/LoadModelSpec.scala b/src/test/scala/edu/ie3/simona/model/participant/load/LoadModelSpec.scala deleted file mode 100644 index cca01b7904..0000000000 --- a/src/test/scala/edu/ie3/simona/model/participant/load/LoadModelSpec.scala +++ /dev/null @@ -1,153 +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.model.participant.load - -import edu.ie3.simona.model.participant.control.QControl -import edu.ie3.simona.model.participant.load.profile.ProfileLoadModel -import edu.ie3.simona.model.participant.load.random.RandomLoadModel -import edu.ie3.simona.test.common.UnitSpec -import edu.ie3.simona.test.common.input.LoadInputTestData -import edu.ie3.util.scala.quantities.{ApparentPower, Voltamperes} -import org.scalatest.PrivateMethodTester -import org.scalatest.prop.TableDrivenPropertyChecks -import squants.energy.{KilowattHours, Power, Watts} - -class LoadModelSpec - extends UnitSpec - with LoadInputTestData - with PrivateMethodTester - with TableDrivenPropertyChecks { - - private implicit val powerTolerance: ApparentPower = Voltamperes(1e-3) - - "The load model object" should { - - "build a correct ProfileLoadModel from correct input" in { - - val params = Table( - ("reference", "scaling", "sRated"), - ( - LoadReference.ActivePower(Watts(268.6)), - 1d, - Voltamperes(282.7368), - ), - ( - LoadReference.EnergyConsumption(KilowattHours(3000.0)), - 1d, - Voltamperes(848.2105), - ), - ( - LoadReference.ActivePower(Watts(268.6)), - 1.5d, - Voltamperes(424.1053), - ), - ( - LoadReference.EnergyConsumption(KilowattHours(3000.0)), - 1.5d, - Voltamperes(1272.3158), - ), - ) - - forAll(params) { - ( - reference: LoadReference, - scaling: Double, - expectedSRated: ApparentPower, - ) => - { - val actual = ProfileLoadModel( - loadInput, - defaultOperationInterval, - scaling, - reference, - ) - inside(actual) { - case ProfileLoadModel( - uuid, - id, - operationInterval, - qControl, - sRated, - cosPhiRated, - loadProfile, - actualReference, - ) => - uuid shouldBe loadInput.getUuid - id shouldBe loadInput.getId - operationInterval shouldBe defaultOperationInterval - qControl shouldBe QControl(loadInput.getqCharacteristics) - sRated should approximate(expectedSRated) - cosPhiRated shouldBe loadInput.getCosPhiRated - loadProfile shouldBe loadInput.getLoadProfile - actualReference shouldBe reference.scale(scaling) - } - } - } - } - - "build a correct RandomLoadModel from correct input" in { - - val params = Table( - ("reference", "scaling", "sRated"), - ( - LoadReference.ActivePower(Watts(268.6)), - 1d, - Voltamperes(311.0105), - ), - ( - LoadReference.EnergyConsumption(KilowattHours(3000.0)), - 1d, - Voltamperes(770.8076), - ), - ( - LoadReference.ActivePower(Watts(268.6)), - 1.5d, - Voltamperes(466.5158), - ), - ( - LoadReference.EnergyConsumption(KilowattHours(3000.0)), - 1.5d, - Voltamperes(1156.2114), - ), - ) - - forAll(params) { - ( - reference: LoadReference, - scaling: Double, - expectedSRated: ApparentPower, - ) => - { - val actual = RandomLoadModel( - loadInput, - defaultOperationInterval, - scaling, - reference, - ) - inside(actual) { - case RandomLoadModel( - uuid, - id, - operationInterval, - qControl, - sRated, - cosPhiRated, - actualReference, - ) => - uuid shouldBe loadInput.getUuid - id shouldBe loadInput.getId - operationInterval shouldBe defaultOperationInterval - qControl shouldBe QControl(loadInput.getqCharacteristics) - sRated should approximate(expectedSRated) - cosPhiRated shouldBe loadInput.getCosPhiRated - actualReference shouldBe reference.scale(scaling) - } - } - } - } - } -} diff --git a/src/test/scala/edu/ie3/simona/model/participant/load/ProfileLoadModelSpec.scala b/src/test/scala/edu/ie3/simona/model/participant/load/ProfileLoadModelSpec.scala deleted file mode 100644 index 0cd81c66b5..0000000000 --- a/src/test/scala/edu/ie3/simona/model/participant/load/ProfileLoadModelSpec.scala +++ /dev/null @@ -1,128 +0,0 @@ -/* - * © 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.participant.load - -import edu.ie3.datamodel.models.OperationTime -import edu.ie3.datamodel.models.input.system.LoadInput -import edu.ie3.datamodel.models.input.system.characteristic.CosPhiFixed -import edu.ie3.datamodel.models.input.{NodeInput, OperatorInput} -import edu.ie3.datamodel.models.profile.BdewStandardLoadProfile -import edu.ie3.datamodel.models.voltagelevels.GermanVoltageLevelUtils -import edu.ie3.simona.model.SystemComponent -import edu.ie3.simona.model.participant.load.LoadReference.{ - ActivePower, - EnergyConsumption, -} -import edu.ie3.simona.model.participant.load.profile.ProfileLoadModel -import edu.ie3.simona.test.common.UnitSpec -import edu.ie3.util.TimeUtil -import edu.ie3.util.quantities.PowerSystemUnits -import edu.ie3.util.scala.quantities.{ApparentPower, Voltamperes} -import org.scalatest.prop.TableDrivenPropertyChecks -import squants.energy.{KilowattHours, Watts} -import tech.units.indriya.quantity.Quantities - -import java.util.UUID - -class ProfileLoadModelSpec extends UnitSpec with TableDrivenPropertyChecks { - - private implicit val tolerance: ApparentPower = Voltamperes(1d) - - "Having a profile load model" when { - val loadInput = - new LoadInput( - UUID.fromString("4eeaf76a-ec17-4fc3-872d-34b7d6004b03"), - "testLoad", - OperatorInput.NO_OPERATOR_ASSIGNED, - OperationTime.notLimited(), - new NodeInput( - UUID.fromString("e5c1cde5-c161-4a4f-997f-fcf31fecbf57"), - "TestNodeInputModel", - OperatorInput.NO_OPERATOR_ASSIGNED, - OperationTime.notLimited(), - Quantities.getQuantity(1d, PowerSystemUnits.PU), - false, - NodeInput.DEFAULT_GEO_POSITION, - GermanVoltageLevelUtils.LV, - -1, - ), - new CosPhiFixed("cosPhiFixed:{(0.0,0.95)}"), - null, - BdewStandardLoadProfile.H0, - false, - Quantities.getQuantity(3000d, PowerSystemUnits.KILOWATTHOUR), - Quantities.getQuantity(282.74d, PowerSystemUnits.VOLTAMPERE), - 0.95, - ) - - val simulationStartDate = - TimeUtil.withDefaults.toZonedDateTime("2019-01-01T00:00:00Z") - val simulationEndDate = - TimeUtil.withDefaults.toZonedDateTime("2019-12-31T23:59:00Z") - val foreSeenOperationInterval = - SystemComponent.determineOperationInterval( - simulationStartDate, - simulationEndDate, - loadInput.getOperationTime, - ) - - "instantiating it" should { - "deliver a proper model" in { - forAll( - Table( - ("profile", "reference", "expectedsRated"), - ( - BdewStandardLoadProfile.H0, - ActivePower(Watts(268.6)), - Voltamperes(282.74d), - ), - ( - BdewStandardLoadProfile.H0, - EnergyConsumption( - KilowattHours(3000d) - ), - Voltamperes(848.22d), - ), - ( - BdewStandardLoadProfile.L0, - ActivePower(Watts(268.6)), - Voltamperes(282.74d), - ), - ( - BdewStandardLoadProfile.L0, - EnergyConsumption( - KilowattHours(3000d) - ), - Voltamperes(759.158d), - ), - ( - BdewStandardLoadProfile.G0, - ActivePower(Watts(268.6)), - Voltamperes(282.74d), - ), - ( - BdewStandardLoadProfile.G0, - EnergyConsumption( - KilowattHours(3000d) - ), - Voltamperes(759.158d), - ), - ) - ) { (profile, reference, expectedSRated) => - val actual = ProfileLoadModel( - loadInput.copy().loadprofile(profile).build(), - foreSeenOperationInterval, - 1.0, - reference, - ) - - actual.sRated should approximate(expectedSRated) - } - } - } - } -} diff --git a/src/test/scala/edu/ie3/simona/model/participant/load/RandomLoadModelSpec.scala b/src/test/scala/edu/ie3/simona/model/participant/load/RandomLoadModelSpec.scala deleted file mode 100644 index eb57409b0d..0000000000 --- a/src/test/scala/edu/ie3/simona/model/participant/load/RandomLoadModelSpec.scala +++ /dev/null @@ -1,153 +0,0 @@ -/* - * © 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.participant.load - -import de.lmu.ifi.dbs.elki.math.statistics.distribution.GeneralizedExtremeValueDistribution -import edu.ie3.datamodel.models.OperationTime -import edu.ie3.datamodel.models.input.system.LoadInput -import edu.ie3.datamodel.models.input.system.characteristic.CosPhiFixed -import edu.ie3.datamodel.models.input.{NodeInput, OperatorInput} -import edu.ie3.datamodel.models.profile.BdewStandardLoadProfile -import edu.ie3.datamodel.models.voltagelevels.GermanVoltageLevelUtils -import edu.ie3.simona.model.SystemComponent -import edu.ie3.simona.model.participant.control.QControl -import edu.ie3.simona.model.participant.load.LoadReference.{ - ActivePower, - EnergyConsumption, -} -import edu.ie3.simona.model.participant.load.random.{ - RandomLoadModel, - RandomLoadParameters, -} -import edu.ie3.simona.test.common.UnitSpec -import edu.ie3.util.TimeUtil -import edu.ie3.util.quantities.PowerSystemUnits -import edu.ie3.util.scala.quantities.{ - ApparentPower, - Kilovoltamperes, - Voltamperes, -} -import org.scalatest.prop.TableDrivenPropertyChecks -import squants.energy.{KilowattHours, Watts} -import tech.units.indriya.quantity.Quantities - -import java.util.UUID - -class RandomLoadModelSpec extends UnitSpec with TableDrivenPropertyChecks { - implicit val tolerance: ApparentPower = Voltamperes(1d) - "Having a random load model" when { - val loadInput = - new LoadInput( - UUID.fromString("4eeaf76a-ec17-4fc3-872d-34b7d6004b03"), - "testLoad", - OperatorInput.NO_OPERATOR_ASSIGNED, - OperationTime.notLimited(), - new NodeInput( - UUID.fromString("e5c1cde5-c161-4a4f-997f-fcf31fecbf57"), - "TestNodeInputModel", - OperatorInput.NO_OPERATOR_ASSIGNED, - OperationTime.notLimited(), - Quantities.getQuantity(1d, PowerSystemUnits.PU), - false, - NodeInput.DEFAULT_GEO_POSITION, - GermanVoltageLevelUtils.LV, - -1, - ), - new CosPhiFixed("cosPhiFixed:{(0.0,0.95)}"), - null, - BdewStandardLoadProfile.H0, - false, - Quantities.getQuantity(3000d, PowerSystemUnits.KILOWATTHOUR), - Quantities.getQuantity(282.74d, PowerSystemUnits.VOLTAMPERE), - 0.95, - ) - - val simulationStartDate = - TimeUtil.withDefaults.toZonedDateTime("2019-01-01T00:00:00Z") - val simulationEndDate = - TimeUtil.withDefaults.toZonedDateTime("2019-12-31T23:59:00Z") - val foreSeenOperationInterval = - SystemComponent.determineOperationInterval( - simulationStartDate, - simulationEndDate, - loadInput.getOperationTime, - ) - - "instantiating it" should { - "deliver a proper model" in { - - val testData = Table( - ("reference", "expectedSRated"), - (ActivePower(Watts(268.6)), Voltamperes(311.0105263157895d)), - (EnergyConsumption(KilowattHours(2000d)), Voltamperes(513.871737d)), - ) - - forAll(testData) { (reference, expectedSRated: ApparentPower) => - val actual = RandomLoadModel( - loadInput, - foreSeenOperationInterval, - 1.0, - reference, - ) - - actual.sRated should approximate(expectedSRated) - } - } - } - - "calculating results" should { - "deliver the correct distribution on request" in { - val dut = new RandomLoadModel( - loadInput.getUuid, - loadInput.getId, - foreSeenOperationInterval, - QControl.apply(loadInput.getqCharacteristics()), - Kilovoltamperes( - loadInput - .getsRated() - .to(PowerSystemUnits.KILOVOLTAMPERE) - .getValue - .doubleValue() - ), - loadInput.getCosPhiRated, - ActivePower(Watts(268.6)), - ) - /* Working day, 61st quarter-hour */ - val queryDate = - TimeUtil.withDefaults.toZonedDateTime("2019-07-19T15:21:00Z") - val expectedParams = new RandomLoadParameters( - 0.405802458524704, - 0.0671483352780342, - 0.0417016632854939, - ) - - /* First query leeds to generation of distribution */ - val getGevDistribution = - PrivateMethod[GeneralizedExtremeValueDistribution]( - Symbol("getGevDistribution") - ) - - def firstHit = dut invokePrivate getGevDistribution(queryDate) - - firstHit.getK shouldBe expectedParams.k - firstHit.getMu shouldBe expectedParams.my - firstHit.getSigma shouldBe expectedParams.sigma - - /* Second query is only look up in storage */ - def secondHit = dut invokePrivate getGevDistribution(queryDate) - - secondHit shouldBe firstHit - } - } - } -} - -object RandomLoadModelSpec { - def get95Quantile[V](sortedArray: Array[V]): V = sortedArray( - (sortedArray.length * 0.95).toInt - ) -} diff --git a/src/test/scala/edu/ie3/simona/model/participant2/FixedFeedInModelSpec.scala b/src/test/scala/edu/ie3/simona/model/participant2/FixedFeedInModelSpec.scala new file mode 100644 index 0000000000..ef42665735 --- /dev/null +++ b/src/test/scala/edu/ie3/simona/model/participant2/FixedFeedInModelSpec.scala @@ -0,0 +1,55 @@ +/* + * © 2020. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.model.participant2 + +import edu.ie3.simona.model.participant.control.QControl +import edu.ie3.simona.model.participant2.ParticipantModel.FixedState +import edu.ie3.simona.test.common.UnitSpec +import edu.ie3.simona.test.common.input.FixedFeedInputTestData +import edu.ie3.util.quantities.PowerSystemUnits +import edu.ie3.util.quantities.PowerSystemUnits.MEGAVOLTAMPERE +import edu.ie3.util.scala.quantities.{Kilovoltamperes, Megavoltamperes} + +class FixedFeedInModelSpec extends UnitSpec with FixedFeedInputTestData { + + "The fixed feed in model" should { + + "build a correct FixedFeedModel from valid input" in { + + val model = FixedFeedInModel(fixedFeedInput) + + model.uuid shouldBe fixedFeedInput.getUuid + model.sRated shouldBe Megavoltamperes( + fixedFeedInput.getsRated().to(MEGAVOLTAMPERE).getValue.doubleValue + ) + model.cosPhiRated shouldBe fixedFeedInput.getCosPhiRated + model.qControl shouldBe QControl(fixedFeedInput.getqCharacteristics) + + } + + "return approximately correct power calculations" in { + + val model = FixedFeedInModel(fixedFeedInput) + + val expectedPower = Kilovoltamperes( + fixedFeedInput + .getsRated() + .to(PowerSystemUnits.KILOWATT) + .getValue + .doubleValue * -1 + ).toActivePower(fixedFeedInput.getCosPhiRated) + + val (operatingPoint, nextTick) = + model.determineOperatingPoint(FixedState(0)) + operatingPoint.activePower shouldBe expectedPower + nextTick shouldBe None + + } + + } + +} diff --git a/src/test/scala/edu/ie3/simona/model/participant2/load/FixedLoadModelSpec.scala b/src/test/scala/edu/ie3/simona/model/participant2/load/FixedLoadModelSpec.scala new file mode 100644 index 0000000000..65f200234a --- /dev/null +++ b/src/test/scala/edu/ie3/simona/model/participant2/load/FixedLoadModelSpec.scala @@ -0,0 +1,55 @@ +/* + * © 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.participant2.load + +import edu.ie3.simona.config.RuntimeConfig.LoadRuntimeConfig +import edu.ie3.simona.model.participant2.ParticipantModel.FixedState +import edu.ie3.simona.test.common.input.LoadInputTestData +import edu.ie3.simona.test.common.UnitSpec +import squants.Power +import squants.energy.Watts + +class FixedLoadModelSpec extends UnitSpec with LoadInputTestData { + + private implicit val tolerance: Power = Watts(1e-2) + + "A fixed load model" should { + + "return the desired power in 1,000 calculations" in { + + val cases = Table( + ("reference", "expectedPower"), + ("power", Watts(268.6)), + ("energy", Watts(342.47)), + ) + + forAll(cases) { (reference, expectedPower) => + val config = LoadRuntimeConfig( + modelBehaviour = "fixed", + reference = reference, + ) + + val model = FixedLoadModel( + loadInput, + config, + ) + + (0 until 1000).foreach { tick => + val (operatingPoint, nextTick) = model.determineOperatingPoint( + FixedState(tick) + ) + + operatingPoint.activePower should approximate(expectedPower) + operatingPoint.reactivePower shouldBe None + nextTick shouldBe None + } + + } + } + + } +} diff --git a/src/test/scala/edu/ie3/simona/model/participant2/load/LoadModelTestHelper.scala b/src/test/scala/edu/ie3/simona/model/participant2/load/LoadModelTestHelper.scala new file mode 100644 index 0000000000..d0da823c14 --- /dev/null +++ b/src/test/scala/edu/ie3/simona/model/participant2/load/LoadModelTestHelper.scala @@ -0,0 +1,74 @@ +/* + * © 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.participant2.load + +import edu.ie3.simona.model.participant2.ParticipantModel.{ + ActivePowerOperatingPoint, + DateTimeState, +} +import squants.{Dimensionless, Each, Energy, Power, Quantity} +import squants.energy.KilowattHours +import squants.time.Minutes + +import java.time.ZonedDateTime +import java.time.temporal.ChronoUnit + +trait LoadModelTestHelper { + + protected def calculateEnergyDiffForYear( + model: LoadModel[DateTimeState], + simulationStartDate: ZonedDateTime, + expectedEnergy: Energy, + ): Dimensionless = { + val duration = Minutes(15d) + + val avgEnergy = calculatePowerForYear( + model, + simulationStartDate, + ).foldLeft(KilowattHours(0)) { case (energySum, power) => + energySum + (power * duration) + } + + getRelativeDifference( + avgEnergy, + expectedEnergy, + ) + } + + protected def calculatePowerForYear( + model: LoadModel[DateTimeState], + simulationStartDate: ZonedDateTime, + ): Iterable[Power] = { + val quarterHoursInYear = 365L * 96L + + (0L until quarterHoursInYear) + .map { quarterHour => + val tick = quarterHour * 15 * 60 + val state = DateTimeState( + tick, + simulationStartDate.plus(quarterHour * 15, ChronoUnit.MINUTES), + ) + + model + .determineOperatingPoint(state) match { + case (ActivePowerOperatingPoint(p), _) => + p + } + } + } + + protected def getRelativeDifference[Q <: Quantity[Q]]( + actualResult: Q, + expectedResult: Q, + ): Dimensionless = + Each((expectedResult - actualResult).abs / expectedResult) + + protected def get95Quantile[V](sortedArray: Array[V]): V = sortedArray( + (sortedArray.length * 0.95).toInt + ) + +} diff --git a/src/test/scala/edu/ie3/simona/model/participant/load/LoadProfileStoreSpec.scala b/src/test/scala/edu/ie3/simona/model/participant2/load/profile/LoadProfileStoreSpec.scala similarity index 95% rename from src/test/scala/edu/ie3/simona/model/participant/load/LoadProfileStoreSpec.scala rename to src/test/scala/edu/ie3/simona/model/participant2/load/profile/LoadProfileStoreSpec.scala index 6b5d7ee5b2..3618f2674b 100644 --- a/src/test/scala/edu/ie3/simona/model/participant/load/LoadProfileStoreSpec.scala +++ b/src/test/scala/edu/ie3/simona/model/participant2/load/profile/LoadProfileStoreSpec.scala @@ -4,19 +4,13 @@ * Research group Distribution grid planning and operation */ -package edu.ie3.simona.model.participant.load +package edu.ie3.simona.model.participant2.load.profile import com.typesafe.scalalogging.LazyLogging import edu.ie3.datamodel.models.profile.BdewStandardLoadProfile._ import edu.ie3.datamodel.models.profile.StandardLoadProfile -import edu.ie3.simona.model.participant.load.profile.{ - LoadProfileKey, - LoadProfileStore, - TypeDayProfile, -} import edu.ie3.simona.test.common.UnitSpec import edu.ie3.util.TimeUtil -import edu.ie3.util.scala.quantities.Voltamperes import org.scalatest.PrivateMethodTester import org.scalatest.prop.TableDrivenPropertyChecks import squants.energy.{KilowattHours, Watts} diff --git a/src/test/scala/edu/ie3/simona/model/participant2/load/profile/ProfileLoadModelSpec.scala b/src/test/scala/edu/ie3/simona/model/participant2/load/profile/ProfileLoadModelSpec.scala new file mode 100644 index 0000000000..85ee997600 --- /dev/null +++ b/src/test/scala/edu/ie3/simona/model/participant2/load/profile/ProfileLoadModelSpec.scala @@ -0,0 +1,164 @@ +/* + * © 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.participant2.load.profile + +import edu.ie3.datamodel.models.profile.BdewStandardLoadProfile._ +import edu.ie3.simona.config.RuntimeConfig.LoadRuntimeConfig +import edu.ie3.simona.model.participant2.load.LoadModelTestHelper +import edu.ie3.simona.test.common.UnitSpec +import edu.ie3.simona.test.common.input.LoadInputTestData +import edu.ie3.simona.test.matchers.DoubleMatchers +import edu.ie3.util.TimeUtil +import edu.ie3.util.quantities.PowerSystemUnits +import edu.ie3.util.scala.quantities.{ + ApparentPower, + Kilovoltamperes, + Voltamperes, +} +import squants.Percent +import squants.energy.{KilowattHours, Power, Watts} +import tech.units.indriya.quantity.Quantities + +class ProfileLoadModelSpec + extends UnitSpec + with DoubleMatchers + with LoadModelTestHelper + with LoadInputTestData { + + private implicit val powerTolerance: ApparentPower = Voltamperes(1e-2) + private implicit val doubleTolerance: Double = 1e-6 + + private val simulationStartDate = + TimeUtil.withDefaults.toZonedDateTime("2022-01-01T00:00:00Z") + + "A profile load model" should { + + "be instantiated correctly with power reference" in { + + forAll( + Table( + ("profile", "sRated", "expectedScalingFactor"), + (H0, 282.736842, 1.0), + (H0, 1000.0, 3.536858), + (L0, 253.052632, 1.0), + (L0, 1000.0, 3.951747), + (G0, 253.052632, 1.0), + (G0, 1000.0, 3.951747), + ) + ) { (profile, sRated, expectedScalingFactor) => + val config = LoadRuntimeConfig(modelBehaviour = "profile") + val model = ProfileLoadModel( + loadInput + .copy() + .loadprofile(profile) + .sRated(Quantities.getQuantity(sRated, PowerSystemUnits.VOLTAMPERE)) + .build(), + config, + ) + + model.referenceScalingFactor should approximate(expectedScalingFactor) + } + } + + "be instantiated correctly with energy reference" in { + + forAll( + Table( + ("profile", "eConsAnnual", "expectedScalingFactor", "expectedSRated"), + (H0, 1000.0, 1.0, 282.74), + (H0, 3000.0, 3.0, 848.22), + (L0, 1000.0, 1.0, 253.053), + (L0, 3000.0, 3.0, 759.158), + (G0, 1000.0, 1.0, 253.053), + (G0, 3000.0, 3.0, 759.158), + ) + ) { (profile, eConsAnnual, expectedScalingFactor, expectedSRated) => + val config = LoadRuntimeConfig( + modelBehaviour = "profile", + reference = "energy", + ) + val model = ProfileLoadModel( + loadInput + .copy() + .loadprofile(profile) + .eConsAnnual( + Quantities.getQuantity(eConsAnnual, PowerSystemUnits.KILOWATTHOUR) + ) + .build(), + config, + ) + + model.referenceScalingFactor should approximate(expectedScalingFactor) + model.sRated should approximate(Voltamperes(expectedSRated)) + } + } + + "reach the targeted annual energy consumption in a simulated year" in { + forAll( + Table("profile", H0, L0, G0) + ) { profile => + val input = loadInput.copy().loadprofile(profile).build() + val config = LoadRuntimeConfig( + modelBehaviour = "profile", + reference = "energy", + ) + + val targetEnergyConsumption = KilowattHours( + loadInput + .geteConsAnnual() + .to(PowerSystemUnits.KILOWATTHOUR) + .getValue + .doubleValue + ) + + val model = ProfileLoadModel(input, config) + + /* Test against a permissible deviation of 2 %. As per official documentation of the bdew load profiles + * [https://www.bdew.de/media/documents/2000131_Anwendung-repraesentativen_Lastprofile-Step-by-step.pdf], 1.5 % + * are officially permissible. But, as we currently do not take (bank) holidays into account, we cannot reach + * this accuracy. */ + + calculateEnergyDiffForYear( + model, + simulationStartDate, + targetEnergyConsumption, + ) should be < Percent(2) + } + } + + "approximately reach the maximum power in a simulated year" in { + forAll( + Table("profile", H0, L0, G0) + ) { profile => + val input = loadInput.copy().loadprofile(profile).build() + val config = LoadRuntimeConfig(modelBehaviour = "profile") + + val model = ProfileLoadModel(input, config) + + val targetMaximumPower = Kilovoltamperes( + input + .getsRated() + .to(PowerSystemUnits.KILOVOLTAMPERE) + .getValue + .doubleValue + ).toActivePower(input.getCosPhiRated) + + val maximumPower = calculatePowerForYear( + model, + simulationStartDate, + ).maxOption.value + + // the maximum value depends on the year of the simulation, + // since the maximum value for h0 will be reached on Saturdays in the winter + // and since the dynamization function reaches its maximum on day 366 (leap year) + implicit val tolerance: Power = Watts(1) + maximumPower should approximate(targetMaximumPower) + } + } + + } +} diff --git a/src/test/scala/edu/ie3/simona/model/participant/load/TypeDayProfileSpec.scala b/src/test/scala/edu/ie3/simona/model/participant2/load/profile/TypeDayProfileSpec.scala similarity index 92% rename from src/test/scala/edu/ie3/simona/model/participant/load/TypeDayProfileSpec.scala rename to src/test/scala/edu/ie3/simona/model/participant2/load/profile/TypeDayProfileSpec.scala index 4e94953967..d18bf95a98 100644 --- a/src/test/scala/edu/ie3/simona/model/participant/load/TypeDayProfileSpec.scala +++ b/src/test/scala/edu/ie3/simona/model/participant2/load/profile/TypeDayProfileSpec.scala @@ -4,9 +4,8 @@ * Research group Distribution grid planning and operation */ -package edu.ie3.simona.model.participant.load +package edu.ie3.simona.model.participant2.load.profile -import edu.ie3.simona.model.participant.load.profile.TypeDayProfile import edu.ie3.simona.test.common.UnitSpec import edu.ie3.util.TimeUtil diff --git a/src/test/scala/edu/ie3/simona/model/participant2/load/random/RandomLoadModelSpec.scala b/src/test/scala/edu/ie3/simona/model/participant2/load/random/RandomLoadModelSpec.scala new file mode 100644 index 0000000000..f6c5e0a791 --- /dev/null +++ b/src/test/scala/edu/ie3/simona/model/participant2/load/random/RandomLoadModelSpec.scala @@ -0,0 +1,181 @@ +/* + * © 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.participant2.load.random + +import de.lmu.ifi.dbs.elki.math.statistics.distribution.GeneralizedExtremeValueDistribution +import edu.ie3.simona.config.RuntimeConfig.LoadRuntimeConfig +import edu.ie3.simona.model.participant2.load.LoadModelTestHelper +import edu.ie3.simona.test.common.UnitSpec +import edu.ie3.simona.test.common.input.LoadInputTestData +import edu.ie3.util.TimeUtil +import edu.ie3.util.quantities.PowerSystemUnits +import edu.ie3.util.scala.quantities.{ + ApparentPower, + Kilovoltamperes, + Voltamperes, +} +import squants.Percent +import squants.energy.KilowattHours +import tech.units.indriya.quantity.Quantities + +class RandomLoadModelSpec + extends UnitSpec + with LoadModelTestHelper + with LoadInputTestData { + + implicit val powerTolerance: ApparentPower = Voltamperes(1e-2) + private implicit val doubleTolerance: Double = 1e-6 + + private val simulationStartDate = + TimeUtil.withDefaults.toZonedDateTime("2019-01-01T00:00:00Z") + + "A random load model" should { + + "be instantiated correctly with power reference" in { + + forAll( + Table( + ("sRated", "expectedScalingFactor"), + (167.368421, 1.0), + (1000.0, 5.9748428), + ) + ) { (sRated, expectedScalingFactor) => + val config = LoadRuntimeConfig(modelBehaviour = "random") + val model = RandomLoadModel( + loadInput + .copy() + .sRated(Quantities.getQuantity(sRated, PowerSystemUnits.VOLTAMPERE)) + .build(), + config, + ) + + model.referenceScalingFactor should approximate(expectedScalingFactor) + } + } + + "be instantiated correctly with energy reference" in { + + forAll( + Table( + ("eConsAnnual", "expectedScalingFactor", "expectedSRated"), + (1000.0, 1.3955921, 256.936), + (2000.0, 2.7911842, 513.8717), + (3000.0, 4.1867763, 770.808), + ) + ) { (eConsAnnual, expectedScalingFactor, expectedSRated) => + val config = LoadRuntimeConfig( + modelBehaviour = "random", + reference = "energy", + ) + val model = RandomLoadModel( + loadInput + .copy() + .eConsAnnual( + Quantities.getQuantity(eConsAnnual, PowerSystemUnits.KILOWATTHOUR) + ) + .build(), + config, + ) + + model.referenceScalingFactor should approximate(expectedScalingFactor) + model.sRated should approximate(Voltamperes(expectedSRated)) + } + } + + "deliver the correct distribution on request" in { + val model = RandomLoadModel( + loadInput, + LoadRuntimeConfig( + modelBehaviour = "random", + reference = "energy", + ), + ) + + /* Working day, 61st quarter-hour */ + val queryDate = + TimeUtil.withDefaults.toZonedDateTime("2019-07-19T15:21:00Z") + val expectedParams = new RandomLoadParameters( + 0.405802458524704, + 0.0671483352780342, + 0.0417016632854939, + ) + + /* First query leeds to generation of distribution */ + val getGevDistribution = + PrivateMethod[GeneralizedExtremeValueDistribution]( + Symbol("getGevDistribution") + ) + + def firstHit = model invokePrivate getGevDistribution(queryDate) + + firstHit.getK shouldBe expectedParams.k + firstHit.getMu shouldBe expectedParams.my + firstHit.getSigma shouldBe expectedParams.sigma + + /* Second query is only look up in storage */ + def secondHit = model invokePrivate getGevDistribution(queryDate) + + secondHit shouldBe firstHit + } + + "reach the targeted annual energy consumption in a simulated year" in { + val config = LoadRuntimeConfig( + modelBehaviour = "random", + reference = "energy", + ) + + val model = RandomLoadModel( + loadInput, + config, + ) + + val targetEnergyConsumption = KilowattHours( + loadInput + .geteConsAnnual() + .to(PowerSystemUnits.KILOWATTHOUR) + .getValue + .doubleValue + ) + + calculateEnergyDiffForYear( + model, + simulationStartDate, + targetEnergyConsumption, + ) should be < Percent(1d) + } + + "approximately reach the maximum power in a simulated year" in { + val config = LoadRuntimeConfig(modelBehaviour = "random") + + val model = RandomLoadModel( + loadInput, + config, + ) + + val targetMaximumPower = Kilovoltamperes( + loadInput + .getsRated() + .to(PowerSystemUnits.KILOVOLTAMPERE) + .getValue + .doubleValue + ).toActivePower(loadInput.getCosPhiRated) + + val powers = calculatePowerForYear( + model, + simulationStartDate, + ).toIndexedSeq.sorted.toArray + + val quantile95 = get95Quantile(powers) + + getRelativeDifference( + quantile95, + targetMaximumPower, + ) should be < Percent(2d) + } + + } +} diff --git a/src/test/scala/edu/ie3/simona/model/participant/load/RandomLoadParamStoreSpec.scala b/src/test/scala/edu/ie3/simona/model/participant2/load/random/RandomLoadParamStoreSpec.scala similarity index 95% rename from src/test/scala/edu/ie3/simona/model/participant/load/RandomLoadParamStoreSpec.scala rename to src/test/scala/edu/ie3/simona/model/participant2/load/random/RandomLoadParamStoreSpec.scala index 5f214e0796..9aecb87a0c 100644 --- a/src/test/scala/edu/ie3/simona/model/participant/load/RandomLoadParamStoreSpec.scala +++ b/src/test/scala/edu/ie3/simona/model/participant2/load/random/RandomLoadParamStoreSpec.scala @@ -4,20 +4,16 @@ * Research group Distribution grid planning and operation */ -package edu.ie3.simona.model.participant.load +package edu.ie3.simona.model.participant2.load.random -import java.io.InputStreamReader - -import edu.ie3.simona.model.participant.load.random.{ - RandomLoadParamStore, - RandomLoadParameters, - TypeDayParameters, -} +import edu.ie3.simona.model.participant2.load.DayType import edu.ie3.simona.test.common.UnitSpec import edu.ie3.util.TimeUtil import org.scalatest.PrivateMethodTester import org.scalatest.prop.TableDrivenPropertyChecks +import java.io.InputStreamReader + class RandomLoadParamStoreSpec extends UnitSpec with PrivateMethodTester diff --git a/src/test/scala/edu/ie3/simona/model/participant/load/TypeDayParametersSpec.scala b/src/test/scala/edu/ie3/simona/model/participant2/load/random/TypeDayParametersSpec.scala similarity index 92% rename from src/test/scala/edu/ie3/simona/model/participant/load/TypeDayParametersSpec.scala rename to src/test/scala/edu/ie3/simona/model/participant2/load/random/TypeDayParametersSpec.scala index 84f1d45e71..61d69135da 100644 --- a/src/test/scala/edu/ie3/simona/model/participant/load/TypeDayParametersSpec.scala +++ b/src/test/scala/edu/ie3/simona/model/participant2/load/random/TypeDayParametersSpec.scala @@ -4,12 +4,8 @@ * Research group Distribution grid planning and operation */ -package edu.ie3.simona.model.participant.load +package edu.ie3.simona.model.participant2.load.random -import edu.ie3.simona.model.participant.load.random.{ - RandomLoadParameters, - TypeDayParameters, -} import edu.ie3.simona.test.common.UnitSpec import edu.ie3.util.TimeUtil diff --git a/src/test/scala/edu/ie3/simona/test/common/ConfigTestData.scala b/src/test/scala/edu/ie3/simona/test/common/ConfigTestData.scala index c046493ca6..5d6babf38c 100644 --- a/src/test/scala/edu/ie3/simona/test/common/ConfigTestData.scala +++ b/src/test/scala/edu/ie3/simona/test/common/ConfigTestData.scala @@ -9,7 +9,6 @@ package edu.ie3.simona.test.common import org.apache.pekko.actor.ActorRef import com.typesafe.config.{Config, ConfigFactory} import edu.ie3.simona.config.SimonaConfig -import edu.ie3.simona.event.listener.SimonaListenerCompanion /** Simple (empty) configuration data. Furthermore, it would make sense to * implement another class which reads a config and provides config based @@ -150,6 +149,4 @@ trait ConfigTestData { protected val simonaConfig: SimonaConfig = SimonaConfig(typesafeConfig) protected val listener: Iterable[ActorRef] = Iterable.empty[ActorRef] - protected val listenerSingletonCompanions = - Map.empty[SimonaListenerCompanion, Option[List[String]]] } diff --git a/src/test/scala/edu/ie3/simona/test/common/DefaultTestData.scala b/src/test/scala/edu/ie3/simona/test/common/DefaultTestData.scala index 0c026e2f66..5489a4e60d 100644 --- a/src/test/scala/edu/ie3/simona/test/common/DefaultTestData.scala +++ b/src/test/scala/edu/ie3/simona/test/common/DefaultTestData.scala @@ -11,7 +11,10 @@ import edu.ie3.datamodel.models.OperationTime import edu.ie3.simona.config.SimonaConfig import edu.ie3.simona.model.SystemComponent import edu.ie3.simona.model.grid.{RefSystem, VoltageLimits} -import edu.ie3.simona.model.participant.load.{LoadModelBehaviour, LoadReference} +import edu.ie3.simona.model.participant2.load.{ + LoadModelBehaviour, + LoadReferenceType, +} import edu.ie3.util.scala.OperationInterval import org.locationtech.jts.geom.{Coordinate, GeometryFactory, Point} import squants.electro.Kilovolts @@ -76,8 +79,8 @@ trait DefaultTestData { * Suitable configuration */ def createSimonaConfig( - modelBehaviour: LoadModelBehaviour.Value, - reference: LoadReference, + modelBehaviour: LoadModelBehaviour.Value = LoadModelBehaviour.FIX, + reference: LoadReferenceType.Value = LoadReferenceType.ACTIVE_POWER, ): SimonaConfig = { val typesafeConfig: Config = ConfigFactory.parseString( s""" @@ -118,7 +121,7 @@ trait DefaultTestData { | uuids = ["4eeaf76a-ec17-4fc3-872d-34b7d6004b03"] | scaling = 1.0 | modelBehaviour = "${modelBehaviour.toString}" - | reference = "${reference.key}" + | reference = "${reference.toString}" | } | ] |} diff --git a/src/test/scala/edu/ie3/simona/test/common/UnitSpec.scala b/src/test/scala/edu/ie3/simona/test/common/UnitSpec.scala index 5cbf725051..b9ad362047 100644 --- a/src/test/scala/edu/ie3/simona/test/common/UnitSpec.scala +++ b/src/test/scala/edu/ie3/simona/test/common/UnitSpec.scala @@ -7,7 +7,11 @@ package edu.ie3.simona.test.common import com.typesafe.scalalogging.LazyLogging -import edu.ie3.simona.test.matchers.{QuantityMatchers, SquantsMatchers} +import edu.ie3.simona.test.matchers.{ + DoubleMatchers, + QuantityMatchers, + SquantsMatchers, +} import edu.ie3.util.scala.quantities.{QuantityUtil => PSQuantityUtil} import org.apache.pekko.actor.testkit.typed.scaladsl.LogCapturing import org.scalatest._ @@ -29,6 +33,7 @@ trait UnitSpec extends should.Matchers with QuantityMatchers with SquantsMatchers + with DoubleMatchers with AnyWordSpecLike with LogCapturing with OptionValues diff --git a/src/test/scala/edu/ie3/simona/test/common/input/EmInputTestData.scala b/src/test/scala/edu/ie3/simona/test/common/input/EmInputTestData.scala index 269f979540..a6be02525f 100644 --- a/src/test/scala/edu/ie3/simona/test/common/input/EmInputTestData.scala +++ b/src/test/scala/edu/ie3/simona/test/common/input/EmInputTestData.scala @@ -27,10 +27,8 @@ import edu.ie3.datamodel.models.{OperationTime, StandardUnits} import edu.ie3.simona.config.RuntimeConfig.EmRuntimeConfig import edu.ie3.simona.config.SimonaConfig import edu.ie3.simona.event.notifier.NotifierConfig -import edu.ie3.simona.model.participant.load.{LoadModelBehaviour, LoadReference} import edu.ie3.simona.util.ConfigUtil import edu.ie3.util.quantities.PowerSystemUnits._ -import squants.energy.Kilowatts import tech.units.indriya.quantity.Quantities import java.util.UUID @@ -90,16 +88,10 @@ trait EmInputTestData householdStorageTypeInput, ) - protected val simonaConfig: SimonaConfig = - createSimonaConfig( - LoadModelBehaviour.FIX, - LoadReference.ActivePower( - Kilowatts(0.0) - ), - ) + protected val simonaConfig: SimonaConfig = createSimonaConfig() - private val configUtil = ConfigUtil.ParticipantConfigUtil( - simonaConfig.simona.runtime.participant + private val configUtil = ConfigUtil.EmConfigUtil( + simonaConfig.simona.runtime.em ) protected val defaultOutputConfig: NotifierConfig = @@ -110,9 +102,7 @@ trait EmInputTestData ) protected val modelConfig: EmRuntimeConfig = - configUtil.getOrDefault[EmRuntimeConfig]( - emInput.getUuid - ) + configUtil.getOrDefault(emInput.getUuid) protected val adaptedTypeInput = new HpTypeInput( UUID.fromString("9802bf35-2a4e-4ff5-be9b-cd9e6a78dcd6"), diff --git a/src/test/scala/edu/ie3/simona/test/common/input/LoadInputTestData.scala b/src/test/scala/edu/ie3/simona/test/common/input/LoadInputTestData.scala index bb02864013..ba850946ae 100644 --- a/src/test/scala/edu/ie3/simona/test/common/input/LoadInputTestData.scala +++ b/src/test/scala/edu/ie3/simona/test/common/input/LoadInputTestData.scala @@ -15,25 +15,23 @@ import edu.ie3.datamodel.models.profile.BdewStandardLoadProfile import edu.ie3.util.quantities.PowerSystemUnits.{KILOWATTHOUR, VOLTAMPERE} import tech.units.indriya.quantity.Quantities -/** //ToDo: Class Description - * - * @version 0.1 - * @since 23.06.20 +/** Exemplary instances of [[LoadInput]] to be used in tests */ trait LoadInputTestData extends NodeInputTestData { - val loadInput = - new LoadInput( - UUID.fromString("4eeaf76a-ec17-4fc3-872d-34b7d6004b03"), - "testLoad", - OperatorInput.NO_OPERATOR_ASSIGNED, - OperationTime.notLimited(), - nodeInputNoSlackNs04KvA, - new CosPhiFixed("cosPhiFixed:{(0.0,0.95)}"), - null, - BdewStandardLoadProfile.H0, - false, - Quantities.getQuantity(3000d, KILOWATTHOUR), - Quantities.getQuantity(282.74d, VOLTAMPERE), - 0.95, - ) + + protected val loadInput = new LoadInput( + UUID.fromString("4eeaf76a-ec17-4fc3-872d-34b7d6004b03"), + "testLoad", + OperatorInput.NO_OPERATOR_ASSIGNED, + OperationTime.notLimited(), + nodeInputNoSlackNs04KvA, + new CosPhiFixed("cosPhiFixed:{(0.0,0.95)}"), + null, + BdewStandardLoadProfile.H0, + false, + Quantities.getQuantity(3000d, KILOWATTHOUR), + Quantities.getQuantity(282.74d, VOLTAMPERE), + 0.95, + ) + } diff --git a/src/test/scala/edu/ie3/simona/test/matchers/DoubleMatchers.scala b/src/test/scala/edu/ie3/simona/test/matchers/DoubleMatchers.scala new file mode 100644 index 0000000000..fe9b7a2b16 --- /dev/null +++ b/src/test/scala/edu/ie3/simona/test/matchers/DoubleMatchers.scala @@ -0,0 +1,28 @@ +/* + * © 2024. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.test.matchers + +import org.scalactic.TolerantNumerics +import org.scalatest.matchers.{MatchResult, Matcher} + +trait DoubleMatchers extends TolerantNumerics { + + class DoubleMatcher(right: Double, implicit val tolerance: Double) + extends Matcher[Double] { + private val equality = tolerantDoubleEquality(tolerance) + + override def apply(left: Double): MatchResult = MatchResult( + equality.areEqual(left, right), + s"The values $left and $right differ more than $tolerance in value", + s"The values $left and $right differ less than $tolerance in value", + ) + } + + def approximate(right: Double)(implicit tolerance: Double) = + new DoubleMatcher(right, tolerance) + +} diff --git a/src/test/scala/edu/ie3/simona/util/ConfigUtilSpec.scala b/src/test/scala/edu/ie3/simona/util/ConfigUtilSpec.scala index 5b345e2d61..881d0aaaa1 100644 --- a/src/test/scala/edu/ie3/simona/util/ConfigUtilSpec.scala +++ b/src/test/scala/edu/ie3/simona/util/ConfigUtilSpec.scala @@ -27,6 +27,7 @@ import edu.ie3.simona.exceptions.InvalidConfigParameterException import edu.ie3.simona.test.common.{ConfigTestData, UnitSpec} import edu.ie3.simona.util.ConfigUtil.NotifierIdentifier._ import edu.ie3.simona.util.ConfigUtil.{ + EmConfigUtil, GridOutputConfigUtil, NotifierIdentifier, ParticipantConfigUtil, @@ -40,6 +41,77 @@ class ConfigUtilSpec extends UnitSpec with TableDrivenPropertyChecks with ConfigTestData { + + "The em config util" should { + "be created correctly with valid data" in { + val emRuntimeConfig = ConfigFactory.parseString( + """simona.runtime.em = { + | defaultConfig = { + | uuids = ["default"] + | scaling = 1.0 + | } + | individualConfigs = [] + |}""".stripMargin + ) + val config = + emRuntimeConfig.withFallback(typesafeConfig).resolve() + val simonaConfig = SimonaConfig(config) + + val actual = ConfigUtil.EmConfigUtil( + simonaConfig.simona.runtime.em + ) + + inside(actual) { case EmConfigUtil(configs, defaultConfigs) => + configs shouldBe Map.empty[UUID, EmRuntimeConfig] + + defaultConfigs.calculateMissingReactivePowerWithModel shouldBe false + defaultConfigs.scaling shouldBe 1.0 + defaultConfigs.uuids shouldBe List("default") + defaultConfigs.aggregateFlex shouldBe "SELF_OPT_EXCL_REG" + defaultConfigs.curtailRegenerative shouldBe false + } + } + + "be created correctly with one UUID correctly" in { + val emRuntimeConfig = ConfigFactory.parseString( + """simona.runtime.em = { + | defaultConfig = { + | uuids = ["default"] + | scaling = 1.0 + | } + | individualConfigs = [ + | { + | calculateMissingReactivePowerWithModel = false + | uuids = ["49f250fa-41ff-4434-a083-79c98d260a76"] + | scaling = 1.3 + | curtailRegenerative = true + | } + | ] + |}""".stripMargin + ) + val config = + emRuntimeConfig.withFallback(typesafeConfig).resolve() + val simonaConfig = SimonaConfig(config) + + val actual = ConfigUtil.EmConfigUtil( + simonaConfig.simona.runtime.em + ) + + inside(actual) { case EmConfigUtil(configs, _) => + configs.size shouldBe 1 + + val individual = + configs(UUID.fromString("49f250fa-41ff-4434-a083-79c98d260a76")) + + individual.calculateMissingReactivePowerWithModel shouldBe false + individual.scaling shouldBe 1.3 + individual.uuids shouldBe List("49f250fa-41ff-4434-a083-79c98d260a76") + individual.aggregateFlex shouldBe "SELF_OPT_EXCL_REG" + individual.curtailRegenerative shouldBe true + } + } + } + "The participant config util" should { "setup correctly with valid load data" in { val loadRuntimeConfig = ConfigFactory.parseString( @@ -63,7 +135,7 @@ class ConfigUtilSpec inside(actual) { case ParticipantConfigUtil(configs, defaultConfigs) => configs shouldBe Map.empty[UUID, LoadRuntimeConfig] - defaultConfigs.size shouldBe 8 + defaultConfigs.size shouldBe 7 inside(defaultConfigs.get(classOf[LoadRuntimeConfig])) { case Some( @@ -123,7 +195,7 @@ class ConfigUtilSpec UUID.fromString("49f250fa-41ff-4434-a083-79c98d260a76") ) - defaultConfigs.size shouldBe 8 + defaultConfigs.size shouldBe 7 inside(defaultConfigs.get(classOf[LoadRuntimeConfig])) { case Some( LoadRuntimeConfig( @@ -335,7 +407,7 @@ class ConfigUtilSpec UUID.fromString("49f250fa-41ff-4434-a083-79c98d260a76") ) - defaultConfigs.size shouldBe 8 + defaultConfigs.size shouldBe 7 inside(defaultConfigs.get(classOf[FixedFeedInRuntimeConfig])) { case Some( FixedFeedInRuntimeConfig( diff --git a/src/test/scala/edu/ie3/simona/util/TickUtilSpec.scala b/src/test/scala/edu/ie3/simona/util/TickUtilSpec.scala new file mode 100644 index 0000000000..7b26ee47be --- /dev/null +++ b/src/test/scala/edu/ie3/simona/util/TickUtilSpec.scala @@ -0,0 +1,51 @@ +/* + * © 2024. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.util + +import edu.ie3.simona.test.common.UnitSpec +import edu.ie3.util.TimeUtil + +class TickUtilSpec extends UnitSpec { + "TimeUtil" should { + "round ticks and date/times correctly" in { + val cases = Table( + ("tick", "dateTime", "resolution", "expectedTick", "expectedDateTime"), + // base scenario + (0, "2024-01-02T00:00:00Z", 900, 0, "2024-01-02T00:00:00Z"), + // unusual tick + (1, "2024-01-02T00:00:00Z", 900, 1, "2024-01-02T00:00:00Z"), + // uneven second + (3600, "2024-01-02T00:00:10Z", 900, 3590, "2024-01-02T00:00:00Z"), + // uneven minute + (3600, "2024-01-02T00:07:00Z", 900, 3180, "2024-01-02T00:00:00Z"), + // uneven minute and second + (3600, "2024-01-02T00:07:07Z", 900, 3173, "2024-01-02T00:00:00Z"), + // second-sized resolution, base scenario + (3600, "2024-01-02T00:00:00Z", 15, 3600, "2024-01-02T00:00:00Z"), + // second-sized resolution, uneven second + (3603, "2024-01-02T00:00:18Z", 15, 3600, "2024-01-02T00:00:15Z"), + // second-sized resolution, uneven minute + (3600, "2024-01-02T00:05:00Z", 15, 3600, "2024-01-02T00:05:00Z"), + // second-sized resolution, uneven minute and second + (3607, "2024-01-02T00:05:07Z", 15, 3600, "2024-01-02T00:05:00Z"), + ) + + forAll(cases) { + case (tick, dateTime, resolution, expectedTick, expectedDateTime) => + val (actualTick, actualDateTime) = TickUtil.roundToResolution( + tick, + TimeUtil.withDefaults.toZonedDateTime(dateTime), + resolution, + ) + + actualTick should equal(expectedTick) + TimeUtil.withDefaults.toString(actualDateTime) should + equal(expectedDateTime) + } + } + } +}