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))
+ }
+}