diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e9c08f40a..9eb2d9e856 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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) +- Add some quote to 'printGoodbye' [#997](https://github.com/ie3-institute/simona/issues/997) - Integration test for thermal grids [#878](https://github.com/ie3-institute/simona/issues/878) ### Changed @@ -87,6 +88,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Prepare ThermalStorageTestData for Storage without storageVolumeLvlMin [#894](https://github.com/ie3-institute/simona/issues/894) - Renamed `ActivityStartTrigger`, `ScheduleTriggerMessage`, `CompletionMessage` in UML Diagrams[#675](https://github.com/ie3-institute/simona/issues/675) - Simplifying quantity integration in QuantityUtil [#973](https://github.com/ie3-institute/simona/issues/973) +- Reorganized Jenkins pipeline to separate build and test stages for better efficiency [#938](https://github.com/ie3-institute/simona/issues/938) +- Rewrote SystemParticipantTest and MockParticipant from groovy to scala [#646](https://github.com/ie3-institute/simona/issues/646) +- Rewrote ChpModelTest from groovy to scala [#646](https://github.com/ie3-institute/simona/issues/646) +- Rewrote CylindricalThermalStorageTest Test from groovy to scala [#646](https://github.com/ie3-institute/simona/issues/646) +- Replace mutable var in ChpModelSpec [#1002](https://github.com/ie3-institute/simona/issues/1002) +- Move compression of output files into `ResultEventListener`[#965](https://github.com/ie3-institute/simona/issues/965) ### Fixed - Removed a repeated line in the documentation of vn_simona config [#658](https://github.com/ie3-institute/simona/issues/658) @@ -117,6 +124,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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) +- Fix CheckWindow duration [#921](https://github.com/ie3-institute/simona/issues/921) +- Fixed ThermalStorageResults having multiple entries [#924](https://github.com/ie3-institute/simona/issues/924) - Fixed Hp results leading to overheating house and other effects [#827](https://github.com/ie3-institute/simona/issues/827) - Fixed thermal storage getting recharged when empty [#827](https://github.com/ie3-institute/simona/issues/827) diff --git a/Jenkinsfile b/Jenkinsfile index 46e3837dd8..f8320f0960 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -101,31 +101,32 @@ node { } } + // Build the project + stage('build') { + gradle('clean assemble', projectName) + } + // test the project stage('run tests') { sh 'java -version' - gradle('--refresh-dependencies clean spotlessCheck pmdMain pmdTest reportScoverage checkScoverage', projectName) + gradle('--refresh-dependencies spotlessCheck pmdMain pmdTest', projectName) sh(script: """set +x && cd $projectName""" + ''' set +x; ./gradlew javadoc''', returnStdout: true) } - // sonarqube analysis - stage('sonarqube analysis') { + // sonarqube analysis & quality gate + stage('sonarqube') { String sonarqubeCurrentBranchName = prFromFork() ? prJsonObj.head.repo.full_name : currentBranchName // forks needs to be handled differently String sonarqubeCmd = determineSonarqubeGradleCmd(sonarqubeProjectKey, sonarqubeCurrentBranchName, targetBranchName, orgName, projectName, projectName) withSonarQubeEnv() { // will pick the global server connection from jenkins for sonarqube gradle(sonarqubeCmd, projectName) } - } - - // sonarqube quality gate - stage("quality gate") { timeout(time: 1, unit: 'HOURS') { - // just in case something goes wrong, pipeline will be killed after a timeout - def qg = waitForQualityGate() // reuse taskId previously collected by withSonarQubeEnv + // Just in case something goes wrong, pipeline will be killed after a timeout + def qg = waitForQualityGate() // Reuse taskId previously collected by withSonarQubeEnv if (qg.status != 'OK') { error "Pipeline aborted due to quality gate failure: ${qg.status}" } @@ -684,4 +685,4 @@ def getBranchType(String branchName) { } else { return null } -} \ No newline at end of file +} diff --git a/build.gradle b/build.gradle index 5d3c289349..01935529d3 100644 --- a/build.gradle +++ b/build.gradle @@ -26,11 +26,11 @@ ext { scalaVersion = '2.13' scalaBinaryVersion = '2.13.15' - pekkoVersion = '1.1.1' + pekkoVersion = '1.1.2' jtsVersion = '1.20.0' confluentKafkaVersion = '7.4.0' tscfgVersion = '1.1.3' - scapegoatVersion = '3.0.3' + scapegoatVersion = '3.1.2' testContainerVersion = '0.41.4' @@ -98,12 +98,12 @@ dependencies { /* logging */ implementation "com.typesafe.scala-logging:scala-logging_${scalaVersion}:3.9.5" // pekko scala logging - implementation "ch.qos.logback:logback-classic:1.5.8" + implementation "ch.qos.logback:logback-classic:1.5.12" /* testing */ testImplementation 'org.spockframework:spock-core:2.3-groovy-4.0' testImplementation 'org.scalatestplus:mockito-3-4_2.13:3.2.10.0' - testImplementation 'org.mockito:mockito-core:5.14.1' // mocking framework + testImplementation 'org.mockito:mockito-core:5.14.2' // mocking framework testImplementation "org.scalatest:scalatest_${scalaVersion}:3.2.19" testRuntimeOnly 'com.vladsch.flexmark:flexmark-all:0.64.8' //scalatest html output testImplementation group: 'org.pegdown', name: 'pegdown', version: '1.6.0' diff --git a/docs/readthedocs/config.md b/docs/readthedocs/config.md index 7b6e0927b2..45deee0778 100644 --- a/docs/readthedocs/config.md +++ b/docs/readthedocs/config.md @@ -94,11 +94,11 @@ simona.output.sink.csv { fileFormat = ".csv" filePrefix = "" fileSuffix = "" - zipFiles = false + compressOutputs = false } ``` -While using a csv sink, the raw data output files can be zipped directly when `zipFiles = true` is used. +While using a csv sink, the raw data output files can be zipped directly when `compressOutputs = true` is used. #### Output configuration of the grid diff --git a/docs/readthedocs/requirements.txt b/docs/readthedocs/requirements.txt index 6c1bfcb1a8..5ed7a8bf17 100644 --- a/docs/readthedocs/requirements.txt +++ b/docs/readthedocs/requirements.txt @@ -1,5 +1,5 @@ -Sphinx==7.4.7 -sphinx-rtd-theme==2.0.0 +Sphinx==8.1.3 +sphinx-rtd-theme==3.0.1 sphinxcontrib-plantuml==0.30 myst-parser==4.0.0 markdown-it-py==3.0.0 diff --git a/input/samples/vn_simona/vn_simona.conf b/input/samples/vn_simona/vn_simona.conf index 5c3d5fc43d..3a795e7cb5 100644 --- a/input/samples/vn_simona/vn_simona.conf +++ b/input/samples/vn_simona/vn_simona.conf @@ -51,7 +51,7 @@ simona.output.sink.csv { fileFormat = ".csv" filePrefix = "" fileSuffix = "" - zipFiles = false + compressOutputs = false } simona.output.grid = { diff --git a/src/main/resources/config/config-template.conf b/src/main/resources/config/config-template.conf index 0ad9ab2d8a..20114926b2 100644 --- a/src/main/resources/config/config-template.conf +++ b/src/main/resources/config/config-template.conf @@ -269,7 +269,7 @@ simona.output.sink.csv { isHierarchic = Boolean | false filePrefix = "" fileSuffix = "" - zipFiles = "Boolean" | false + compressOutputs = "Boolean" | false } #@optional simona.output.sink.influxDb1x { diff --git a/src/main/scala/edu/ie3/simona/agent/participant/hp/HpAgentFundamentals.scala b/src/main/scala/edu/ie3/simona/agent/participant/hp/HpAgentFundamentals.scala index a5a8578e9a..4139ae4acb 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant/hp/HpAgentFundamentals.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant/hp/HpAgentFundamentals.scala @@ -188,7 +188,8 @@ trait HpAgentFundamentals ) val accompanyingResults = baseStateData.model.thermalGrid.results( - updatedState.thermalGridState + tick, + updatedState.thermalGridState, )(baseStateData.startDate) val result = AccompaniedSimulationResult(power, accompanyingResults) @@ -252,7 +253,8 @@ trait HpAgentFundamentals relevantData, ) val accompanyingResults = baseStateData.model.thermalGrid.results( - lastModelState.thermalGridState + currentTick, + lastModelState.thermalGridState, )(baseStateData.startDate) val result = AccompaniedSimulationResult(power, accompanyingResults) diff --git a/src/main/scala/edu/ie3/simona/config/SimonaConfig.scala b/src/main/scala/edu/ie3/simona/config/SimonaConfig.scala index 947570eb4c..e31ccce104 100644 --- a/src/main/scala/edu/ie3/simona/config/SimonaConfig.scala +++ b/src/main/scala/edu/ie3/simona/config/SimonaConfig.scala @@ -2050,11 +2050,11 @@ object SimonaConfig { ) object Sink { final case class Csv( + compressOutputs: scala.Boolean, fileFormat: java.lang.String, filePrefix: java.lang.String, fileSuffix: java.lang.String, isHierarchic: scala.Boolean, - zipFiles: scala.Boolean, ) object Csv { def apply( @@ -2063,6 +2063,10 @@ object SimonaConfig { $tsCfgValidator: $TsCfgValidator, ): SimonaConfig.Simona.Output.Sink.Csv = { SimonaConfig.Simona.Output.Sink.Csv( + compressOutputs = + c.hasPathOrNull("compressOutputs") && c.getBoolean( + "compressOutputs" + ), fileFormat = if (c.hasPathOrNull("fileFormat")) c.getString("fileFormat") else ".csv", @@ -2074,7 +2078,6 @@ 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/event/listener/ResultEventListener.scala b/src/main/scala/edu/ie3/simona/event/listener/ResultEventListener.scala index d5acf17912..8e7dd12abd 100644 --- a/src/main/scala/edu/ie3/simona/event/listener/ResultEventListener.scala +++ b/src/main/scala/edu/ie3/simona/event/listener/ResultEventListener.scala @@ -68,41 +68,48 @@ object ResultEventListener extends Transformer3wResultSupport { resultFileHierarchy: ResultFileHierarchy ): Iterable[Future[(Class[_], ResultEntitySink)]] = { resultFileHierarchy.resultSinkType match { - case _: ResultSinkType.Csv => - resultFileHierarchy.resultEntitiesToConsider - .map(resultClass => { - resultFileHierarchy.rawOutputDataFilePaths - .get(resultClass) - .map(Future.successful) - .getOrElse( - Future.failed( - new FileHierarchyException( - s"Unable to get file path for result class '${resultClass.getSimpleName}' from output file hierarchy! " + - s"Available file result file paths: ${resultFileHierarchy.rawOutputDataFilePaths}" - ) + case csv: ResultSinkType.Csv => + val enableCompression = csv.compressOutputs + + resultFileHierarchy.resultEntitiesToConsider.map { resultClass => + val filePathOpt = + resultFileHierarchy.rawOutputDataFilePaths.get(resultClass) + + val filePathFuture = filePathOpt match { + case Some(fileName) => Future.successful(fileName) + case None => + Future.failed( + new FileHierarchyException( + s"Unable to get file path for result class '${resultClass.getSimpleName}' from output file hierarchy! " + + s"Available file result file paths: ${resultFileHierarchy.rawOutputDataFilePaths}" ) ) - .flatMap { fileName => - if (fileName.endsWith(".csv") || fileName.endsWith(".csv.gz")) { - Future { - ( - resultClass, - ResultEntityCsvSink( - fileName.replace(".gz", ""), - new ResultEntityProcessor(resultClass), - fileName.endsWith(".gz"), - ), - ) - } - } else { - Future.failed( - new ProcessResultEventException( - s"Invalid output file format for file $fileName provided. Currently only '.csv' or '.csv.gz' is supported!" - ) + } + + filePathFuture.map { fileName => + val finalFileName = + fileName match { + case name if name.endsWith(".csv.gz") && enableCompression => + name.replace(".gz", "") + case name if name.endsWith(".csv") => name + case fileName => + throw new ProcessResultEventException( + s"Invalid output file format for file $fileName provided or compression is not activated but filename indicates compression. Currently only '.csv' or '.csv.gz' is supported!" ) - } } - }) + + ( + resultClass, + ResultEntityCsvSink( + finalFileName, + new ResultEntityProcessor(resultClass), + enableCompression, + ), + ) + + } + } + case ResultSinkType.InfluxDb1x(url, database, scenario) => // creates one connection per result entity that should be processed resultFileHierarchy.resultEntitiesToConsider 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 16f76a84f0..ac4fac97dd 100644 --- a/src/main/scala/edu/ie3/simona/io/result/ResultSinkType.scala +++ b/src/main/scala/edu/ie3/simona/io/result/ResultSinkType.scala @@ -21,7 +21,7 @@ object ResultSinkType { fileFormat: String = ".csv", filePrefix: String = "", fileSuffix: String = "", - zipFiles: Boolean = false, + compressOutputs: Boolean = false, ) extends ResultSinkType final case class InfluxDb1x(url: String, database: String, scenario: String) @@ -53,7 +53,7 @@ object ResultSinkType { params.fileFormat, params.filePrefix, params.fileSuffix, - params.zipFiles, + params.compressOutputs, ) case Some(params: SimonaConfig.Simona.Output.Sink.InfluxDb1x) => InfluxDb1x(buildInfluxDb1xUrl(params), params.database, runName) diff --git a/src/main/scala/edu/ie3/simona/io/runtime/RuntimeEventLogSink.scala b/src/main/scala/edu/ie3/simona/io/runtime/RuntimeEventLogSink.scala index b46ccc06b0..86c731a3ed 100644 --- a/src/main/scala/edu/ie3/simona/io/runtime/RuntimeEventLogSink.scala +++ b/src/main/scala/edu/ie3/simona/io/runtime/RuntimeEventLogSink.scala @@ -27,7 +27,6 @@ import scala.concurrent.duration.DurationLong final case class RuntimeEventLogSink( simulationStartDate: ZonedDateTime, log: Logger, - private var last: Long = 0L, ) extends RuntimeEventSink { override def handleRuntimeEvent( @@ -45,15 +44,13 @@ final case class RuntimeEventLogSink( case CheckWindowPassed(tick, duration) => log.info( - s"******* Simulation until ${calcTime(tick)} completed. ${durationAndMemoryString(duration - last)} ******" + s"******* Simulation until ${calcTime(tick)} completed. ${durationAndMemoryString(duration)} ******" ) - last = duration case Ready(tick, duration) => log.info( - s"******* Switched from 'Simulating' to 'Ready'. Last simulated time: ${calcTime(tick)}. ${durationAndMemoryString(duration - last)} ******" + s"******* Switched from 'Simulating' to 'Ready'. Last simulated time: ${calcTime(tick)}. ${durationAndMemoryString(duration)} ******" ) - last = duration case Simulating(startTick, endTick) => log.info( diff --git a/src/main/scala/edu/ie3/simona/main/RunSimona.scala b/src/main/scala/edu/ie3/simona/main/RunSimona.scala index 8265ed0cdb..c255990a05 100644 --- a/src/main/scala/edu/ie3/simona/main/RunSimona.scala +++ b/src/main/scala/edu/ie3/simona/main/RunSimona.scala @@ -66,6 +66,7 @@ trait RunSimona[T <: SimonaSetup] extends LazyLogging { "\"Ich bin der Anfang, das Ende, die Eine, die Viele ist. Ich bin die Borg.\" - Borg-Königin (in Star Trek: Der erste Kontakt)", "\"A horse! A horse! My kingdom for a horse!\" - King Richard III (in Shakespeare's Richard III, 1594)", "\"Und wenn du lange in einen Abgrund blickst, blickt der Abgrund auch in dich hinein\" - F. Nietzsche", + "\"Before anything else, preparation is the key to success.\" - Alexander Graham Bell", ) val rand = new Random diff --git a/src/main/scala/edu/ie3/simona/main/RunSimonaStandalone.scala b/src/main/scala/edu/ie3/simona/main/RunSimonaStandalone.scala index 971d403aaf..fbb1bef45f 100644 --- a/src/main/scala/edu/ie3/simona/main/RunSimonaStandalone.scala +++ b/src/main/scala/edu/ie3/simona/main/RunSimonaStandalone.scala @@ -10,17 +10,12 @@ 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.ExecutionContext.Implicits.global -import scala.concurrent.duration.{Duration, DurationInt} -import scala.jdk.FutureConverters.CompletionStageOps -import scala.util.{Failure, Success} +import scala.concurrent.duration.DurationInt /** Run a standalone simulation of simona * @@ -29,7 +24,6 @@ import scala.util.{Failure, Success} 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 @@ -62,36 +56,6 @@ 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/thermal/CylindricalThermalStorage.scala b/src/main/scala/edu/ie3/simona/model/thermal/CylindricalThermalStorage.scala index cc25b5615a..0523d72e86 100644 --- a/src/main/scala/edu/ie3/simona/model/thermal/CylindricalThermalStorage.scala +++ b/src/main/scala/edu/ie3/simona/model/thermal/CylindricalThermalStorage.scala @@ -58,7 +58,7 @@ final case class CylindricalThermalStorage( bus: ThermalBusInput, maxEnergyThreshold: Energy, chargingPower: Power, - override protected var _storedEnergy: Energy, + override var _storedEnergy: Energy, ) extends ThermalStorage( uuid, id, 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 f37ade37f8..1da1dfeba8 100644 --- a/src/main/scala/edu/ie3/simona/model/thermal/ThermalGrid.scala +++ b/src/main/scala/edu/ie3/simona/model/thermal/ThermalGrid.scala @@ -1312,6 +1312,7 @@ final case class ThermalGrid( *
  • the house has reached it's lower temperature boundary,
  • there * is no infeed from external and
  • the storage is not empty * itself
  • + * * @param tick * The current tick * @param maybeHouseState @@ -1383,6 +1384,9 @@ final case class ThermalGrid( /** Convert the given state of the thermal grid into result models of it's * constituent models + * + * @param currentTick + * Actual simulation tick * @param state * State to be converted * @param startDateTime @@ -1390,85 +1394,36 @@ final case class ThermalGrid( * @return * A [[Seq]] of results of the constituent thermal model */ - def results( - state: ThermalGridState - )(implicit startDateTime: ZonedDateTime): Seq[ResultEntity] = { - /* FIXME: We only want to write results when there is a change within the participant. - * At the moment we write an storage result when the house result gets updated and vice versa. - * */ - - val houseResultTick: Option[Long] = house - .zip(state.houseState) - .flatMap { - case ( - _, - ThermalHouseState(tick, _, _), - ) => - Some(tick) - case _ => None - } + def results(currentTick: Long, state: ThermalGridState)(implicit + startDateTime: ZonedDateTime + ): Seq[ResultEntity] = { - val storageResultTick: Option[Long] = heatStorage - .zip(state.storageState) - .flatMap { - case ( - _, - ThermalStorageState(tick, _, _), - ) => - Some(tick) - case _ => None - } - - val domesticHotWaterStorageResultTick: Option[Long] = - domesticHotWaterStorage - .zip(state.domesticHotWaterStorageState) - .flatMap { - case ( - _, - ThermalStorageState(tick, _, _), - ) => - Some(tick) - case _ => None - } - - val actualResultTick = Seq( - houseResultTick, - storageResultTick, - domesticHotWaterStorageResultTick, - ).flatten.maxOption.getOrElse( - throw new InconsistentStateException( - s"Was not able to get tick for thermal result. Result tick of thermal house: $houseResultTick," + - s" Result tick of thermal heat storage: $storageResultTick, Result tick of domestic hot water storage: $domesticHotWaterStorageResultTick." - ) - ) - - val houseResults = house + val maybeHouseResult = house .zip(state.houseState) + .filter { case (_, state) => state.tick == currentTick } .map { case ( thermalHouse, ThermalHouseState(_, innerTemperature, thermalInfeed), ) => - Seq( - new ThermalHouseResult( - actualResultTick.toDateTime, - thermalHouse.uuid, - thermalInfeed.toMegawatts.asMegaWatt, - innerTemperature.toKelvinScale.asKelvin, - ) + new ThermalHouseResult( + tick.toDateTime, + thermalHouse.uuid, + thermalInfeed.toMegawatts.asMegaWatt, + innerTemperature.toKelvinScale.asKelvin, ) } - .getOrElse(Seq.empty[ResultEntity]) - val storageResults = heatStorage + val maybeStorageResult = storage .zip(state.storageState) + .filter { case (_, state) => state.tick == currentTick } .map { case ( storage: CylindricalThermalStorage, ThermalStorageState(_, storedEnergy, qDot), ) => - houseResults :+ new CylindricalStorageResult( - actualResultTick.toDateTime, + new CylindricalStorageResult( + tick.toDateTime, storage.uuid, storedEnergy.toMegawattHours.asMegaWattHour, qDot.toMegawatts.asMegaWatt, @@ -1479,6 +1434,8 @@ final case class ThermalGrid( s"Result handling for storage type '${heatStorage.getClass.getSimpleName}' not supported." ) } + + Seq(maybeHouseResult, maybeStorageResult).flatten .getOrElse(houseResults) val finalResults = domesticHotWaterStorage diff --git a/src/main/scala/edu/ie3/simona/scheduler/RuntimeNotifier.scala b/src/main/scala/edu/ie3/simona/scheduler/RuntimeNotifier.scala index 844021c530..73ec02a4af 100644 --- a/src/main/scala/edu/ie3/simona/scheduler/RuntimeNotifier.scala +++ b/src/main/scala/edu/ie3/simona/scheduler/RuntimeNotifier.scala @@ -111,8 +111,12 @@ final case class RuntimeNotifier( val completedWindows = (adjustedLastCheck + checkWindow) to completedTick by checkWindow - completedWindows.foreach { tick => - notify(CheckWindowPassed(tick, duration(lastStartTime, nowTime))) + completedWindows.foldLeft(lastCheckWindowTime) { + case (lastTime, tick) => + notify( + CheckWindowPassed(tick, duration(lastTime, nowTime)) + ) + None } completedWindows.lastOption 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 cb704e2ff8..65d47863cf 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, - val resultFileHierarchy: ResultFileHierarchy, + resultFileHierarchy: ResultFileHierarchy, runtimeEventQueue: Option[LinkedBlockingQueue[RuntimeEvent]] = None, override val args: Array[String], ) extends SimonaSetup { diff --git a/src/test/groovy/edu/ie3/simona/model/participant/ChpModelTest.groovy b/src/test/groovy/edu/ie3/simona/model/participant/ChpModelTest.groovy deleted file mode 100644 index a07273bb27..0000000000 --- a/src/test/groovy/edu/ie3/simona/model/participant/ChpModelTest.groovy +++ /dev/null @@ -1,243 +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 static edu.ie3.util.quantities.PowerSystemUnits.* -import static tech.units.indriya.quantity.Quantities.getQuantity -import static tech.units.indriya.unit.Units.PERCENT - -import edu.ie3.datamodel.models.OperationTime -import edu.ie3.datamodel.models.StandardUnits -import edu.ie3.datamodel.models.input.OperatorInput -import edu.ie3.datamodel.models.input.system.ChpInput -import edu.ie3.datamodel.models.input.system.characteristic.CosPhiFixed -import edu.ie3.datamodel.models.input.system.type.ChpTypeInput -import edu.ie3.datamodel.models.input.thermal.CylindricalStorageInput -import edu.ie3.datamodel.models.input.thermal.ThermalBusInput -import edu.ie3.datamodel.models.voltagelevels.GermanVoltageLevelUtils -import edu.ie3.simona.model.participant.ChpModel.ChpState -import edu.ie3.simona.model.thermal.CylindricalThermalStorage -import edu.ie3.util.scala.quantities.KilowattHoursPerKelvinCubicMeters$ -import edu.ie3.util.TimeUtil -import edu.ie3.util.scala.quantities.Sq -import spock.lang.Shared -import spock.lang.Specification -import spock.lang.Unroll -import squants.energy.KilowattHours$ -import squants.energy.Kilowatts$ -import squants.space.CubicMeters$ -import squants.thermal.Celsius$ -import testutils.TestObjectFactory - -class ChpModelTest extends Specification { - - @Shared - static final Double TOLERANCE = 0.0001d - @Shared - ChpState chpStateNotRunning = new ChpState(false, 0, Sq.create(0, Kilowatts$.MODULE$), Sq.create(0, KilowattHours$.MODULE$)) - @Shared - ChpState chpStateRunning = new ChpState(true, 0, Sq.create(0, Kilowatts$.MODULE$), Sq.create(0, KilowattHours$.MODULE$)) - @Shared - CylindricalStorageInput storageInput - @Shared - ChpInput chpInput - - def setupSpec() { - def thermalBus = new ThermalBusInput(UUID.randomUUID(), "thermal bus") - - storageInput = new CylindricalStorageInput( - UUID.randomUUID(), - "ThermalStorage", - thermalBus, - getQuantity(100, StandardUnits.VOLUME), - getQuantity(20, StandardUnits.VOLUME), - getQuantity(30, StandardUnits.TEMPERATURE), - getQuantity(40, StandardUnits.TEMPERATURE), - getQuantity(1.15, StandardUnits.SPECIFIC_HEAT_CAPACITY)) - - def chpTypeInput = new ChpTypeInput( - UUID.randomUUID(), - "ChpTypeInput", - getQuantity(10000d, EURO), - getQuantity(200d, EURO_PER_MEGAWATTHOUR), - getQuantity(19, PERCENT), - getQuantity(76, PERCENT), - getQuantity(100, KILOVOLTAMPERE), - 0.95, - getQuantity(50, KILOWATT), - getQuantity(0, KILOWATT)) - - chpInput = new ChpInput( - UUID.randomUUID(), - "ChpInput", - OperatorInput.NO_OPERATOR_ASSIGNED, - OperationTime.notLimited(), - TestObjectFactory.buildNodeInput(false, GermanVoltageLevelUtils.MV_10KV, 0), - thermalBus, - new CosPhiFixed("cosPhiFixed:{(0.0,0.95)}"), - null, - chpTypeInput, - null, - false) - } - - static def buildChpModel(CylindricalThermalStorage thermalStorage) { - return new ChpModel( - UUID.randomUUID(), - "ChpModel", - null, - null, - Sq.create(100, Kilowatts$.MODULE$), - 0.95, - Sq.create(50, Kilowatts$.MODULE$), - thermalStorage) - } - - static def buildChpRelevantData(ChpState chpState, Double heatDemand) { - return new ChpModel.ChpRelevantData(chpState, Sq.create(heatDemand, KilowattHours$.MODULE$), 7200) - } - - static def buildThermalStorage(CylindricalStorageInput storageInput, Double storageLvl) { - def storedEnergy = CylindricalThermalStorage.volumeToEnergy( - Sq.create(storageLvl, CubicMeters$.MODULE$), - Sq.create(storageInput.c.value.toDouble(), KilowattHoursPerKelvinCubicMeters$.MODULE$), - Sq.create(storageInput.inletTemp.value.doubleValue(), Celsius$.MODULE$), - Sq.create(storageInput.returnTemp.value.doubleValue(), Celsius$.MODULE$) - ) - def thermalStorage = CylindricalThermalStorage.apply(storageInput, storedEnergy) - return thermalStorage - } - - @Unroll - def "Check active power after calculating next state with #chpState and heat demand #heatDemand kWh:"() { - given: - def chpData = buildChpRelevantData(chpState, heatDemand) - def thermalStorage = buildThermalStorage(storageInput, storageLvl) - def chpModel = buildChpModel(thermalStorage) - - when: - def activePower = chpModel.calculateNextState(chpData).activePower() - - then: - activePower.toKilowatts() == expectedActivePower - - where: - chpState | storageLvl | heatDemand || expectedActivePower - chpStateNotRunning | 90 | 0 || 0 // tests case (false, false, true) - chpStateNotRunning | 90 | 8 * 115 || 95 // tests case (false, true, false) - chpStateNotRunning | 90 | 10 || 0 // tests case (false, true, true) - chpStateRunning | 90 | 0 || 95 // tests case (true, false, true) - chpStateRunning | 90 | 8 * 115 || 95 // tests case (true, true, false) - chpStateRunning | 90 | 10 || 95 // tests case (true, true, true) - chpStateRunning | 90 | 7 * 115 + 1 || 95 // test case (_, true, false) and demand covered together with chp - chpStateRunning | 90 | 9 * 115 || 95 // test case (_, true, false) and demand not covered together with chp - chpStateRunning | 92 | 1 || 95 // test case (true, true, true) and storage volume exceeds maximum - /* The following tests do not exist: (false, false, false), (true, false, false) */ - } - - @Unroll - def "Check total energy after calculating next state with #chpState and heat demand #heatDemand kWh:"() { - given: - def chpData = buildChpRelevantData(chpState, heatDemand) - def thermalStorage = buildThermalStorage(storageInput, storageLvl) - def chpModel = buildChpModel(thermalStorage) - - when: - def nextState = chpModel.calculateNextState(chpData) - def thermalEnergy = nextState.thermalEnergy() - - then: - Math.abs(thermalEnergy.toKilowattHours() - expectedTotalEnergy) < TOLERANCE - - where: - chpState | storageLvl | heatDemand || expectedTotalEnergy - chpStateNotRunning | 90 | 0 || 0 // tests case (false, false, true) - chpStateNotRunning | 90 | 8 * 115 || 100 // tests case (false, true, false) - chpStateNotRunning | 90 | 10 || 0 // tests case (false, true, true) - chpStateRunning | 90 | 0 || 100 // tests case (true, false, true) - chpStateRunning | 90 | 8 * 115 || 100 // tests case (true, true, false) - chpStateRunning | 90 | 10 || 100 // tests case (true, true, true) - chpStateRunning | 90 | 7 * 115 + 1 || 100 // test case (_, true, false) and demand covered together with chp - chpStateRunning | 90 | 9 * 115 || 100 // test case (_, true, false) and demand not covered together with chp - chpStateRunning | 92 | 1 || 93 // test case (true, true, true) and storage volume exceeds maximum - /* The following tests do not exist: (false, false, false), (true, false, false) */ - } - - def "Check storage level after calculating next state with #chpState and heat demand #heatDemand kWh:"() { - given: - def chpData = buildChpRelevantData(chpState, heatDemand) - def thermalStorage = buildThermalStorage(storageInput, storageLvl) - def chpModel = buildChpModel(thermalStorage) - - when: - chpModel.calculateNextState(chpData) - - then: - thermalStorage._storedEnergy() =~ expectedStoredEnergy - - where: - chpState | storageLvl | heatDemand | expectedStoredEnergy - chpStateNotRunning | 90d | 0d || 1035d // tests case (false, false, true) - chpStateNotRunning | 90d | 8d * 115d || 230d // tests case (false, true, false) - chpStateNotRunning | 90d | 10d || 1025d // tests case (false, true, true) - chpStateRunning | 90d | 0d || 1135d // tests case (true, false, true) - chpStateRunning | 90d | 8d * 115d || 230d // tests case (true, true, false) - chpStateRunning | 90d | 10d || 1125d // tests case (true, true, true) - chpStateRunning | 90d | 806d || 329d // test case (_, true, false) and demand covered together with chp - chpStateRunning | 90d | 9d * 115d || 230d // test case (_, true, false) and demand not covered together with chp - chpStateRunning | 92d | 1d || 1150d // test case (true, true, true) and storage volume exceeds maximum - /* The following tests do not exist: (false, false, false), (true, false, false) */ - } - - def "Check time tick and running status after calculating next state with #chpState and heat demand #heatDemand kWh:"() { - given: - def chpData = buildChpRelevantData(chpState, heatDemand) - def thermalStorage = buildThermalStorage(storageInput, storageLvl) - def chpModel = buildChpModel(thermalStorage) - - when: - def nextState = chpModel.calculateNextState(chpData) - - then: - nextState.lastTimeTick() == expectedTimeTick - nextState.isRunning() == expectedRunningStatus - - where: - chpState | storageLvl | heatDemand | expectedTimeTick | expectedRunningStatus - chpStateNotRunning | 90 | 0 || 7200 | false // tests case (false, false, true) - chpStateNotRunning | 90 | 8 * 115 || 7200 | true // tests case (false, true, false) - chpStateNotRunning | 90 | 10 || 7200 | false // tests case (false, true, true) - chpStateRunning | 90 | 0 || 7200 | true // tests case (true, false, true) - chpStateRunning | 90 | 8 * 115 || 7200 | true // tests case (true, true, false) - chpStateRunning | 90 | 10 || 7200 | true // tests case (true, true, true) - chpStateRunning | 90 | 806 || 7200 | true // test case (_, true, false) and demand covered together with chp - chpStateRunning | 90 | 9 * 115 || 7200 | true // test case (_, true, false) and demand not covered together with chp - chpStateRunning | 92 | 1 || 7200 | false // test case (true, true, true) and storage volume exceeds maximum - /* The following tests do not exist: (false, false, false), (true, false, false) */ - } - - def "Check apply, validation and build method:"() { - when: - def thermalStorage = buildThermalStorage(storageInput, 90) - def chpModelCaseClass = buildChpModel(thermalStorage) - def startDate = TimeUtil.withDefaults.toZonedDateTime("2021-01-01T00:00:00Z") - def endDate = startDate.plusSeconds(86400L) - def chpModelCaseObject = ChpModel.apply( - chpInput, - startDate, - endDate, - null, - 1.0, - thermalStorage) - - then: - chpModelCaseClass.sRated() == chpModelCaseObject.sRated() - chpModelCaseClass.cosPhiRated() == chpModelCaseObject.cosPhiRated() - chpModelCaseClass.pThermal() == chpModelCaseObject.pThermal() - chpModelCaseClass.storage() == chpModelCaseObject.storage() - } -} diff --git a/src/test/groovy/edu/ie3/simona/model/participant/SystemParticipantTest.groovy b/src/test/groovy/edu/ie3/simona/model/participant/SystemParticipantTest.groovy deleted file mode 100644 index d13782be0c..0000000000 --- a/src/test/groovy/edu/ie3/simona/model/participant/SystemParticipantTest.groovy +++ /dev/null @@ -1,242 +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.datamodel.models.input.system.characteristic.CosPhiFixed -import edu.ie3.datamodel.models.input.system.characteristic.CosPhiP -import edu.ie3.datamodel.models.input.system.characteristic.QV -import edu.ie3.simona.model.participant.control.QControl -import edu.ie3.simona.test.common.model.MockParticipant -import edu.ie3.util.scala.OperationInterval -import edu.ie3.util.scala.quantities.Sq -import spock.lang.Specification -import squants.* -import squants.energy.* - -class SystemParticipantTest extends Specification { - - def "Test calculateQ for a load or generation unit with fixed cosphi"() { - given: "the mocked system participant model with a q_v characteristic" - - def loadMock = new MockParticipant( - UUID.fromString("b69f6675-5284-4e28-add5-b76952ec1ec2"), - "System participant calculateQ Test", - OperationInterval.apply(0L, 86400L), - QControl.apply(new CosPhiFixed(varCharacteristicString)), - Sq.create(200, Kilowatts$.MODULE$), - 1d) - Dimensionless adjustedVoltage = Sq.create(1, Each$.MODULE$) // needed for method call but not applicable for cosphi_p - - when: "the reactive power is calculated" - Power power = Sq.create(pVal, Kilowatts$.MODULE$) - def qCalc = loadMock.calculateReactivePower(power, adjustedVoltage) - - then: "compare the results in watt" - Math.abs(qCalc.toKilovars() - qSol.doubleValue()) < 0.0001 - - where: - varCharacteristicString | pVal || qSol - "cosPhiFixed:{(0.0,0.9)}" | 0 || 0 - "cosPhiFixed:{(0.0,0.9)}" | 50 || 24.216105241892627000 - "cosPhiFixed:{(0.0,0.9)}" | 100 || 48.432210483785254000 - "cosPhiFixed:{(0.0,0.9)}" | 200 || 0 - "cosPhiFixed:{(0.0,0.9)}" | -50 || -24.216105241892627000 - "cosPhiFixed:{(0.0,0.9)}" | -100 || -48.432210483785254000 - "cosPhiFixed:{(0.0,0.9)}" | -200 || 0 - "cosPhiFixed:{(0.0,1.0)}" | 100 || 0 - } - - def "Test calculateQ for a load unit with cosphi_p"() { - given: "the mocked load model" - - def loadMock = new MockParticipant( - UUID.fromString("3d28b9f7-929a-48e3-8696-ad2330a04225"), - "Load calculateQ Test", - OperationInterval.apply(0L, 86400L), - QControl.apply(new CosPhiP(varCharacteristicString)), - Sq.create(102, Kilowatts$.MODULE$), - 1d) - - Dimensionless adjustedVoltage = Sq.create(1, Each$.MODULE$) // needed for method call but not applicable for cosphi_p - - when: "the reactive power is calculated" - Power power = Sq.create(p, Kilowatts$.MODULE$) - def qCalc = loadMock.calculateReactivePower(power, adjustedVoltage) - - then: "compare the results in watt" - Math.abs(qCalc.toKilovars() - qSol.doubleValue()) < 0.0001 - - where: // explained below - varCharacteristicString | p || qSol - "cosPhiP:{(0,1),(0.05,1),(0.1,1),(0.15,1),(0.2,1),(0.25,1),(0.3,1),(0.35,1),(0.4,1),(0.45,1),(0.5,1),(0.55,0.99),(0.6,0.98),(0.65,0.97),(0.7,0.96),(0.75,0.95),(0.8,0.94),(0.85,0.93),(0.9,0.92),(0.95,0.91),(1,0.9)}" | 100.0d || 20.09975124224169d - "cosPhiP:{(0,-1),(0.05,-1),(0.1,-1),(0.15,-1),(0.2,-1),(0.25,-1),(0.3,-1),(0.35,-1),(0.4,-1),(0.45,-1),(0.5,-1),(0.55,-0.99),(0.6,-0.98),(0.65,-0.97),(0.7,-0.96),(0.75,-0.95),(0.8,-0.94),(0.85,-0.93),(0.9,-0.92),(0.95,-0.91),(1,-0.9)}" | 100.0d || -20.09975124224169d - - // first line is "with P" -> positive Q (influence on voltage level: decrease) is expected - // second line is "against P" -> negative Q (influence on voltage level: increase) is expected - } - - def "Test calculateQ for a generation unit with cosphi_p"() { - given: "the mocked generation model" - - def loadMock = new MockParticipant( - UUID.fromString("30f84d97-83b4-4b71-9c2d-dbc7ebb1127c"), - "Generation calculateQ Test", - OperationInterval.apply(0L, 86400L), - QControl.apply(new CosPhiP(varCharacteristicString)), - Sq.create(101, Kilowatts$.MODULE$), - 1d) - - Dimensionless adjustedVoltage = Sq.create(1, Each$.MODULE$) // needed for method call but not applicable for cosphi_p - - when: "the reactive power is calculated" - Power power = Sq.create(p, Kilowatts$.MODULE$) - def qCalc = loadMock.calculateReactivePower(power, adjustedVoltage) - - then: "compare the results in watt" - Math.abs(qCalc.toKilovars() - qSol.doubleValue()) < 0.0001 - - where: // explained below - varCharacteristicString | p || qSol - "cosPhiP:{(-1,0.9),(-0.95,0.91),(-0.9,0.92),(-0.85,0.93),(-0.8,0.94),(-0.75,0.95),(-0.7,0.96),(-0.65,0.97),(-0.6,0.98),(-0.55,0.99),(-0.5,1),(-0.45,1),(-0.4,1),(-0.35,1),(-0.3,1),(-0.25,1),(-0.2,1),(-0.15,1),(-0.1,1),(-0.05,1),(0,1)}" | -100.0d || -14.177446878757818d - "cosPhiP:{(-1,-0.9),(-0.95,-0.91),(-0.9,-0.92),(-0.85,-0.93),(-0.8,-0.94),(-0.75,-0.95),(-0.7,-0.96),(-0.65,-0.97),(-0.6,-0.98),(-0.55,-0.99),(-0.5,-1),(-0.45,-1),(-0.4,-1),(-0.35,-1),(-0.3,-1),(-0.25,-1),(-0.2,-1),(-0.15,-1),(-0.1,-1),(-0.05,-1),(0,-1)}" | -100.0d || 14.177446878757818d - - // first line is "with P" -> negative Q (influence on voltage level: increase) is expected - // second line is "against P" -> positive Q (influence on voltage level: decrease) is expected - } - - def "Test calculateQ for a standard q_v characteristic"() { - given: "the mocked system participant model with a q_v characteristic" - - Power p = Sq.create(42, Kilowatts$.MODULE$) - - def loadMock = new MockParticipant( - UUID.fromString("d8461624-d142-4360-8e02-c21965ec555e"), - "System participant calculateQ Test", - OperationInterval.apply(0L, 86400L), - QControl.apply(new QV("qV:{(0.93,-1),(0.97,0),(1,0),(1.03,0),(1.07,1)}")), - Sq.create(200, Kilowatts$.MODULE$), - 0.98) - - when: "the reactive power is calculated" - Dimensionless adjustedVoltage = Sq.create(adjustedVoltageVal.doubleValue(), Each$.MODULE$) - def qCalc = loadMock.calculateReactivePower(p, adjustedVoltage) - - then: "compare the results in watt" - Math.abs(qCalc.toKilovars() - qSol.doubleValue()) < 0.0001 - - where: - adjustedVoltageVal || qSol - 0.9 || -39.79949748426482 - 0.93 || -39.79949748426482 - 0.95 || -19.89974874213241 - 0.97 || 0 - 1.00 || 0 - 1.03 || 0 - 1.05 || 19.89974874213241 - 1.07 || 39.79949748426482 - 1.1 || 39.79949748426482 - } - - def "Test calculateQ for a standard q_v characteristic if active power is zero and cosPhiRated 1"() { - given: "the mocked system participant model with a q_v characteristic" - - Power p = Sq.create(0, Kilowatts$.MODULE$) - - def loadMock = new MockParticipant( - UUID.fromString("d8461624-d142-4360-8e02-c21965ec555e"), - "System participant calculateQ Test", - OperationInterval.apply(0L, 86400L), - QControl.apply(new QV("qV:{(0.93,-1),(0.97,0),(1,0),(1.03,0),(1.07,1)}")), - Sq.create(200, Kilowatts$.MODULE$), - 1d) - - when: "the reactive power is calculated" - Dimensionless adjustedVoltage = Sq.create(adjustedVoltageVal.doubleValue(), Each$.MODULE$) - def qCalc = loadMock.calculateReactivePower(p, adjustedVoltage) - - then: "compare the results in watt" - Math.abs(qCalc.toKilovars() - qSol.doubleValue()) < 0.0001 - - where: - adjustedVoltageVal || qSol - 0.9 || 0 - 0.93 || 0 - 0.95 || 0 - 0.97 || 0 - 1.00 || 0 - 1.03 || 0 - 1.05 || 0 - 1.07 || 0 - 1.1 || 0 - } - - def "Test calculateQ for a standard q_v characteristic if active power is not zero and cosPhiRated 0.95"() { - given: "the mocked system participant model with a q_v characteristic" - - Power p = Sq.create(100, Kilowatts$.MODULE$) - - def loadMock = new MockParticipant( - UUID.fromString("d8461624-d142-4360-8e02-c21965ec555e"), - "System participant calculateQ Test", - OperationInterval.apply(0L, 86400L), - QControl.apply(new QV("qV:{(0.93,-1),(0.97,0),(1,0),(1.03,0),(1.07,1)}")), - Sq.create(200, Kilowatts$.MODULE$), - 0.95) - - when: "the reactive power is calculated" - Dimensionless adjustedVoltage = Sq.create(adjustedVoltageVal.doubleValue(), Each$.MODULE$) - def qCalc = loadMock.calculateReactivePower(p, adjustedVoltage) - - then: "compare the results in watt" - Math.abs(qCalc.toKilovars() - qSol.doubleValue()) < 0.0001 - - where: - adjustedVoltageVal || qSol - 0.9 || -62.449979983984 - 0.93 || -62.449979983984 - 0.95 || -31.224989991992 - 0.97 || 0 - 1.00 || 0 - 1.03 || 0 - 1.05 || 31.224989991992 - 1.07 || 62.449979983984 - 1.1 || 62.449979983984 - } - - def "Test calculateQ for a standard q_v characteristic if active power is 195 and cosPhiRated 0.95"() { - given: "the mocked system participant model with a q_v characteristic" - - Power p = Sq.create(195, Kilowatts$.MODULE$) - - def loadMock = new MockParticipant( - UUID.fromString("d8461624-d142-4360-8e02-c21965ec555e"), - "System participant calculateQ Test", - OperationInterval.apply(0L, 86400L), - QControl.apply(new QV("qV:{(0.93,-1),(0.97,0),(1,0),(1.03,0),(1.07,1)}")), - Sq.create(200, Kilowatts$.MODULE$), - 0.95) - - when: "the reactive power is calculated" - Dimensionless adjustedVoltage = Sq.create(adjustedVoltageVal.doubleValue(), Each$.MODULE$) - def qCalc = loadMock.calculateReactivePower(p, adjustedVoltage) - - then: "compare the results in watt" - Math.abs(qCalc.toKilovars() - qSol.doubleValue()) < 0.0001 - - where: - adjustedVoltageVal || qSol - 0.9 || -44.440972086578 - 0.93 || -44.440972086578 - 0.95 || -31.224989991992 - 0.97 || 0 - 1.00 || 0 - 1.03 || 0 - 1.05 || 31.224989991992 - 1.07 || 44.440972086578 - 1.1 || 44.440972086578 - } -} diff --git a/src/test/groovy/edu/ie3/simona/model/thermal/CylindricalThermalStorageTest.groovy b/src/test/groovy/edu/ie3/simona/model/thermal/CylindricalThermalStorageTest.groovy deleted file mode 100644 index af5ffef8e5..0000000000 --- a/src/test/groovy/edu/ie3/simona/model/thermal/CylindricalThermalStorageTest.groovy +++ /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.thermal - -import static edu.ie3.util.quantities.PowerSystemUnits.KILOWATTHOUR -import static tech.units.indriya.quantity.Quantities.getQuantity - -import edu.ie3.datamodel.models.StandardUnits -import edu.ie3.datamodel.models.input.thermal.CylindricalStorageInput -import edu.ie3.util.scala.quantities.KilowattHoursPerKelvinCubicMeters$ -import edu.ie3.util.scala.quantities.Sq -import spock.lang.Shared -import spock.lang.Specification -import squants.energy.KilowattHours$ -import squants.energy.Kilowatts$ -import squants.space.CubicMeters$ -import squants.thermal.Celsius$ - -class CylindricalThermalStorageTest extends Specification { - - static final double TESTING_TOLERANCE = 1e-10 - - @Shared - CylindricalStorageInput storageInput - - def setupSpec() { - storageInput = new CylindricalStorageInput( - UUID.randomUUID(), - "ThermalStorage", - null, - getQuantity(100, StandardUnits.VOLUME), - getQuantity(20, StandardUnits.VOLUME), - getQuantity(30, StandardUnits.TEMPERATURE), - getQuantity(40, StandardUnits.TEMPERATURE), - getQuantity(1.15, StandardUnits.SPECIFIC_HEAT_CAPACITY)) - } - - static def buildThermalStorage(CylindricalStorageInput storageInput, Double volume) { - def storedEnergy = CylindricalThermalStorage.volumeToEnergy(Sq.create(volume, CubicMeters$.MODULE$), - Sq.create(storageInput.c.value.doubleValue(), KilowattHoursPerKelvinCubicMeters$.MODULE$), - Sq.create(storageInput.inletTemp.value.doubleValue(), Celsius$.MODULE$), - Sq.create(storageInput.returnTemp.value.doubleValue(), Celsius$.MODULE$)) - def thermalStorage = CylindricalThermalStorage.apply(storageInput, storedEnergy) - return thermalStorage - } - - def vol2Energy(Double volume) { - return CylindricalThermalStorage.volumeToEnergy( // FIXME below: get values in units with to..() - Sq.create(volume, CubicMeters$.MODULE$), - Sq.create(storageInput.c.value.doubleValue(), KilowattHoursPerKelvinCubicMeters$.MODULE$), - Sq.create(storageInput.inletTemp.value.doubleValue(), Celsius$.MODULE$), - Sq.create(storageInput.returnTemp.value.doubleValue(), Celsius$.MODULE$)) - } - - def "Check storage level operations:"() { - given: - def storage = buildThermalStorage(storageInput, 70) - - when: - def initialLevel = - getQuantity(storage._storedEnergy().toKilowattHours(), KILOWATTHOUR) - storage._storedEnergy_$eq(vol2Energy(50d),) - def newLevel1 = getQuantity(storage._storedEnergy().toKilowattHours(), KILOWATTHOUR) - def surplus = storage.tryToStoreAndReturnRemainder( - vol2Energy(55d)) - def newLevel2 = getQuantity(storage._storedEnergy().toKilowattHours(), KILOWATTHOUR) - def isCovering = storage.isDemandCoveredByStorage(Sq.create(5, KilowattHours$.MODULE$)) - def lack = - storage.tryToTakeAndReturnLack( - vol2Energy(95d) - ) - def newLevel3 = getQuantity(storage._storedEnergy().toKilowattHours(), KILOWATTHOUR) - def notCovering = storage.isDemandCoveredByStorage(Sq.create(1, KilowattHours$.MODULE$)) - - then: - initialLevel.value.doubleValue() =~ vol2Energy(70d).toKilowattHours() - newLevel1.value.doubleValue() =~ vol2Energy(50d).toKilowattHours() - surplus =~ vol2Energy(5d) - newLevel2.value.doubleValue() =~ vol2Energy(100d).toKilowattHours() - lack =~ vol2Energy(15d) - newLevel3.value.doubleValue() =~ vol2Energy(20d).toKilowattHours() - isCovering - !notCovering - } - - def "Check converting methods:"() { - given: - def storage = buildThermalStorage(storageInput, 70) - - when: - def usableThermalEnergy = storage.usableThermalEnergy() - - then: - Math.abs(usableThermalEnergy.toKilowattHours() - 5 * 115) < TESTING_TOLERANCE - } - - def "Check apply, validation and build method:"() { - when: - def storage = buildThermalStorage(storageInput, 70) - - then: - storage.uuid() == storageInput.uuid - storage.id() == storageInput.id - storage.operatorInput() == storageInput.operator - storage.operationTime() == storageInput.operationTime - storage.bus() == storageInput.thermalBus - } - - def "Check mutable state update:"() { - when: - def storage = buildThermalStorage(storageInput, 70) - def lastState = new ThermalStorage.ThermalStorageState(tick, Sq.create(storedEnergy, KilowattHours$.MODULE$), Sq.create(qDot, Kilowatts$.MODULE$)) - def result = storage.updateState(newTick, Sq.create(newQDot, Kilowatts$.MODULE$), lastState) - - then: - Math.abs(result._1().storedEnergy().toKilowattHours() - expectedStoredEnergy.doubleValue()) < TESTING_TOLERANCE - result._2.defined - result._2.get() == expectedThreshold - - where: - tick | storedEnergy | qDot | newTick | newQDot || expectedStoredEnergy | expectedThreshold - 0L | 250.0d | 10.0d | 3600L | 42.0d || 260.0d | new ThermalStorage.ThermalStorageThreshold.StorageFull(79886L) - 0L | 250.0d | 10.0d | 3600L | -42.0d || 260.0d | new ThermalStorage.ThermalStorageThreshold.StorageEmpty(6171L) - 0L | 250.0d | -10.0d | 3600L | 42.0d || 240.0d | new ThermalStorage.ThermalStorageThreshold.StorageFull(81600L) - 0L | 250.0d | -10.0d | 3600L | -42.0d || 240.0d | new ThermalStorage.ThermalStorageThreshold.StorageEmpty(4457L) - 0L | 250.0d | -10.0d | 3600L | -42.0d || 240.0d | new ThermalStorage.ThermalStorageThreshold.StorageEmpty(4457L) - 0L | 1000.0d | 149.0d | 3600L | 5000.0d || 1149.0d | new ThermalStorage.ThermalStorageThreshold.StorageFull(3601L) - 0L | 240.0d | -9.0d | 3600L | -5000.0d || 231.0d | new ThermalStorage.ThermalStorageThreshold.StorageEmpty(3601L) - } - - def "Check mutable state update, if no threshold is reached:"() { - when: - def storage = buildThermalStorage(storageInput, 70) - def lastState = new ThermalStorage.ThermalStorageState(tick, Sq.create(storedEnergy, KilowattHours$.MODULE$), Sq.create(qDot, Kilowatts$.MODULE$)) - def result = storage.updateState(newTick, Sq.create(newQDot, Kilowatts$.MODULE$), lastState) - - then: - Math.abs(result._1().storedEnergy().toKilowattHours() - expectedStoredEnergy.doubleValue()) < TESTING_TOLERANCE - result._2.empty - - where: - tick | storedEnergy | qDot | newTick | newQDot || expectedStoredEnergy - 0L | 250.0d | 10.0d | 3600L | 0.0d || 260.0d - 0L | 250.0d | -10.0d | 3600L | 0.0d || 240.0d - } -} diff --git a/src/test/groovy/edu/ie3/simona/test/common/model/MockParticipant.groovy b/src/test/groovy/edu/ie3/simona/test/common/model/MockParticipant.groovy deleted file mode 100644 index acfc06fa14..0000000000 --- a/src/test/groovy/edu/ie3/simona/test/common/model/MockParticipant.groovy +++ /dev/null @@ -1,60 +0,0 @@ -/* - * © 2022. TU Dortmund University, - * Institute of Energy Systems, Energy Efficiency and Energy Economics, - * Research group Distribution grid planning and operation - */ - -package edu.ie3.simona.test.common.model - -import edu.ie3.simona.agent.participant.data.Data -import edu.ie3.simona.model.participant.CalcRelevantData -import edu.ie3.simona.model.participant.ModelState -import edu.ie3.simona.model.participant.SystemParticipant -import edu.ie3.simona.model.participant.control.QControl -import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage -import edu.ie3.util.scala.OperationInterval -import edu.ie3.util.scala.quantities.Sq -import scala.Tuple2 -import squants.Dimensionless -import squants.energy.* - -class MockParticipant extends SystemParticipant { - - MockParticipant( - UUID uuid, - String id, - OperationInterval operationInterval, - QControl qControl, - Power sRated, - Double cosPhiRated - ) { - super( - uuid, - id, - operationInterval, - qControl, - sRated, - cosPhiRated - ) - } - - @Override - Data.PrimaryData.ApparentPower calculatePower(long tick, Dimensionless voltage, ModelState state, CalcRelevantData data) { - return super.calculateApparentPower(tick, voltage, state, data) - } - - @Override - Power calculateActivePower(ModelState maybeModelState, CalcRelevantData data) { - return Sq.create(0, Megawatts$.MODULE$) - } - - @Override - FlexibilityMessage.ProvideFlexOptions determineFlexOptions(CalcRelevantData data, ModelState lastState) { - return null - } - - @Override - Tuple2 handleControlledPowerChange(CalcRelevantData data, ModelState lastState, Power setPower) { - return null - } -} diff --git a/src/test/scala/edu/ie3/simona/config/ConfigFailFastSpec.scala b/src/test/scala/edu/ie3/simona/config/ConfigFailFastSpec.scala index 9a90f8f053..ae7bfae423 100644 --- a/src/test/scala/edu/ie3/simona/config/ConfigFailFastSpec.scala +++ b/src/test/scala/edu/ie3/simona/config/ConfigFailFastSpec.scala @@ -856,7 +856,9 @@ class ConfigFailFastSpec extends UnitSpec with ConfigTestData { intercept[InvalidConfigParameterException] { ConfigFailFast invokePrivate checkDataSink( Sink( - Some(Csv("", "", "", isHierarchic = false, zipFiles = false)), + Some( + Csv(compressOutputs = false, "", "", "", isHierarchic = false) + ), Some(InfluxDb1x("", 0, "")), None, ) diff --git a/src/test/scala/edu/ie3/simona/event/listener/ResultEventListenerSpec.scala b/src/test/scala/edu/ie3/simona/event/listener/ResultEventListenerSpec.scala index 40b4b3f123..f110feca8e 100644 --- a/src/test/scala/edu/ie3/simona/event/listener/ResultEventListenerSpec.scala +++ b/src/test/scala/edu/ie3/simona/event/listener/ResultEventListenerSpec.scala @@ -20,6 +20,7 @@ import edu.ie3.simona.event.ResultEvent.{ ParticipantResultEvent, PowerFlowResultEvent, } +import edu.ie3.simona.io.result.ResultSinkType.Csv import edu.ie3.simona.io.result.{ResultEntitySink, ResultSinkType} import edu.ie3.simona.test.common.result.PowerFlowResultData import edu.ie3.simona.test.common.{IOTestCommons, UnitSpec} @@ -71,16 +72,21 @@ class ResultEventListenerSpec runId: Int, fileFormat: String, classes: Set[Class[_ <: ResultEntity]] = resultEntitiesToBeWritten, - ): ResultFileHierarchy = + compressResults: Boolean = false, + ): ResultFileHierarchy = { + val resultSinkType: ResultSinkType = + Csv(fileFormat, "", "", compressResults) + ResultFileHierarchy( outputDir = testTmpDir + File.separator + runId, simulationName, ResultEntityPathConfig( classes, - ResultSinkType.Csv(fileFormat = fileFormat), + resultSinkType, ), createDirs = true, ) + } def createDir( resultFileHierarchy: ResultFileHierarchy @@ -367,7 +373,8 @@ class ResultEventListenerSpec "shutting down" should { "shutdown and compress the data when requested to do so without any errors" in { - val specificOutputFileHierarchy = resultFileHierarchy(6, ".csv.gz") + val specificOutputFileHierarchy = + resultFileHierarchy(6, ".csv.gz", compressResults = true) val listenerRef = spawn( ResultEventListener( specificOutputFileHierarchy 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 389eba443f..7e17edc51c 100644 --- a/src/test/scala/edu/ie3/simona/io/result/ResultSinkTypeSpec.scala +++ b/src/test/scala/edu/ie3/simona/io/result/ResultSinkTypeSpec.scala @@ -23,7 +23,7 @@ class ResultSinkTypeSpec extends UnitSpec { filePrefix = "", fileSuffix = "", isHierarchic = false, - zipFiles = false, + compressOutputs = false, ) ), influxDb1x = None, @@ -35,7 +35,7 @@ class ResultSinkTypeSpec extends UnitSpec { fileFormat shouldBe conf.csv.value.fileFormat filePrefix shouldBe conf.csv.value.filePrefix fileSuffix shouldBe conf.csv.value.fileSuffix - zipFiles shouldBe conf.csv.value.zipFiles + zipFiles shouldBe conf.csv.value.compressOutputs case _ => fail("Wrong ResultSinkType got instantiated.") } @@ -107,7 +107,7 @@ class ResultSinkTypeSpec extends UnitSpec { filePrefix = "", fileSuffix = "", isHierarchic = false, - zipFiles = false, + compressOutputs = false, ) ), influxDb1x = Some( diff --git a/src/test/scala/edu/ie3/simona/model/participant/ChpModelSpec.scala b/src/test/scala/edu/ie3/simona/model/participant/ChpModelSpec.scala new file mode 100644 index 0000000000..2d4a66da85 --- /dev/null +++ b/src/test/scala/edu/ie3/simona/model/participant/ChpModelSpec.scala @@ -0,0 +1,380 @@ +/* + * © 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.datamodel.models.input.system.ChpInput +import edu.ie3.datamodel.models.input.system.`type`.ChpTypeInput +import edu.ie3.datamodel.models.input.OperatorInput +import edu.ie3.datamodel.models.input.system.characteristic.CosPhiFixed +import edu.ie3.datamodel.models.input.thermal.{ + CylindricalStorageInput, + ThermalBusInput, +} +import edu.ie3.datamodel.models.voltagelevels.GermanVoltageLevelUtils +import edu.ie3.datamodel.models.{OperationTime, StandardUnits} +import edu.ie3.simona.model.participant.ChpModel.{ChpRelevantData, ChpState} +import edu.ie3.simona.model.thermal.CylindricalThermalStorage +import edu.ie3.simona.test.common.{DefaultTestData, UnitSpec} +import edu.ie3.util.TimeUtil +import edu.ie3.util.quantities.PowerSystemUnits +import edu.ie3.util.quantities.PowerSystemUnits.{ + EURO, + EURO_PER_MEGAWATTHOUR, + KILOVOLTAMPERE, + KILOWATT, +} +import edu.ie3.util.scala.quantities._ +import org.scalatest.BeforeAndAfterAll +import org.scalatest.prop.TableDrivenPropertyChecks +import squants.energy.{KilowattHours, Kilowatts} +import squants.space.CubicMeters +import squants.thermal.Celsius +import tech.units.indriya.quantity.Quantities.getQuantity +import tech.units.indriya.unit.Units +import tech.units.indriya.unit.Units.PERCENT +import testutils.TestObjectFactory + +import java.util.UUID + +class ChpModelSpec + extends UnitSpec + with BeforeAndAfterAll + with TableDrivenPropertyChecks + with DefaultTestData { + + implicit val Tolerance: Double = 1e-12 + val chpStateNotRunning: ChpState = + ChpState(isRunning = false, 0, Kilowatts(0), KilowattHours(0)) + val chpStateRunning: ChpState = + ChpState(isRunning = true, 0, Kilowatts(0), KilowattHours(0)) + + val (storageInput, chpInput) = setupSpec() + + def setupSpec(): (CylindricalStorageInput, ChpInput) = { + val thermalBus = new ThermalBusInput(UUID.randomUUID(), "thermal bus") + + val storageInput = new CylindricalStorageInput( + UUID.randomUUID(), + "ThermalStorage", + thermalBus, + getQuantity(100, StandardUnits.VOLUME), + getQuantity(20, StandardUnits.VOLUME), + getQuantity(30, StandardUnits.TEMPERATURE), + getQuantity(40, StandardUnits.TEMPERATURE), + getQuantity(1.15, StandardUnits.SPECIFIC_HEAT_CAPACITY), + ) + + val chpTypeInput = new ChpTypeInput( + UUID.randomUUID(), + "ChpTypeInput", + getQuantity(10000d, EURO), + getQuantity(200, EURO_PER_MEGAWATTHOUR), + getQuantity(19, PERCENT), + getQuantity(76, PERCENT), + getQuantity(100, KILOVOLTAMPERE), + 0.95, + getQuantity(50d, KILOWATT), + getQuantity(0, KILOWATT), + ) + + val chpInput = new ChpInput( + UUID.randomUUID(), + "ChpInput", + OperatorInput.NO_OPERATOR_ASSIGNED, + OperationTime.notLimited(), + TestObjectFactory + .buildNodeInput(false, GermanVoltageLevelUtils.MV_10KV, 0), + thermalBus, + new CosPhiFixed("cosPhiFixed:{(0.0,0.95)}"), + null, + chpTypeInput, + null, + false, + ) + + (storageInput, chpInput) + } + + def buildChpModel(thermalStorage: CylindricalThermalStorage): ChpModel = { + ChpModel( + UUID.randomUUID(), + "ChpModel", + null, + null, + Kilowatts(100), + 0.95, + Kilowatts(50), + thermalStorage, + ) + } + + def buildChpRelevantData( + chpState: ChpState, + heatDemand: Double, + ): ChpRelevantData = { + ChpRelevantData(chpState, KilowattHours(heatDemand), 7200) + } + + def buildThermalStorage( + storageInput: CylindricalStorageInput, + volume: Double, + ): CylindricalThermalStorage = { + val storedEnergy = CylindricalThermalStorage.volumeToEnergy( + CubicMeters(volume), + KilowattHoursPerKelvinCubicMeters( + storageInput.getC + .to(PowerSystemUnits.KILOWATTHOUR_PER_KELVIN_TIMES_CUBICMETRE) + .getValue + .doubleValue + ), + Celsius( + storageInput.getInletTemp.to(Units.CELSIUS).getValue.doubleValue() + ), + Celsius( + storageInput.getReturnTemp.to(Units.CELSIUS).getValue.doubleValue() + ), + ) + CylindricalThermalStorage(storageInput, storedEnergy) + } + + "A ChpModel" should { + "Check active power after calculating next state with #chpState and heat demand #heatDemand kWh:" in { + val testCases = Table( + ("chpState", "storageLvl", "heatDemand", "expectedActivePower"), + (chpStateNotRunning, 90, 0, 0), // tests case (false, false, true) + ( + chpStateNotRunning, + 90, + 8 * 115, + 95, + ), // tests case (false, true, false) + (chpStateNotRunning, 90, 10, 0), // tests case (false, true, true) + (chpStateRunning, 90, 0, 95), // tests case (true, false, true) + (chpStateRunning, 90, 8 * 115, 95), // tests case (true, true, false) + (chpStateRunning, 90, 10, 95), // tests case (true, true, true) + ( + chpStateRunning, + 90, + 7 * 115 + 1, + 95, + ), // test case (_, true, false) and demand covered together with chp + ( + chpStateRunning, + 90, + 9 * 115, + 95, + ), // test case (_, true, false) and demand not covered together with chp + ( + chpStateRunning, + 92, + 1, + 95, + ), // test case (true, true, true) and storage volume exceeds maximum + ) + + forAll(testCases) { + (chpState, storageLvl, heatDemand, expectedActivePower) => + val chpData = buildChpRelevantData(chpState, heatDemand) + val thermalStorage = buildThermalStorage(storageInput, storageLvl) + val chpModel = buildChpModel(thermalStorage) + + val activePower = chpModel.calculateNextState(chpData).activePower + activePower.toKilowatts shouldEqual expectedActivePower + } + } + + "Check total energy after calculating next state with #chpState and heat demand #heatDemand kWh:" in { + val testCases = Table( + ("chpState", "storageLvl", "heatDemand", "expectedTotalEnergy"), + (chpStateNotRunning, 90, 0, 0), // tests case (false, false, true) + ( + chpStateNotRunning, + 90, + 8 * 115, + 100, + ), // tests case (false, true, false) + (chpStateNotRunning, 90, 10, 0), // tests case (false, true, true) + (chpStateRunning, 90, 0, 100), // tests case (true, false, true) + (chpStateRunning, 90, 8 * 115, 100), // tests case (true, true, false) + (chpStateRunning, 90, 10, 100), // tests case (true, true, true) + ( + chpStateRunning, + 90, + 7 * 115 + 1, + 100, + ), // test case (_, true, false) and demand covered together with chp + ( + chpStateRunning, + 90, + 9 * 115, + 100, + ), // test case (_, true, false) and demand not covered together with chp + ( + chpStateRunning, + 92, + 1, + 93, + ), // test case (true, true, true) and storage volume exceeds maximum + ) + + forAll(testCases) { + (chpState, storageLvl, heatDemand, expectedTotalEnergy) => + val chpData = buildChpRelevantData(chpState, heatDemand) + val thermalStorage = buildThermalStorage(storageInput, storageLvl) + val chpModel = buildChpModel(thermalStorage) + + val nextState = chpModel.calculateNextState(chpData) + val thermalEnergy = nextState.thermalEnergy + thermalEnergy.toKilowattHours shouldEqual expectedTotalEnergy + } + } + + "Check storage level after calculating next state with #chpState and heat demand #heatDemand kWh:" in { + val testCases = Table( + ("chpState", "storageLvl", "heatDemand", "expectedStoredEnergy"), + (chpStateNotRunning, 90, 0, 1035), // tests case (false, false, true) + ( + chpStateNotRunning, + 90, + 8 * 115, + 230, + ), // tests case (false, true, false) + (chpStateNotRunning, 90, 10, 1025), // tests case (false, true, true) + (chpStateRunning, 90, 0, 1135), // tests case (true, false, true) + (chpStateRunning, 90, 8 * 115, 230), // tests case (true, true, false) + (chpStateRunning, 90, 10, 1125), // tests case (true, true, true) + ( + chpStateRunning, + 90, + 806, + 329, + ), // test case (_, true, false) and demand covered together with chp + ( + chpStateRunning, + 90, + 9 * 115, + 230, + ), // test case (_, true, false) and demand not covered together with chp + ( + chpStateRunning, + 92, + 1, + 1150, + ), // test case (true, true, true) and storage volume exceeds maximum + ) + + forAll(testCases) { + (chpState, storageLvl, heatDemand, expectedStoredEnergy) => + val chpData = buildChpRelevantData(chpState, heatDemand) + val thermalStorage = buildThermalStorage(storageInput, storageLvl) + val chpModel = buildChpModel(thermalStorage) + + chpModel.calculateNextState(chpData) + thermalStorage._storedEnergy.toKilowattHours should be( + expectedStoredEnergy + ) + } + } + + "Check time tick and running status after calculating next state with #chpState and heat demand #heatDemand kWh:" in { + val testCases = Seq( + // (ChpState, Storage Level, Heat Demand, Expected Time Tick, Expected Running Status) + ( + chpStateNotRunning, + 90, + 0, + 7200, + false, + ), // Test case (false, false, true) + ( + chpStateNotRunning, + 90, + 8 * 115, + 7200, + true, + ), // Test case (false, true, false) + ( + chpStateNotRunning, + 90, + 10, + 7200, + false, + ), // Test case (false, true, true) + (chpStateRunning, 90, 0, 7200, true), // Test case (true, false, true) + ( + chpStateRunning, + 90, + 8 * 115, + 7200, + true, + ), // Test case (true, true, false) + (chpStateRunning, 90, 10, 7200, true), // Test case (true, true, true) + ( + chpStateRunning, + 90, + 806, + 7200, + true, + ), // Test case (_, true, false) and demand covered together with chp + ( + chpStateRunning, + 90, + 9 * 115, + 7200, + true, + ), // Test case (_, true, false) and demand not covered together with chp + ( + chpStateRunning, + 92, + 1, + 7200, + false, + ), // Test case (true, true, true) and storage volume exceeds maximum + ) + + for ( + ( + chpState, + storageLvl, + heatDemand, + expectedTimeTick, + expectedRunningStatus, + ) <- testCases + ) { + val chpData = buildChpRelevantData(chpState, heatDemand) + val thermalStorage = buildThermalStorage(storageInput, storageLvl) + val chpModel = buildChpModel(thermalStorage) + + val nextState = chpModel.calculateNextState(chpData) + + nextState.lastTimeTick shouldEqual expectedTimeTick + nextState.isRunning shouldEqual expectedRunningStatus + } + } + + "apply, validate, and build correctly" in { + val thermalStorage = buildThermalStorage(storageInput, 90) + val chpModelCaseClass = buildChpModel(thermalStorage) + val startDate = + TimeUtil.withDefaults.toZonedDateTime("2021-01-01T00:00:00Z") + val endDate = startDate.plusSeconds(86400L) + val chpModelCaseObject = ChpModel( + chpInput, + startDate, + endDate, + null, + 1.0, + thermalStorage, + ) + + chpModelCaseClass.sRated shouldEqual chpModelCaseObject.sRated + chpModelCaseClass.cosPhiRated shouldEqual chpModelCaseObject.cosPhiRated + chpModelCaseClass.pThermal shouldEqual chpModelCaseObject.pThermal + chpModelCaseClass.storage shouldEqual chpModelCaseObject.storage + } + } +} diff --git a/src/test/scala/edu/ie3/simona/model/participant/SystemParticipantSpec.scala b/src/test/scala/edu/ie3/simona/model/participant/SystemParticipantSpec.scala new file mode 100644 index 0000000000..0c36be3e9c --- /dev/null +++ b/src/test/scala/edu/ie3/simona/model/participant/SystemParticipantSpec.scala @@ -0,0 +1,266 @@ +/* + * © 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.datamodel.models.input.system.characteristic.{ + CosPhiFixed, + CosPhiP, + QV, +} +import edu.ie3.simona.model.participant.control.QControl +import edu.ie3.simona.test.common.UnitSpec +import edu.ie3.simona.test.common.model.MockParticipant +import edu.ie3.util.scala.OperationInterval +import edu.ie3.util.scala.quantities.{Kilovars, Megavars, ReactivePower} +import org.scalatest.matchers.should.Matchers +import squants._ +import squants.energy._ + +import java.util.UUID +import scala.language.postfixOps + +class SystemParticipantSpec extends UnitSpec with Matchers { + + private implicit val tolerance: ReactivePower = Megavars( + 1e-5 + ) + + "SystemParticipant" should { + "calculate reactive power correctly for fixed cos phi" in { + val adjustedVoltage = + Each(1) // not applicable for cos phi_fixed but required + + val testCases = Table( + ("varCharacteristicString", "pVal", "qSol"), + ("cosPhiFixed:{(0.0,0.9)}", 0, Kilovars(0)), + ("cosPhiFixed:{(0.0,0.9)}", 50, Kilovars(24.216)), + ("cosPhiFixed:{(0.0,0.9)}", 100, Kilovars(48.432)), + ("cosPhiFixed:{(0.0,0.9)}", 200, Kilovars(0)), + ("cosPhiFixed:{(0.0,0.9)}", -50, Kilovars(-24.216)), + ("cosPhiFixed:{(0.0,0.9)}", -100, Kilovars(-48.432)), + ("cosPhiFixed:{(0.0,0.9)}", -200, Kilovars(0)), + ("cosPhiFixed:{(0.0,1.0)}", 100, Kilovars(0)), + ) + + forAll(testCases) { (varCharacteristicString, pVal, qSol) => + val loadMock = new MockParticipant( + UUID.fromString("b69f6675-5284-4e28-add5-b76952ec1ec2"), + "System participant calculateQ Test", + OperationInterval(0L, 86400L), + QControl(new CosPhiFixed(varCharacteristicString)), + Kilowatts(200), + 1d, + ) + val power = Kilowatts(pVal) + val qCalc = loadMock.calculateReactivePower(power, adjustedVoltage) + qCalc should approximate(qSol) + } + } + } + + "calculate reactive power correctly for cosphi_p" in { + + val adjustedVoltage = + Each(1) // needed for method call but not applicable for cos phi_p + + val testCases = Table( + ("varCharacteristicString", "pVal", "qSol"), + ( + "cosPhiP:{(0,1),(0.05,1),(0.1,1),(0.15,1),(0.2,1),(0.25,1),(0.3,1),(0.35,1),(0.4,1),(0.45,1),(0.5,1),(0.55,0.99),(0.6,0.98),(0.65,0.97),(0.7,0.96),(0.75,0.95),(0.8,0.94),(0.85,0.93),(0.9,0.92),(0.95,0.91),(1,0.9)}", + 100, + Kilovars(20.099), + ), + ( + "cosPhiP:{(0,-1),(0.05,-1),(0.1,-1),(0.15,-1),(0.2,-1),(0.25,-1),(0.3,-1),(0.35,-1),(0.4,-1),(0.45,-1),(0.5,-1),(0.55,-0.99),(0.6,-0.98),(0.65,-0.97),(0.7,-0.96),(0.75,-0.95),(0.8,-0.94),(0.85,-0.93),(0.9,-0.92),(0.95,-0.91),(1,-0.9)}", + 100, + Kilovars(-20.099), + ), + ) + + // first line is "with P" -> negative Q (influence on voltage level: increase) is expected + // second line is "against P" -> positive Q (influence on voltage level: decrease) is expected + + forAll(testCases) { (varCharacteristicString, pVal, qSol) => + val loadMock = new MockParticipant( + UUID.fromString("30f84d97-83b4-4b71-9c2d-dbc7ebb1127c"), + "Generation calculateQ Test", + OperationInterval(0L, 86400L), + QControl( + new CosPhiP(varCharacteristicString) + ), + Kilowatts(102), + 1d, + ) + val power = Kilowatts(pVal) + val qCalc = loadMock.calculateReactivePower(power, adjustedVoltage) + qCalc should approximate(qSol) + } + } + + "calculate reactive power correctly for generation unit with cosphi_p" in { + val adjustedVoltage = + Each(1) // needed for method call but not applicable for cos phi_p + + val testCases = Table( + ("varCharacteristicString", "pVal", "qSol"), + ( + "cosPhiP:{(-1,0.9),(-0.95,0.91),(-0.9,0.92),(-0.85,0.93),(-0.8,0.94),(-0.75,0.95),(-0.7,0.96),(-0.65,0.97),(-0.6,0.98),(-0.55,0.99),(-0.5,1),(-0.45,1),(-0.4,1),(-0.35,1),(-0.3,1),(-0.25,1),(-0.2,1),(-0.15,1),(-0.1,1),(-0.05,1),(0,1)}", + -100, + Kilovars(-14.177), + ), + ( + "cosPhiP:{(-1,-0.9),(-0.95,-0.91),(-0.9,-0.92),(-0.85,-0.93),(-0.8,-0.94),(-0.75,-0.95),(-0.7,-0.96),(-0.65,-0.97),(-0.6,-0.98),(-0.55,-0.99),(-0.5,-1),(-0.45,-1),(-0.4,-1),(-0.35,-1),(-0.3,-1),(-0.25,-1),(-0.2,-1),(-0.15,-1),(-0.1,-1),(-0.05,-1),(0,-1)}", + -100, + Kilovars(14.177), + ), + ) + + // first line is "with P" -> negative Q (influence on voltage level: increase) is expected + // second line is "against P" -> positive Q (influence on voltage level: decrease) is expected + + forAll(testCases) { (varCharacteristicString, pVal, qSol) => + val loadMock = new MockParticipant( + UUID.fromString("30f84d97-83b4-4b71-9c2d-dbc7ebb1127c"), + "Generation calculateQ Test", + OperationInterval(0L, 86400L), + QControl( + new CosPhiP(varCharacteristicString) + ), + Kilowatts(101), + 1d, + ) + val power = Kilowatts(pVal) + val qCalc = loadMock.calculateReactivePower(power, adjustedVoltage) + qCalc should approximate(qSol) + + } + } + + "calculate reactive power correctly for a standard q_v characteristic" in { + val loadMock = new MockParticipant( + UUID.fromString("d8461624-d142-4360-8e02-c21965ec555e"), + "System participant calculateQ Test", + OperationInterval(0L, 86400L), + QControl(new QV("qV:{(0.93,-1),(0.97,0),(1,0),(1.03,0),(1.07,1)}")), + Kilowatts(200), + 0.98, + ) + + val testCases = Table( + ("adjustedVoltageVal", "qSol"), + (0.9, Kilovars(-39.799)), + (0.93, Kilovars(-39.799)), + (0.95, Kilovars(-19.899)), + (0.97, Kilovars(0)), + (1.00, Kilovars(0)), + (1.03, Kilovars(0)), + (1.05, Kilovars(19.899)), + (1.07, Kilovars(39.799)), + (1.1, Kilovars(39.799)), + ) + + forAll(testCases) { (adjustedVoltageVal, qSol) => + val adjustedVoltage = Each(adjustedVoltageVal) + val p = Kilowatts(42) + val qCalc = loadMock.calculateReactivePower(p, adjustedVoltage) + qCalc should approximate(qSol) + } + } + + "calculate reactive power correctly for q_v characteristic if active power is zero and cosPhiRated is 1" in { + val loadMock = new MockParticipant( + UUID.fromString("d8461624-d142-4360-8e02-c21965ec555e"), + "System participant calculateQ Test", + OperationInterval(0L, 86400L), + QControl(new QV("qV:{(0.93,-1),(0.97,0),(1,0),(1.03,0),(1.07,1)}")), + Kilowatts(200), + 1d, + ) + + val testCases = Table( + ("adjustedVoltageVal", "qSol"), + (0.9, Kilovars(0)), + (0.93, Kilovars(0)), + (0.95, Kilovars(0)), + (0.97, Kilovars(0)), + (1.00, Kilovars(0)), + (1.03, Kilovars(0)), + (1.05, Kilovars(0)), + (1.07, Kilovars(0)), + (1.1, Kilovars(0)), + ) + + forAll(testCases) { (adjustedVoltageVal, qSol) => + val adjustedVoltage = Each(adjustedVoltageVal) + val p = Kilowatts(0) + val qCalc = loadMock.calculateReactivePower(p, adjustedVoltage) + qCalc should approximate(qSol) + } + } + + "calculate reactive power correctly for q_v characteristic if active power is not zero and cosPhiRated is 0.95" in { + val loadMock = new MockParticipant( + UUID.fromString("d8461624-d142-4360-8e02-c21965ec555e"), + "System participant calculateQ Test", + OperationInterval(0L, 86400L), + QControl(new QV("qV:{(0.93,-1),(0.97,0),(1,0),(1.03,0),(1.07,1)}")), + Kilowatts(200), + 0.95, + ) + + val testCases = Table( + ("adjustedVoltageVal", "qSol"), + (0.9, Kilovars(-62.44)), + (0.93, Kilovars(-62.44)), + (0.95, Kilovars(-31.22)), + (0.97, Kilovars(0)), + (1.00, Kilovars(0)), + (1.03, Kilovars(0)), + (1.05, Kilovars(31.22)), + (1.07, Kilovars(62.44)), + (1.1, Kilovars(62.44)), + ) + + forAll(testCases) { (adjustedVoltageVal, expectedQ) => + val adjustedVoltage = Each(adjustedVoltageVal) + val p = Kilowatts(100) + val qCalc = loadMock.calculateReactivePower(p, adjustedVoltage) + qCalc should approximate(expectedQ) + } + } + + "calculate reactive power correctly for a standard q_v characteristic if active power is 195 and cosPhiRated is 0.95" in { + val activePower: Power = Kilowatts(195) + val loadMock = new MockParticipant( + UUID.fromString("d8461624-d142-4360-8e02-c21965ec555e"), + "System participant calculateQ Test", + OperationInterval(0L, 86400L), + QControl(new QV("qV:{(0.93,-1),(0.97,0),(1,0),(1.03,0),(1.07,1)}")), + Kilowatts(200), + 0.95, + ) + + val testCases = Table( + ("adjustedVoltageVal", "qSol"), + (0.9, Kilovars(-44.44)), + (0.93, Kilovars(-44.44)), + (0.95, Kilovars(-31.22)), + (0.97, Kilovars(0)), + (1.00, Kilovars(0)), + (1.03, Kilovars(0)), + (1.05, Kilovars(31.22)), + (1.07, Kilovars(44.44)), + (1.1, Kilovars(44.44)), + ) + + forAll(testCases) { (adjustedVoltageVal, qSol) => + val adjustedVoltage: Dimensionless = Each(adjustedVoltageVal) + val qCalc = loadMock.calculateReactivePower(activePower, adjustedVoltage) + qCalc should approximate(qSol) + } + } +} diff --git a/src/test/scala/edu/ie3/simona/model/thermal/CylindricalThermalStorageSpec.scala b/src/test/scala/edu/ie3/simona/model/thermal/CylindricalThermalStorageSpec.scala new file mode 100644 index 0000000000..52cbb30f79 --- /dev/null +++ b/src/test/scala/edu/ie3/simona/model/thermal/CylindricalThermalStorageSpec.scala @@ -0,0 +1,261 @@ +/* + * © 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.thermal + +import edu.ie3.datamodel.models.StandardUnits +import edu.ie3.datamodel.models.input.thermal.CylindricalStorageInput +import edu.ie3.simona.test.common.UnitSpec +import edu.ie3.util.quantities.PowerSystemUnits +import edu.ie3.util.scala.quantities.KilowattHoursPerKelvinCubicMeters +import org.scalatest.BeforeAndAfterAll +import org.scalatest.matchers.should.Matchers +import squants.Energy +import squants.energy.{KilowattHours, Kilowatts} +import squants.space.{CubicMeters, Volume} +import squants.thermal.Celsius +import tech.units.indriya.quantity.Quantities.getQuantity +import tech.units.indriya.unit.Units + +import java.util.UUID + +class CylindricalThermalStorageSpec + extends UnitSpec + with Matchers + with BeforeAndAfterAll { + + implicit val tolerance: Energy = KilowattHours(1e-10) + + lazy val storageInput = new CylindricalStorageInput( + UUID.randomUUID(), + "ThermalStorage", + null, + getQuantity(100, StandardUnits.VOLUME), + getQuantity(20, StandardUnits.VOLUME), + getQuantity(30, StandardUnits.TEMPERATURE), + getQuantity(40, StandardUnits.TEMPERATURE), + getQuantity(1.15, StandardUnits.SPECIFIC_HEAT_CAPACITY), + ) + + def buildThermalStorage( + storageInput: CylindricalStorageInput, + volume: Volume, + ): CylindricalThermalStorage = { + val storedEnergy = CylindricalThermalStorage.volumeToEnergy( + volume, + KilowattHoursPerKelvinCubicMeters( + storageInput.getC + .to(PowerSystemUnits.KILOWATTHOUR_PER_KELVIN_TIMES_CUBICMETRE) + .getValue + .doubleValue + ), + Celsius( + storageInput.getInletTemp.to(Units.CELSIUS).getValue.doubleValue() + ), + Celsius( + storageInput.getReturnTemp.to(Units.CELSIUS).getValue.doubleValue() + ), + ) + CylindricalThermalStorage(storageInput, storedEnergy) + } + + def vol2Energy(volume: Volume): Energy = { + CylindricalThermalStorage.volumeToEnergy( + volume, + KilowattHoursPerKelvinCubicMeters( + storageInput.getC + .to(PowerSystemUnits.KILOWATTHOUR_PER_KELVIN_TIMES_CUBICMETRE) + .getValue + .doubleValue + ), + Celsius( + storageInput.getInletTemp.to(Units.CELSIUS).getValue.doubleValue() + ), + Celsius( + storageInput.getReturnTemp.to(Units.CELSIUS).getValue.doubleValue() + ), + ) + } + + "CylindricalThermalStorage Model" should { + + "Check storage level operations:" in { + val storage = buildThermalStorage(storageInput, CubicMeters(70)) + + val initialLevel = storage._storedEnergy + storage._storedEnergy_=(vol2Energy(CubicMeters(50))) + val newLevel1 = storage._storedEnergy + val surplus = + storage.tryToStoreAndReturnRemainder(vol2Energy(CubicMeters(55))) + val newLevel2 = storage._storedEnergy + val isCovering = storage.isDemandCoveredByStorage(KilowattHours(5)) + val lack = storage.tryToTakeAndReturnLack(vol2Energy(CubicMeters(95))) + val newLevel3 = storage._storedEnergy + val notCovering = storage.isDemandCoveredByStorage(KilowattHours(1)) + + initialLevel should approximate(vol2Energy(CubicMeters(70))) + newLevel1 should approximate(vol2Energy(CubicMeters(50))) + surplus.value shouldBe vol2Energy(CubicMeters(5)) + newLevel2 should approximate(vol2Energy(CubicMeters(100))) + lack.value shouldBe vol2Energy(CubicMeters(15)) + newLevel3 should approximate(vol2Energy(CubicMeters(20))) + isCovering shouldBe true + notCovering shouldBe false + } + + "Converting methods work correctly" in { + val storage = buildThermalStorage(storageInput, CubicMeters(70)) + + val usableThermalEnergy = storage.usableThermalEnergy + usableThermalEnergy should approximate(KilowattHours(5 * 115)) + } + + "Apply, validation, and build method work correctly" in { + val storage = buildThermalStorage(storageInput, CubicMeters(70)) + + storage.uuid shouldBe storageInput.getUuid + storage.id shouldBe storageInput.getId + storage.operatorInput shouldBe storageInput.getOperator + storage.operationTime shouldBe storageInput.getOperationTime + storage.bus shouldBe storageInput.getThermalBus + } + + "Check mutable state update correctly update the state with thresholds" in { + val cases = Table( + ( + "tick", + "storedEnergy", + "qDot", + "newTick", + "newQDot", + "expectedStoredEnergy", + "expectedThreshold", + ), + ( + 0L, + 250.0, + 10.0, + 3600L, + 42.0, + 260.0, + ThermalStorage.ThermalStorageThreshold.StorageFull(79886L), + ), + ( + 0L, + 250.0, + 10.0, + 3600L, + -42.0, + 260.0, + ThermalStorage.ThermalStorageThreshold.StorageEmpty(6171L), + ), + ( + 0L, + 250.0, + -10.0, + 3600L, + 42.0, + 240.0, + ThermalStorage.ThermalStorageThreshold.StorageFull(81600L), + ), + ( + 0L, + 250.0, + -10.0, + 3600L, + -42.0, + 240.0, + ThermalStorage.ThermalStorageThreshold.StorageEmpty(4457L), + ), + ( + 0L, + 1000.0, + 149.0, + 3600L, + 5000.0, + 1149.0, + ThermalStorage.ThermalStorageThreshold.StorageFull(3601L), + ), + ( + 0L, + 240.0, + -9.0, + 3600L, + -5000.0, + 231.0, + ThermalStorage.ThermalStorageThreshold.StorageEmpty(3601L), + ), + ) + + forAll(cases) { + ( + tick, + storedEnergy, + qDot, + newTick, + newQDot, + expectedStoredEnergy, + expectedThreshold, + ) => + val storage = buildThermalStorage(storageInput, CubicMeters(70)) + val lastState = ThermalStorage.ThermalStorageState( + tick, + KilowattHours(storedEnergy), + Kilowatts(qDot), + ) + val result = + storage.updateState(newTick, Kilowatts(newQDot), lastState) + + result._1.storedEnergy should approximate( + KilowattHours(expectedStoredEnergy) + ) + + result._2 match { + case Some(threshold) => threshold shouldBe expectedThreshold + case None => fail("Expected a threshold but got None") + } + } + + } + + "Check mutable state update, if no threshold is reached update state without hitting a threshold" in { + val cases = Table( + ( + "tick", + "storedEnergy", + "qDot", + "newTick", + "newQDot", + "expectedStoredEnergy", + ), + (0L, 250.0, 10.0, 3600L, 0.0, 260.0), + (0L, 250.0, -10.0, 3600L, 0.0, 240.0), + ) + + forAll(cases) { + (tick, storedEnergy, qDot, newTick, newQDot, expectedStoredEnergy) => + val storage = buildThermalStorage(storageInput, CubicMeters(70)) + val lastState = ThermalStorage.ThermalStorageState( + tick, + KilowattHours(storedEnergy), + Kilowatts(qDot), + ) + val result = + storage.updateState(newTick, Kilowatts(newQDot), lastState) + + result._1.storedEnergy should approximate( + KilowattHours(expectedStoredEnergy) + ) + + result._2 match { + case Some(threshold) => + fail(s"Expected no threshold, but got: $threshold") + case None => succeed + } + } + } + } +} diff --git a/src/test/scala/edu/ie3/simona/test/common/model/MockParticipant.scala b/src/test/scala/edu/ie3/simona/test/common/model/MockParticipant.scala new file mode 100644 index 0000000000..ad407dbfb4 --- /dev/null +++ b/src/test/scala/edu/ie3/simona/test/common/model/MockParticipant.scala @@ -0,0 +1,74 @@ +/* + * © 2022. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.test.common.model + +import edu.ie3.simona.agent.participant.data.Data +import edu.ie3.simona.model.participant.control.QControl +import edu.ie3.simona.model.participant.{ + CalcRelevantData, + FlexChangeIndicator, + ModelState, + SystemParticipant, +} +import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage +import edu.ie3.util.scala.OperationInterval +import squants.Dimensionless +import squants.energy._ + +import java.util.UUID + +class MockParticipant( + uuid: UUID, + id: String, + operationInterval: OperationInterval, + qControl: QControl, + sRated: Power, + cosPhiRated: Double, +) extends SystemParticipant[ + CalcRelevantData, + Data.PrimaryData.ApparentPower, + ModelState, + ]( + uuid, + id, + operationInterval, + qControl, + sRated, + cosPhiRated, + ) { + + override def calculatePower( + tick: Long, + voltage: Dimensionless, + state: ModelState, + data: CalcRelevantData, + ): Data.PrimaryData.ApparentPower = { + super.calculateApparentPower(tick, voltage, state, data) + } + + override def calculateActivePower( + maybeModelState: ModelState, + data: CalcRelevantData, + ): Power = { + Kilowatts(0) + } + + override def determineFlexOptions( + data: CalcRelevantData, + lastState: ModelState, + ): FlexibilityMessage.ProvideFlexOptions = { + null + } + + override def handleControlledPowerChange( + data: CalcRelevantData, + lastState: ModelState, + setPower: Power, + ): (ModelState, FlexChangeIndicator) = { + (lastState, FlexChangeIndicator(changesAtTick = None)) + } +}