diff --git a/CHANGELOG.md b/CHANGELOG.md index 1818551417..6a2db8e8b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add gradle application plugin for command line execution with gradle run [#890](https://github.com/ie3-institute/simona/issues/890) - Additional tests to check flexibility options of thermal house and storage [#729](https://github.com/ie3-institute/simona/issues/729) - EmAgents should be able to handle initialization [#945](https://github.com/ie3-institute/simona/issues/945) +- Added option to directly zip the output files [#793](https://github.com/ie3-institute/simona/issues/793) +- Added weatherData HowTo for Copernicus ERA5 data [#967](https://github.com/ie3-institute/simona/issues/967) ### Changed - Adapted to changed data source in PSDM [#435](https://github.com/ie3-institute/simona/issues/435) @@ -113,6 +115,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix determineState of ThermalHouse [#926](https://github.com/ie3-institute/simona/issues/926) - Fix activation of Hp when not under control of an EM [#922](https://github.com/ie3-institute/simona/issues/922) - Fix expected secondaryData in baseStateData [#955](https://github.com/ie3-institute/simona/issues/955) +- Improve code quality in fixedloadmodelspec and other tests [#919](https://github.com/ie3-institute/simona/issues/919) +- Fix power flow calculation with em agents [#962](https://github.com/ie3-institute/simona/issues/962) +- Fix scheduling at Evcs with more than one Ev at a time without Em [#787](https://github.com/ie3-institute/simona/issues/787) - Refactoring of `ThermalGrid.handleInfeed` to fix thermal storage recharge correctly when empty [#930](https://github.com/ie3-institute/simona/issues/930) ## [3.0.0] - 2023-08-07 diff --git a/build.gradle b/build.gradle index b47bc21820..c95d8e7366 100644 --- a/build.gradle +++ b/build.gradle @@ -149,7 +149,7 @@ dependencies { implementation 'javax.measure:unit-api:2.2' implementation 'tech.units:indriya:2.2' // quantities implementation "org.typelevel:squants_${scalaVersion}:1.8.3" - implementation 'org.apache.commons:commons-csv:1.11.0' + implementation 'org.apache.commons:commons-csv:1.12.0' implementation 'org.scalanlp:breeze_2.13:2.1.0' // scientific calculations (http://www.scalanlp.org/) implementation 'de.lmu.ifi.dbs.elki:elki:0.7.5' // Statistics (for random load model) implementation 'org.jgrapht:jgrapht-core:1.5.2' diff --git a/docs/readthedocs/_static/bibliography/bibtexAll.bib b/docs/readthedocs/_static/bibliography/bibtexAll.bib index e3a6a3f045..23ecc48185 100644 --- a/docs/readthedocs/_static/bibliography/bibtexAll.bib +++ b/docs/readthedocs/_static/bibliography/bibtexAll.bib @@ -189,4 +189,10 @@ @Book{Kittl_2022 address = {Düren}, year = {2022}, doi = {10.17877/DE290R-22548} +} + +@MISC{Radiation_ECMWF, + author = {Robin Hogan}, + title = {Radiation Quantities in the ECMWF model and MARS}, + howpublished = {\url{https://www.ecmwf.int/sites/default/files/elibrary/2015/18490-radiation-quantities-ecmwf-model-and-mars.pdf}} } \ No newline at end of file diff --git a/docs/readthedocs/config.md b/docs/readthedocs/config.md index ec8244a7e9..7b6e0927b2 100644 --- a/docs/readthedocs/config.md +++ b/docs/readthedocs/config.md @@ -94,9 +94,13 @@ simona.output.sink.csv { fileFormat = ".csv" filePrefix = "" fileSuffix = "" + zipFiles = false } ``` +While using a csv sink, the raw data output files can be zipped directly when `zipFiles = true` is used. + + #### Output configuration of the grid The grid output configuration defines for which grid components simulation values are to be output. diff --git a/docs/readthedocs/howto/weatherDataHowToCopernicusERA5.md b/docs/readthedocs/howto/weatherDataHowToCopernicusERA5.md new file mode 100644 index 0000000000..18594a0bba --- /dev/null +++ b/docs/readthedocs/howto/weatherDataHowToCopernicusERA5.md @@ -0,0 +1,34 @@ +(weatherDataHowToCopernicusERA5)= + +# How To use Copernicus ERA5 weather data in SIMONA + +To use weather data from the past within SIMONA we recommend to use the dataset [ERA5 hourly data on single levels from 1940 to present](https://cds-beta.climate.copernicus.eu/datasets/reanalysis-era5-single-levels?tab=download) of [Copernicus Climate Data Store](https://cds-beta.climate.copernicus.eu/). + +The following data parameter should be used + +- Wind + - 100m u-component of wind + - 100m v-component of wind +- Radiation + - Total sky direct solar radiation at surface (FDIR) + - Surface solar radiation downwards (SSRD) +- Temperature + - 2m temperature + +Since SIMONAs [PV Model](pv_model) requires direct and diffuse solar radiation, the diffuse solar radiation need to be determined from the ERA5 data. + +## Pre-Processing solar radiation weather data + +To obtain diffuse solar radiation data from ERA5 weather data, the necessary diffuse solar radiation (FDIFF) at surface can be calculated by + +$$ + FDIFF = SSRD - FDIR +$$ + +*with*\ +**SSRD** = Surface solar radiation downwards\ +**FDIR** = Total sky direct solar radiation at surface + + +**References:** +* {cite:cts}`Radiation_ECMWF` diff --git a/docs/readthedocs/usersguide.md b/docs/readthedocs/usersguide.md index 9bd66ac3c9..5b75b15ac7 100644 --- a/docs/readthedocs/usersguide.md +++ b/docs/readthedocs/usersguide.md @@ -119,6 +119,13 @@ Besides a configuration and the actual grid and grid participants, SIMONA also e There is an option to use sample weather data, but if you want sensible results, definitely consider supplying suitable data. Information on the expected data format and different supported sources are given in the input parameters section of the {doc}`config` file. +The following How-To's are available: +```{toctree} +--- +maxdepth: 1 +--- +howto/weatherDataHowToCopernicusERA5 +``` ## Simulation Outputs diff --git a/input/samples/vn_simona/vn_simona.conf b/input/samples/vn_simona/vn_simona.conf index 9d7b4e4c39..5c3d5fc43d 100644 --- a/input/samples/vn_simona/vn_simona.conf +++ b/input/samples/vn_simona/vn_simona.conf @@ -51,6 +51,7 @@ simona.output.sink.csv { fileFormat = ".csv" filePrefix = "" fileSuffix = "" + zipFiles = false } simona.output.grid = { diff --git a/src/main/resources/config/config-template.conf b/src/main/resources/config/config-template.conf index 0b636a5d50..97deefbd79 100644 --- a/src/main/resources/config/config-template.conf +++ b/src/main/resources/config/config-template.conf @@ -269,6 +269,7 @@ simona.output.sink.csv { isHierarchic = Boolean | false filePrefix = "" fileSuffix = "" + zipFiles = "Boolean" | false } #@optional simona.output.sink.influxDb1x { diff --git a/src/main/scala/edu/ie3/simona/agent/participant/ParticipantAgentFundamentals.scala b/src/main/scala/edu/ie3/simona/agent/participant/ParticipantAgentFundamentals.scala index 0b0ad206e0..984815a846 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant/ParticipantAgentFundamentals.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant/ParticipantAgentFundamentals.scala @@ -808,6 +808,7 @@ protected trait ParticipantAgentFundamentals[ nextActivation, ) + unstashAll() stay() using stateDataFinal } diff --git a/src/main/scala/edu/ie3/simona/agent/participant/evcs/EvcsAgentFundamentals.scala b/src/main/scala/edu/ie3/simona/agent/participant/evcs/EvcsAgentFundamentals.scala index d98e837e48..69b072e55b 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant/evcs/EvcsAgentFundamentals.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant/evcs/EvcsAgentFundamentals.scala @@ -65,7 +65,7 @@ import squants.{Dimensionless, Each, Power} import java.time.ZonedDateTime import java.util.UUID -import scala.collection.SortedSet +import scala.collection.immutable.SortedSet import scala.reflect.{ClassTag, classTag} protected trait EvcsAgentFundamentals @@ -494,9 +494,10 @@ protected trait EvcsAgentFundamentals val relevantData = createCalcRelevantData(modelBaseStateData, tick) + val lastState = getLastOrInitialStateData(modelBaseStateData, tick) + val updatedBaseStateData = { if (relevantData.arrivals.nonEmpty) { - val lastState = getLastOrInitialStateData(modelBaseStateData, tick) val currentEvs = modelBaseStateData.model.determineCurrentEvs( relevantData, @@ -528,6 +529,15 @@ protected trait EvcsAgentFundamentals modelBaseStateData } + // if the lastState's tick is the same as the actual tick the results have already been determined and announced when we handled the departedEvs + if (lastState.tick != tick) { + determineResultsAnnounceUpdateValueStore( + lastState, + currentTick, + modelBaseStateData, + ) + } + // We're only here if we're not flex-controlled, thus sending a Completion is always right goToIdleReplyCompletionAndScheduleTriggerForNextAction( updatedBaseStateData, diff --git a/src/main/scala/edu/ie3/simona/config/SimonaConfig.scala b/src/main/scala/edu/ie3/simona/config/SimonaConfig.scala index d8ec6729ef..947570eb4c 100644 --- a/src/main/scala/edu/ie3/simona/config/SimonaConfig.scala +++ b/src/main/scala/edu/ie3/simona/config/SimonaConfig.scala @@ -2054,6 +2054,7 @@ object SimonaConfig { filePrefix: java.lang.String, fileSuffix: java.lang.String, isHierarchic: scala.Boolean, + zipFiles: scala.Boolean, ) object Csv { def apply( @@ -2073,6 +2074,7 @@ object SimonaConfig { else "", isHierarchic = c.hasPathOrNull("isHierarchic") && c.getBoolean("isHierarchic"), + zipFiles = c.hasPathOrNull("zipFiles") && c.getBoolean("zipFiles"), ) } } diff --git a/src/main/scala/edu/ie3/simona/io/result/ResultSinkType.scala b/src/main/scala/edu/ie3/simona/io/result/ResultSinkType.scala index 3510810062..16f76a84f0 100644 --- a/src/main/scala/edu/ie3/simona/io/result/ResultSinkType.scala +++ b/src/main/scala/edu/ie3/simona/io/result/ResultSinkType.scala @@ -21,6 +21,7 @@ object ResultSinkType { fileFormat: String = ".csv", filePrefix: String = "", fileSuffix: String = "", + zipFiles: Boolean = false, ) extends ResultSinkType final case class InfluxDb1x(url: String, database: String, scenario: String) @@ -48,7 +49,12 @@ object ResultSinkType { sink.headOption match { case Some(params: SimonaConfig.Simona.Output.Sink.Csv) => - Csv(params.fileFormat, params.filePrefix, params.fileSuffix) + Csv( + params.fileFormat, + params.filePrefix, + params.fileSuffix, + params.zipFiles, + ) case Some(params: SimonaConfig.Simona.Output.Sink.InfluxDb1x) => InfluxDb1x(buildInfluxDb1xUrl(params), params.database, runName) case Some(params: SimonaConfig.ResultKafkaParams) => diff --git a/src/main/scala/edu/ie3/simona/main/RunSimonaStandalone.scala b/src/main/scala/edu/ie3/simona/main/RunSimonaStandalone.scala index d0a50802b8..971d403aaf 100644 --- a/src/main/scala/edu/ie3/simona/main/RunSimonaStandalone.scala +++ b/src/main/scala/edu/ie3/simona/main/RunSimonaStandalone.scala @@ -10,12 +10,17 @@ import edu.ie3.simona.config.{ArgsParser, ConfigFailFast, SimonaConfig} import edu.ie3.simona.main.RunSimona._ import edu.ie3.simona.sim.SimonaSim import edu.ie3.simona.sim.setup.SimonaStandaloneSetup +import edu.ie3.util.io.FileIOUtils import org.apache.pekko.actor.typed.scaladsl.AskPattern._ import org.apache.pekko.actor.typed.{ActorSystem, Scheduler} import org.apache.pekko.util.Timeout +import java.nio.file.Path import scala.concurrent.Await -import scala.concurrent.duration.DurationInt +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.duration.{Duration, DurationInt} +import scala.jdk.FutureConverters.CompletionStageOps +import scala.util.{Failure, Success} /** Run a standalone simulation of simona * @@ -24,6 +29,7 @@ import scala.concurrent.duration.DurationInt object RunSimonaStandalone extends RunSimona[SimonaStandaloneSetup] { override implicit val timeout: Timeout = Timeout(12.hours) + implicit val compressTimeoutDuration: Duration = 15.minutes override def setup(args: Array[String]): SimonaStandaloneSetup = { // get the config and prepare it with the provided args @@ -56,9 +62,38 @@ object RunSimonaStandalone extends RunSimona[SimonaStandaloneSetup] { case SimonaEnded(successful) => simonaSim.terminate() + val config = SimonaConfig(simonaSetup.typeSafeConfig).simona.output + + config.sink.csv.map(_.zipFiles).foreach { zipFiles => + if (zipFiles) { + val rawOutputPath = + Path.of(simonaSetup.resultFileHierarchy.rawOutputDataDir) + + rawOutputPath.toFile.listFiles().foreach { file => + val fileName = file.getName + val archiveName = fileName.replace(".csv", "") + val filePath = rawOutputPath.resolve(fileName) + + val compressFuture = + FileIOUtils + .compressFile(filePath, rawOutputPath.resolve(archiveName)) + .asScala + compressFuture.onComplete { + case Success(_) => + FileIOUtils.deleteRecursively(filePath) + case Failure(exception) => + logger.error( + s"Compression of output file to '$archiveName' has failed. Keep raw data.", + exception, + ) + } + Await.ready(compressFuture, compressTimeoutDuration) + } + } + } + successful } - } } diff --git a/src/main/scala/edu/ie3/simona/model/participant/evcs/EvcsModel.scala b/src/main/scala/edu/ie3/simona/model/participant/evcs/EvcsModel.scala index b00ec5e5e3..a1d21252ec 100644 --- a/src/main/scala/edu/ie3/simona/model/participant/evcs/EvcsModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant/evcs/EvcsModel.scala @@ -19,7 +19,6 @@ import edu.ie3.simona.model.participant.evcs.uncontrolled.{ ConstantPowerCharging, MaximumPowerCharging, } -import edu.ie3.util.scala.quantities.DefaultQuantities._ import edu.ie3.simona.model.participant.{ CalcRelevantData, FlexChangeIndicator, @@ -32,7 +31,8 @@ import edu.ie3.simona.util.TickUtil.TickLong import edu.ie3.util.quantities.PowerSystemUnits._ import edu.ie3.util.quantities.QuantityUtils.RichQuantityDouble import edu.ie3.util.scala.OperationInterval -import squants.energy.{KilowattHours, Kilowatts} +import edu.ie3.util.scala.quantities.DefaultQuantities._ +import squants.energy.Kilowatts import squants.time.Seconds import squants.{Dimensionless, Energy, Power} import tech.units.indriya.unit.Units.PERCENT @@ -514,13 +514,17 @@ final case class EvcsModel( modelState: EvcsState, data: EvcsRelevantData, ): ApparentPower = - throw new NotImplementedError("Use calculatePowerAndEvSoc() instead.") + throw new NotImplementedError( + "Use calculateNewScheduling() or chargeEv() instead." + ) override protected def calculateActivePower( modelState: EvcsState, data: EvcsRelevantData, ): Power = - throw new NotImplementedError("Use calculatePowerAndEvSoc() instead.") + throw new NotImplementedError( + "Use calculateNewScheduling() or chargeEv() instead." + ) override def determineFlexOptions( data: EvcsRelevantData, diff --git a/src/main/scala/edu/ie3/simona/sim/setup/SimonaStandaloneSetup.scala b/src/main/scala/edu/ie3/simona/sim/setup/SimonaStandaloneSetup.scala index 65d47863cf..cb704e2ff8 100644 --- a/src/main/scala/edu/ie3/simona/sim/setup/SimonaStandaloneSetup.scala +++ b/src/main/scala/edu/ie3/simona/sim/setup/SimonaStandaloneSetup.scala @@ -63,7 +63,7 @@ import scala.jdk.CollectionConverters._ class SimonaStandaloneSetup( val typeSafeConfig: Config, simonaConfig: SimonaConfig, - resultFileHierarchy: ResultFileHierarchy, + val resultFileHierarchy: ResultFileHierarchy, runtimeEventQueue: Option[LinkedBlockingQueue[RuntimeEvent]] = None, override val args: Array[String], ) extends SimonaSetup { diff --git a/src/test/scala/edu/ie3/simona/agent/participant/EvcsAgentModelCalculationSpec.scala b/src/test/scala/edu/ie3/simona/agent/participant/EvcsAgentModelCalculationSpec.scala index e4e9f25ff1..9b1502bce4 100644 --- a/src/test/scala/edu/ie3/simona/agent/participant/EvcsAgentModelCalculationSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/participant/EvcsAgentModelCalculationSpec.scala @@ -7,8 +7,12 @@ package edu.ie3.simona.agent.participant import com.typesafe.config.ConfigFactory +import edu.ie3.datamodel.models.OperationTime +import edu.ie3.datamodel.models.input.OperatorInput import edu.ie3.datamodel.models.input.system.EvcsInput -import edu.ie3.datamodel.models.input.system.characteristic.QV +import edu.ie3.datamodel.models.input.system.`type`.chargingpoint.ChargingPointTypeUtils +import edu.ie3.datamodel.models.input.system.`type`.evcslocation.EvcsLocationType +import edu.ie3.datamodel.models.input.system.characteristic.{CosPhiFixed, QV} import edu.ie3.datamodel.models.result.system.{EvResult, EvcsResult} import edu.ie3.simona.agent.ValueStore import edu.ie3.simona.agent.grid.GridAgentMessages.{ @@ -61,6 +65,7 @@ import squants.energy._ import squants.{Each, Energy, Power} import java.time.ZonedDateTime +import java.util.UUID import scala.collection.immutable.{SortedMap, SortedSet} class EvcsAgentModelCalculationSpec @@ -1987,6 +1992,386 @@ class EvcsAgentModelCalculationSpec } - } + "provide correct results for three evs charging at same time without Em" in { + val evService = TestProbe("evService") + val resultListener = TestProbe("ResultListener") + + val inputModelUuid = + UUID.fromString("3278d111-b6ce-438c-8b1a-d060be93e520") + val evcsInputModel = new EvcsInput( + inputModelUuid, + "Dummy_EvcsModel", + OperatorInput.NO_OPERATOR_ASSIGNED, + OperationTime.notLimited(), + nodeInputNoSlackNs04KvA, + CosPhiFixed.CONSTANT_CHARACTERISTIC, + null, + ChargingPointTypeUtils.ChargingStationType2, + 4, + 0.95, + EvcsLocationType.HOME, + true, + ) + + val initStateData = ParticipantInitializeStateData[ + EvcsInput, + EvcsRuntimeConfig, + ApparentPower, + ]( + evcsInputModel, + modelConfig = modelConfig, + secondaryDataServices = Iterable( + ActorExtEvDataService(evService.ref) + ), + simulationStartDate = simulationStartDate, + simulationEndDate = simulationEndDate, + resolution = 900L, + requestVoltageDeviationThreshold = requestVoltageDeviationThreshold, + outputConfig = NotifierConfig( + simulationResultInfo = true, + powerRequestReply = false, + flexResult = true, + ), + primaryServiceProxy = primaryServiceProxy.ref, + ) + val evcsAgent = TestFSMRef( + new EvcsAgent( + scheduler = scheduler.ref, + initStateData = initStateData, + listener = Iterable(resultListener.ref), + ) + ) + scheduler.send(evcsAgent, Activation(INIT_SIM_TICK)) + + /* Actor should ask for registration with primary service */ + primaryServiceProxy.expectMsg( + PrimaryServiceRegistrationMessage(inputModelUuid) + ) + /* State should be information handling and having correct state data */ + evcsAgent.stateName shouldBe HandleInformation + evcsAgent.stateData match { + case ParticipantInitializingStateData( + inputModel, + modelConfig, + secondaryDataServices, + simulationStartDate, + simulationEndDate, + resolution, + requestVoltageDeviationThreshold, + outputConfig, + maybeEmAgent, + ) => + inputModel shouldBe SimpleInputContainer(evcsInputModel) + modelConfig shouldBe modelConfig + secondaryDataServices shouldBe Iterable( + ActorExtEvDataService(evService.ref) + ) + simulationStartDate shouldBe simulationStartDate + simulationEndDate shouldBe simulationEndDate + resolution shouldBe resolution + requestVoltageDeviationThreshold shouldBe requestVoltageDeviationThreshold + outputConfig shouldBe NotifierConfig( + simulationResultInfo = true, + powerRequestReply = false, + flexResult = true, + ) + maybeEmAgent shouldBe None + case unsuitableStateData => + fail(s"Agent has unsuitable state data '$unsuitableStateData'.") + } + + /* Refuse registration */ + primaryServiceProxy.send( + evcsAgent, + RegistrationFailedMessage(primaryServiceProxy.ref), + ) + + evService.expectMsg(RegisterForEvDataMessage(evcsInputModel.getUuid)) + evService.send( + evcsAgent, + RegistrationSuccessfulMessage(evService.ref, Some(0)), + ) + + scheduler.expectMsg(Completion(evcsAgent.toTyped, Some(0))) + + /* TICK 0 (expected activation) + - currently no cars + */ + scheduler.send(evcsAgent, Activation(0)) + + evService.send( + evcsAgent, + ProvideEvDataMessage( + 0, + evService.ref, + ArrivingEvs(Seq.empty), + Some(900), + ), + ) + + scheduler.expectMsg(Completion(evcsAgent.toTyped, Some(900))) + + /* TICK 900 + * - ev900 arrives + * - charging with 11 kW + */ + scheduler.send(evcsAgent, Activation(900)) + + val ev900 = EvModelWrapper(evA.copyWithDeparture(3600)) + + evService.send( + evcsAgent, + ProvideEvDataMessage( + 900, + evService.ref, + ArrivingEvs(Seq(ev900)), + Some(1800), + ), + ) + + scheduler.expectMsg(Completion(evcsAgent.toTyped, Some(1800))) + + resultListener.expectMsgType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: EvcsResult) => + result.getInputModel shouldBe evcsInputModel.getUuid + result.getTime shouldBe 0.toDateTime + result.getP should beEquivalentTo(0d.asKiloWatt) + result.getQ should beEquivalentTo(0d.asMegaVar) + } + + /* TICK 1800 + * - ev1800 arrives + * - charging with 11 kW + */ + scheduler.send(evcsAgent, Activation(1800)) + + val ev1800 = EvModelWrapper(evB.copyWithDeparture(4500)) + + evService.send( + evcsAgent, + ProvideEvDataMessage( + 1800, + evService.ref, + ArrivingEvs(Seq(ev1800)), + Some(2700), + ), + ) + + scheduler.expectMsg(Completion(evcsAgent.toTyped, Some(2700))) + + resultListener.expectMsgType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: EvResult) => + result.getInputModel match { + case model if model == ev900.uuid => + result.getTime shouldBe 900.toDateTime + result.getP should beEquivalentTo(11d.asKiloWatt) + result.getQ should beEquivalentTo(0d.asMegaVar) + result.getSoc should beEquivalentTo(0.asPercent, 1e-2) + } + } + + resultListener.expectMsgType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: EvcsResult) => + result.getInputModel shouldBe evcsInputModel.getUuid + result.getTime shouldBe 900.toDateTime + result.getP should beEquivalentTo(11d.asKiloWatt) + result.getQ should beEquivalentTo(0d.asMegaVar) + } + + /* TICK 2700 + * - ev2700 arrives + * - charging with 22 kW + */ + scheduler.send(evcsAgent, Activation(2700)) + + val ev2700 = EvModelWrapper(evC.copyWithDeparture(5400)) + + evService.send( + evcsAgent, + ProvideEvDataMessage( + 2700, + evService.ref, + ArrivingEvs(Seq(ev2700)), + None, + ), + ) + + scheduler.expectMsg(Completion(evcsAgent.toTyped, None)) + + resultListener.receiveN(2).foreach { + case ParticipantResultEvent(result: EvResult) => + result.getInputModel match { + case model if model == ev900.uuid => + result.getTime shouldBe 1800.toDateTime + result.getP should beEquivalentTo(11d.asKiloWatt) + result.getQ should beEquivalentTo(0d.asMegaVar) + result.getSoc should beEquivalentTo(4.74d.asPercent, 1e-2) + case model if model == ev1800.uuid => + result.getTime shouldBe 1800.toDateTime + result.getP should beEquivalentTo(11d.asKiloWatt) + result.getQ should beEquivalentTo(0d.asMegaVar) + result.getSoc should beEquivalentTo(0.asPercent, 1e-2) + } + } + + resultListener.expectMsgType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: EvcsResult) => + result.getInputModel shouldBe evcsInputModel.getUuid + result.getTime shouldBe 1800.toDateTime + result.getP should beEquivalentTo(22d.asKiloWatt) + result.getQ should beEquivalentTo(0d.asMegaVar) + } + + // TICK 3600: ev900 leaves + evService.send( + evcsAgent, + DepartingEvsRequest(3600, Seq(ev900.uuid)), + ) + + evService.expectMsgType[DepartingEvsResponse] match { + case DepartingEvsResponse(evcs, evModels) => + evcs shouldBe evcsInputModel.getUuid + evModels should have size 1 + evModels.headOption match { + case Some(evModel) => + evModel.uuid shouldBe ev900.uuid + evModel.storedEnergy should approximate(KilowattHours(8.25)) + case None => fail("Expected to get at least one ev.") + } + } + + resultListener.receiveN(4).foreach { + case ParticipantResultEvent(result: EvResult) => + result.getInputModel match { + case model if model == ev900.uuid => + result.getTime match { + case time if time == 2700.toDateTime => + result.getP should beEquivalentTo(11d.asKiloWatt) + result.getQ should beEquivalentTo(0d.asMegaVar) + result.getSoc should beEquivalentTo(9.48d.asPercent, 1e-2) + case time if time == 3600.toDateTime => + result.getP should beEquivalentTo(0d.asKiloWatt) + result.getQ should beEquivalentTo(0d.asMegaVar) + result.getSoc should beEquivalentTo(14.22d.asPercent, 1e-2) + } + case model if model == ev1800.uuid => + result.getTime shouldBe 2700.toDateTime + result.getP should beEquivalentTo(11d.asKiloWatt) + result.getQ should beEquivalentTo(0d.asMegaVar) + result.getSoc should beEquivalentTo(3.44d.asPercent, 1e-2) + case model if model == ev2700.uuid => + result.getTime shouldBe 2700.toDateTime + result.getP should beEquivalentTo(22d.asKiloWatt) + result.getQ should beEquivalentTo(0d.asMegaVar) + result.getSoc should beEquivalentTo(0.asPercent, 1e-2) + } + } + + resultListener.expectMsgType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: EvcsResult) => + result.getInputModel shouldBe evcsInputModel.getUuid + result.getTime shouldBe 2700.toDateTime + result.getP should beEquivalentTo(44d.asKiloWatt) + result.getQ should beEquivalentTo(0d.asMegaVar) + } + + // TICK 4500: ev1800 leaves + + evService.send( + evcsAgent, + DepartingEvsRequest(4500, Seq(ev1800.uuid)), + ) + + evService.expectMsgType[DepartingEvsResponse] match { + case DepartingEvsResponse(evcs, evModels) => + evcs shouldBe evcsInputModel.getUuid + evModels should have size 1 + evModels.headOption match { + case Some(evModel) => + evModel.uuid shouldBe ev1800.uuid + evModel.storedEnergy should approximate(KilowattHours(8.25)) + case None => fail("Expected to get at least one ev.") + } + } + + resultListener.receiveN(3).foreach { + case ParticipantResultEvent(result: EvResult) => + result.getInputModel match { + case model if model == ev1800.uuid => + result.getTime match { + case time if time == 3600.toDateTime => + result.getP should beEquivalentTo(11d.asKiloWatt) + result.getQ should beEquivalentTo(0d.asMegaVar) + result.getSoc should beEquivalentTo(6.88.asPercent, 1e-2) + case time if time == 4500.toDateTime => + result.getP should beEquivalentTo(0d.asKiloWatt) + result.getQ should beEquivalentTo(0d.asMegaVar) + result.getSoc should beEquivalentTo(10.31.asPercent, 1e-2) + } + case model if model == ev2700.uuid => + result.getTime shouldBe 3600.toDateTime + result.getP should beEquivalentTo(22d.asKiloWatt) + result.getQ should beEquivalentTo(0d.asMegaVar) + result.getSoc should beEquivalentTo(4.58d.asPercent, 1e-2) + } + } + + resultListener.expectMsgType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: EvcsResult) => + result.getInputModel shouldBe evcsInputModel.getUuid + result.getTime shouldBe 3600.toDateTime + result.getP should beEquivalentTo(33d.asKiloWatt) + result.getQ should beEquivalentTo(0d.asMegaVar) + } + + // TICK 5400: ev2700 leaves + + evService.send( + evcsAgent, + DepartingEvsRequest(5400, Seq(ev2700.uuid)), + ) + + evService.expectMsgType[DepartingEvsResponse] match { + case DepartingEvsResponse(evcs, evModels) => + evcs shouldBe evcsInputModel.getUuid + evModels should have size 1 + evModels.headOption match { + case Some(evModel) => + evModel.uuid shouldBe ev2700.uuid + evModel.storedEnergy should approximate(KilowattHours(16.5)) + case None => fail("Expected to get at least one ev.") + } + } + + resultListener.receiveN(2).foreach { + case ParticipantResultEvent(result: EvResult) => + result.getInputModel match { + case model if model == ev2700.uuid => + result.getTime match { + case time if time == 4500.toDateTime => + result.getP should beEquivalentTo(22d.asKiloWatt) + result.getQ should beEquivalentTo(0d.asMegaVar) + result.getSoc should beEquivalentTo(9.17.asPercent, 1e-2) + case time if time == 5400.toDateTime => + result.getP should beEquivalentTo(0d.asKiloWatt) + result.getQ should beEquivalentTo(0d.asMegaVar) + result.getSoc should beEquivalentTo(13.75.asPercent, 1e-2) + } + } + } + + resultListener.expectMsgType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: EvcsResult) => + result.getInputModel shouldBe evcsInputModel.getUuid + result.getTime shouldBe 4500.toDateTime + result.getP should beEquivalentTo(22d.asKiloWatt) + result.getQ should beEquivalentTo(0d.asMegaVar) + } + /* FixMe: We would expect another Evcs Result for the lastTick of 5400 here. + But this can't be calculated since there is no nextTick. + For simulation it is as well necessary to fix this e.g. by writing the lastResults when finishing simulation. + */ + } + } } diff --git a/src/test/scala/edu/ie3/simona/config/ConfigFailFastSpec.scala b/src/test/scala/edu/ie3/simona/config/ConfigFailFastSpec.scala index c38ef668f1..9a90f8f053 100644 --- a/src/test/scala/edu/ie3/simona/config/ConfigFailFastSpec.scala +++ b/src/test/scala/edu/ie3/simona/config/ConfigFailFastSpec.scala @@ -856,7 +856,7 @@ class ConfigFailFastSpec extends UnitSpec with ConfigTestData { intercept[InvalidConfigParameterException] { ConfigFailFast invokePrivate checkDataSink( Sink( - Some(Csv("", "", "", isHierarchic = false)), + Some(Csv("", "", "", isHierarchic = false, zipFiles = false)), Some(InfluxDb1x("", 0, "")), None, ) diff --git a/src/test/scala/edu/ie3/simona/io/result/ResultSinkTypeSpec.scala b/src/test/scala/edu/ie3/simona/io/result/ResultSinkTypeSpec.scala index 2c93aced13..389eba443f 100644 --- a/src/test/scala/edu/ie3/simona/io/result/ResultSinkTypeSpec.scala +++ b/src/test/scala/edu/ie3/simona/io/result/ResultSinkTypeSpec.scala @@ -23,6 +23,7 @@ class ResultSinkTypeSpec extends UnitSpec { filePrefix = "", fileSuffix = "", isHierarchic = false, + zipFiles = false, ) ), influxDb1x = None, @@ -30,10 +31,11 @@ class ResultSinkTypeSpec extends UnitSpec { ) inside(ResultSinkType(conf, "testRun")) { - case Csv(fileFormat, filePrefix, fileSuffix) => + case Csv(fileFormat, filePrefix, fileSuffix, zipFiles) => fileFormat shouldBe conf.csv.value.fileFormat filePrefix shouldBe conf.csv.value.filePrefix fileSuffix shouldBe conf.csv.value.fileSuffix + zipFiles shouldBe conf.csv.value.zipFiles case _ => fail("Wrong ResultSinkType got instantiated.") } @@ -105,6 +107,7 @@ class ResultSinkTypeSpec extends UnitSpec { filePrefix = "", fileSuffix = "", isHierarchic = false, + zipFiles = false, ) ), influxDb1x = Some( diff --git a/src/test/scala/edu/ie3/simona/model/grid/SystemComponentSpec.scala b/src/test/scala/edu/ie3/simona/model/grid/SystemComponentSpec.scala index dce98587ad..098282e388 100644 --- a/src/test/scala/edu/ie3/simona/model/grid/SystemComponentSpec.scala +++ b/src/test/scala/edu/ie3/simona/model/grid/SystemComponentSpec.scala @@ -75,7 +75,8 @@ class SystemComponentSpec extends UnitSpec with DefaultTestData { val simulationEnd: ZonedDateTime = TimeUtil.withDefaults.toZonedDateTime("2019-01-02T00:00:00Z") - val testCases = Seq( + val testCases = Table( + ("operationStart", "operationEnd", "expectedInterval"), ( Some(TimeUtil.withDefaults.toZonedDateTime("2019-01-01T00:00:00Z")), Some(TimeUtil.withDefaults.toZonedDateTime("2019-01-02T00:00:00Z")), @@ -103,22 +104,23 @@ class SystemComponentSpec extends UnitSpec with DefaultTestData { ), ) - for ((operationStart, operationEnd, expected) <- testCases) { - val operationTimeBuilder = setup() - - operationStart.foreach(operationTimeBuilder.withStart) - operationEnd.foreach(operationTimeBuilder.withEnd) - - val operationTime: OperationTime = operationTimeBuilder.build() - - val interval: OperationInterval = - SystemComponent.determineOperationInterval( - defaultSimulationStart, - simulationEnd, - operationTime, - ) - - interval should be(expected) + forAll(testCases) { + ( + operationStart: Option[ZonedDateTime], + operationEnd: Option[ZonedDateTime], + expected: OperationInterval, + ) => + val operationTimeBuilder = setup() + operationStart.foreach(operationTimeBuilder.withStart) + operationEnd.foreach(operationTimeBuilder.withEnd) + val operationTime: OperationTime = operationTimeBuilder.build() + val interval: OperationInterval = + SystemComponent.determineOperationInterval( + defaultSimulationStart, + simulationEnd, + operationTime, + ) + interval should be(expected) } } diff --git a/src/test/scala/edu/ie3/simona/model/participant/WecModelSpec.scala b/src/test/scala/edu/ie3/simona/model/participant/WecModelSpec.scala index 8933819773..82f69d3b0d 100644 --- a/src/test/scala/edu/ie3/simona/model/participant/WecModelSpec.scala +++ b/src/test/scala/edu/ie3/simona/model/participant/WecModelSpec.scala @@ -104,38 +104,54 @@ class WecModelSpec extends UnitSpec with DefaultTestData { "determine Betz coefficient correctly" in { val wecModel = buildWecModel() - val velocities = Seq(2.0, 2.5, 18.0, 27.0, 34.0, 40.0) - val expectedBetzResults = Seq(0.115933516, 0.2010945555, 0.108671106, - 0.032198846, 0.000196644, 0.0) - velocities.zip(expectedBetzResults).foreach { - case (velocity, betzResult) => - val windVel = MetersPerSecond(velocity) - val betzFactor = wecModel.determineBetzCoefficient(windVel) - val expected = Each(betzResult) - betzFactor shouldEqual expected + + val testCases = Table( + ("velocity", "expectedBetzResult"), + (2.0, 0.115933516), + (2.5, 0.2010945555), + (18.0, 0.108671106), + (27.0, 0.032198846), + (34.0, 0.000196644), + (40.0, 0.0), + ) + + forAll(testCases) { case (velocity: Double, expectedBetzResult: Double) => + val windVel = MetersPerSecond(velocity) + val betzFactor = wecModel.determineBetzCoefficient(windVel) + + betzFactor shouldEqual Each(expectedBetzResult) } } "calculate active power output depending on velocity" in { val wecModel = buildWecModel() - val velocities = - Seq(1.0, 2.0, 3.0, 7.0, 9.0, 13.0, 15.0, 19.0, 23.0, 27.0, 34.0, 40.0) - val expectedPowers = - Seq(0, -2948.8095851378266, -24573.41320418286, -522922.2325710509, - -1140000, -1140000, -1140000, -1140000, -1140000, -1140000, - -24573.39638823692, 0) - - velocities.zip(expectedPowers).foreach { case (velocity, power) => - val wecData = new WecRelevantData( + val testCases = Table( + ("velocity", "expectedPower"), + (1.0, 0.0), + (2.0, -2948.8095851378266), + (3.0, -24573.41320418286), + (7.0, -522922.2325710509), + (9.0, -1140000.0), + (13.0, -1140000.0), + (15.0, -1140000.0), + (19.0, -1140000.0), + (23.0, -1140000.0), + (27.0, -1140000.0), + (34.0, -24573.39638823692), + (40.0, 0.0), + ) + + forAll(testCases) { (velocity: Double, expectedPower: Double) => + val wecData = WecRelevantData( MetersPerSecond(velocity), Celsius(20), Some(Pascals(101325d)), ) val result = wecModel.calculateActivePower(ModelState.ConstantState, wecData) - val expectedPower = Watts(power) + val expectedPowerInWatts = Watts(expectedPower) - result should be(expectedPower) + result should be(expectedPowerInWatts) } } @@ -170,21 +186,23 @@ class WecModelSpec extends UnitSpec with DefaultTestData { "calculate active power output depending on temperature" in { val wecModel = buildWecModel() - val temperatures = Seq(35.0, 20.0, -25.0) - val expectedPowers = - Seq(-23377.23862017266, -24573.41320418286, -29029.60338829823) + val testCases = Table( + ("temperature", "expectedPower"), + (35.0, -23377.23862017266), + (20.0, -24573.41320418286), + (-25.0, -29029.60338829823), + ) - temperatures.zip(expectedPowers).foreach { case (temperature, power) => - val wecData = new WecRelevantData( + forAll(testCases) { (temperature: Double, expectedPower: Double) => + val wecData = WecRelevantData( MetersPerSecond(3.0), Celsius(temperature), Some(Pascals(101325d)), ) - val result = { + val result = wecModel.calculateActivePower(ModelState.ConstantState, wecData) - } - val expectedPower = Watts(power) - result shouldBe expectedPower + val expectedPowerInWatts = Watts(expectedPower) + result shouldBe expectedPowerInWatts } } } 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 index 98fd066841..1682d21b0c 100644 --- a/src/test/scala/edu/ie3/simona/model/participant/load/FixedLoadModelSpec.scala +++ b/src/test/scala/edu/ie3/simona/model/participant/load/FixedLoadModelSpec.scala @@ -94,13 +94,12 @@ class FixedLoadModelSpec reference, ) - for (_ <- 0 until 10000) { + (1 to 10000).foreach { _ => val calculatedPower = dut .calculateActivePower( ModelState.ConstantState, FixedLoadModel.FixedLoadRelevantData, ) - calculatedPower should approximate(expectedPower) } } @@ -116,8 +115,10 @@ class FixedLoadModelSpec forAll(testData) { (reference, expectedPower: Power) => val relevantData = FixedLoadModel.FixedLoadRelevantData - var scale = 0.0 - while (scale <= 2) { + val scales: LazyList[Double] = + LazyList.iterate(0.0)(_ + 0.1).takeWhile(_ <= 2.0) + + scales.foreach { scale => val scaledSRated = Kilowatts( loadInput.getsRated .to(PowerSystemUnits.KILOWATT) @@ -142,8 +143,6 @@ class FixedLoadModelSpec val expectedScaledPower = expectedPower * scale calculatedPower should approximate(expectedScaledPower) - - scale += 0.1 } } } diff --git a/src/test/scala/edu/ie3/simona/model/thermal/ThermalHouseSpec.scala b/src/test/scala/edu/ie3/simona/model/thermal/ThermalHouseSpec.scala index 0a967553e2..f4a7c97574 100644 --- a/src/test/scala/edu/ie3/simona/model/thermal/ThermalHouseSpec.scala +++ b/src/test/scala/edu/ie3/simona/model/thermal/ThermalHouseSpec.scala @@ -42,13 +42,14 @@ class ThermalHouseSpec extends UnitSpec with HpInputTestData { (23d, true, false), ) - testCases.foreach { case (innerTemperature, isTooHigh, isTooLow) => - val innerTemp = Temperature(innerTemperature, Celsius) - val isHigher = thermalHouseTest.isInnerTemperatureTooHigh(innerTemp) - val isLower = thermalHouseTest.isInnerTemperatureTooLow(innerTemp) - - isHigher shouldBe isTooHigh - isLower shouldBe isTooLow + forAll(testCases) { + (innerTemperature: Double, isTooHigh: Boolean, isTooLow: Boolean) => + val innerTemp = Temperature(innerTemperature, Celsius) + val isHigher = thermalHouseTest.isInnerTemperatureTooHigh(innerTemp) + val isLower = thermalHouseTest.isInnerTemperatureTooLow(innerTemp) + + isHigher shouldBe isTooHigh + isLower shouldBe isTooLow } } diff --git a/src/test/scala/edu/ie3/simona/test/common/EvTestData.scala b/src/test/scala/edu/ie3/simona/test/common/EvTestData.scala index ca77f35553..56c90c8e41 100644 --- a/src/test/scala/edu/ie3/simona/test/common/EvTestData.scala +++ b/src/test/scala/edu/ie3/simona/test/common/EvTestData.scala @@ -14,7 +14,7 @@ import java.util.UUID trait EvTestData { protected val evA: MockEvModel = new MockEvModel( - UUID.fromString("73c041c7-68e9-470e-8ca2-21fd7dbd1797"), + UUID.fromString("0-0-0-0-a"), "evA", Quantities.getQuantity(11d, PowerSystemUnits.KILOWATT), Quantities.getQuantity(11d, PowerSystemUnits.KILOWATT), @@ -22,11 +22,19 @@ trait EvTestData { 200, ) protected val evB: MockEvModel = new MockEvModel( - UUID.fromString("6d7d27a1-5cbb-4b73-aecb-dfcc5a6fb22e"), + UUID.fromString("0-0-0-0-b"), "evB", Quantities.getQuantity(11d, PowerSystemUnits.KILOWATT), Quantities.getQuantity(11d, PowerSystemUnits.KILOWATT), Quantities.getQuantity(80d, PowerSystemUnits.KILOWATTHOUR), 200, ) + protected val evC: MockEvModel = new MockEvModel( + UUID.fromString("0-0-0-0-c"), + "evC", + Quantities.getQuantity(22d, PowerSystemUnits.KILOWATT), + Quantities.getQuantity(22d, PowerSystemUnits.KILOWATT), + Quantities.getQuantity(120d, PowerSystemUnits.KILOWATTHOUR), + 200, + ) }